From 45d9e738a5a8916408a39d5df3c0dcd201b9e1b0 Mon Sep 17 00:00:00 2001 From: Peter Iakovlev Date: Sat, 21 Apr 2018 00:20:56 +0400 Subject: [PATCH] no message --- .../MenuIcons/Proxy.imageset/Contents.json | 22 + .../Proxy.imageset/SettingsProxyIcon@2x.png | Bin 0 -> 1704 bytes .../Proxy.imageset/SettingsProxyIcon@3x.png | Bin 0 -> 2382 bytes TelegramUI.xcodeproj/project.pbxproj | 52 +- ...onSequenceCountrySelectionController.swift | 3 + TelegramUI/ChannelInfoController.swift | 27 +- TelegramUI/ChatButtonKeyboardInputNode.swift | 2 +- TelegramUI/ChatController.swift | 25 +- TelegramUI/ChatControllerInteraction.swift | 4 +- TelegramUI/ChatControllerNode.swift | 30 +- TelegramUI/ChatInputNode.swift | 2 +- TelegramUI/ChatListController.swift | 33 +- TelegramUI/ChatListTitleProxyNode.swift | 110 ++++ TelegramUI/ChatMediaInputGridEntries.swift | 99 +++- TelegramUI/ChatMediaInputNode.swift | 252 +++++++-- .../ChatMediaInputStickerGridItem.swift | 8 +- TelegramUI/ChatMediaInputStickerPane.swift | 3 +- TelegramUI/ChatMediaInputTrendingPane.swift | 1 + TelegramUI/ChatMessageBubbleItemNode.swift | 5 +- .../ChatPresentationInterfaceState.swift | 36 +- .../ChatRecentActionsControllerNode.swift | 2 +- .../ChatTextInputActionButtonsNode.swift | 2 +- TelegramUI/ChatTextInputAttributes.swift | 16 +- TelegramUI/ChatTextInputPanelNode.swift | 8 +- TelegramUI/ChatTitleView.swift | 14 +- TelegramUI/ComponentsThemes.swift | 6 - .../DataAndStorageSettingsController.swift | 8 +- .../DefaultDarkAccentPresentationTheme.swift | 4 + TelegramUI/DefaultDarkPresentationTheme.swift | 4 + TelegramUI/DefaultPresentationTheme.swift | 4 + TelegramUI/GalleryControllerNode.swift | 66 ++- TelegramUI/GalleryPagerNode.swift | 9 +- .../GalleryThumbnailContainerNode.swift | 109 +++- TelegramUI/GenerateTextEntities.swift | 20 +- TelegramUI/ItemListPeerItem.swift | 13 - TelegramUI/ItemListSingleLineInputItem.swift | 4 +- ...MediaNavigationAccessoryItemListNode.swift | 2 +- TelegramUI/NetworkStatusTitleView.swift | 94 +++- TelegramUI/OngoingCallContext.swift | 4 +- TelegramUI/OngoingCallThreadLocalContext.h | 13 +- TelegramUI/OngoingCallThreadLocalContext.mm | 21 +- TelegramUI/OpenResolvedUrl.swift | 10 +- TelegramUI/OpenUrl.swift | 4 +- TelegramUI/OverlayPlayerControllerNode.swift | 2 +- .../PeerMediaCollectionController.swift | 1 + TelegramUI/PresentationCall.swift | 4 +- TelegramUI/PresentationCallManager.swift | 19 +- TelegramUI/PresentationStrings.swift | 6 +- TelegramUI/PresentationTheme.swift | 10 +- TelegramUI/PresentationThemeSettings.swift | 4 +- TelegramUI/ProxyListSettingsController.swift | 420 +++++++++++++++ .../ProxyServerActionSheetController.swift | 328 ++++++++++++ ...ft => ProxyServerSettingsController.swift} | 187 ++----- TelegramUI/ProxySettingsActionItem.swift | 242 +++++++++ TelegramUI/ProxySettingsServerItem.swift | 494 ++++++++++++++++++ TelegramUI/Resources/PhoneCountries.txt | 5 +- TelegramUI/SettingsController.swift | 66 ++- .../StickerPackPreviewControllerNode.swift | 6 +- TelegramUI/StickerPackPreviewGridItem.swift | 4 +- TelegramUI/StickerPaneSearchBarNode.swift | 455 ++++++++++++++++ .../StickerPaneSearchBarPlaceholderItem.swift | 109 ++++ .../StickerPaneSearchContainerNode.swift | 413 +++++++++++++++ TelegramUI/StickerPaneSearchGlobaltem.swift | 272 ++++++++++ TelegramUI/StickerPaneSearchStickerItem.swift | 186 +++++++ TelegramUI/StickerPreviewPeekContent.swift | 22 +- TelegramUI/ThemeSettingsChatPreviewItem.swift | 2 +- TelegramUI/UrlEscaping.swift | 13 + TelegramUI/UrlHandling.swift | 2 +- TelegramUI/UserInfoController.swift | 27 +- 69 files changed, 4003 insertions(+), 447 deletions(-) create mode 100644 Images.xcassets/Settings/MenuIcons/Proxy.imageset/Contents.json create mode 100644 Images.xcassets/Settings/MenuIcons/Proxy.imageset/SettingsProxyIcon@2x.png create mode 100644 Images.xcassets/Settings/MenuIcons/Proxy.imageset/SettingsProxyIcon@3x.png create mode 100644 TelegramUI/ChatListTitleProxyNode.swift create mode 100644 TelegramUI/ProxyListSettingsController.swift create mode 100644 TelegramUI/ProxyServerActionSheetController.swift rename TelegramUI/{ProxySettingsController.swift => ProxyServerSettingsController.swift} (53%) create mode 100644 TelegramUI/ProxySettingsActionItem.swift create mode 100644 TelegramUI/ProxySettingsServerItem.swift create mode 100644 TelegramUI/StickerPaneSearchBarNode.swift create mode 100644 TelegramUI/StickerPaneSearchBarPlaceholderItem.swift create mode 100644 TelegramUI/StickerPaneSearchContainerNode.swift create mode 100644 TelegramUI/StickerPaneSearchGlobaltem.swift create mode 100644 TelegramUI/StickerPaneSearchStickerItem.swift create mode 100644 TelegramUI/UrlEscaping.swift diff --git a/Images.xcassets/Settings/MenuIcons/Proxy.imageset/Contents.json b/Images.xcassets/Settings/MenuIcons/Proxy.imageset/Contents.json new file mode 100644 index 0000000000..68533f8fc7 --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/Proxy.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "SettingsProxyIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "SettingsProxyIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/Proxy.imageset/SettingsProxyIcon@2x.png b/Images.xcassets/Settings/MenuIcons/Proxy.imageset/SettingsProxyIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..bda0b2552dd8f0dae92f8bd6ecdba104689ca75f GIT binary patch literal 1704 zcmV;Z23PrsP)Px*Vo5|nRA>e5T5D`nMHD`B@9y@o?b1@A(iWstC@4@(07U{Jt%@X+@(3|v&=@F~ zMq`YRXq1?k_|F&#CO(QmFd8eUeSo4oq6LjWV^lC@L|WJSyt}Krt1J?Zlrjc0fkOc>mcxN(FaRg#AP&K9 zc@twS0-T3{K_BYk!wy<7?`48;I3*%}yQ}!UOUXJ#edE)bD^(f&zTjrY`6kZ!n0Sio zuZCU%zW6T%m-E&7DerpK8pZ)#>D^Qk;-MN2P^>1lNx4^Ktg5bf+s|uP8R!^jviV5H zjR^x5#Km(#Lvi(SY3kC#bEAPI)cAF9;tc(e@{B$qzAoP-yR5FHwBE!-afv|R$r~A@ z^islnmB7QiOz#`A9X;b^0x*83?G)FkFxqO7Ah~rq8O@Mqi5{4eGSQ3({k$BQgM3$9 zne;R3^lv$o!`N+p5JJ7)G19rxq}>Z{cPGf=D24;MfQVm zmwQGG)6Dc~K>y2w|3H1`K{(KL1P1tE42_iqR_R9yJJj_P1R?|K=20(O2DIwJW(f1h zr~^u-Kum-o8IhKHj9O_S8#NlsfQ}jhjUC?vs_{kHhV15u+GeOUDr$H{BQWxx5^^7n zCc{9xaknwj$sg`9+@)cMfjXr(jAtHF=kFK3p;?KyTeoZ8ifzt+8Ya2&B-vfzZq0qD z2@M0KsoRZyt-KwZ;2f`(owV!=`Jr{NEor;lATkl6<@!tjH9U1mnX>`w_KPyO=w~6u zmMH8(DTw>jivvkd1*6##=1l=T(RV7LP^&8EXWp%dI^BQXRH(%i(BFp{=)%mo3Q)6s znF4wy@F%nkUQv{>GBY=0c8q;Cb{{t--V{&@Q#N1(Z#dE;IpvDjUwZ$5zXw~CvXP7_ zpw!_3|BuiY2^i@+DNpH``7kxLP-$<#4ZYc98fZU)thw4?Ru6?g$B_ja$FEh&wBR-M zo>0n0Gp2!3$F-gN;k>~BShV6P7!!IazY5$!np7Q(1mT_5kE2Np=PdzB&coK$9Y8Tz zqluh@7xOm3^wcSeh)>(=fnrfsBujvjB4;t!yruOcLpo)MZg{=mS(xW8QTV_2{|vi3 zzER3%GlpHjL>-fI;~hD$ZrsX5%?ayfr%#8cZhb7~Q$lvRrDc0UdZU$=0wpCr@7N0= z3|>Ee%PJ69@(ELvItf;deFzq1&Wq9gXwR?k=H+ThMofdr0vxhRPQLf5Eo48ZhhTC6X=I(XnACZY#6ois5+Pk#;*gjGj(O`V0`GrgJmyM(A{H zKc0;a_hB%N=mi|n$-@CqEt8HF7i7+XY=;L9;{jCjpz&>&TK2Wdv>1^SIr&ptFC1T yx(FT8A?anl5q+XFK;KFFgm5?ej250m{r?N_G$QSb&iQTt0000Px;2uVaiRCodHooR3sMHI*1Y%a1P2_z9Yf}luHDU%>rcve(A3xkSUq83;spjLre zSfzZx5AlOmiKQR#D3vdYMJXN#C?2S2)KWoF!VQ8FQNoe?*pRThPb;+_`q7=^)pG;i2J63t4!mUs%Qn&5@w}K--rIMN!E0MwIr;u`*%e{Q+q{4~{r2;0! zBoX1>|L3DQ%>myBZf!O3GC80Y=cIV=UOn;U=5S9FT!3u~`o3o)UlJbJ{U3#ZqHNFg zWR`^kHayK?3Q%KRuu&jJb5_+-a09>4AO^Uwv92iy#bf;(+`6ApJ&}6MLRYIm{lk5D z^{^2>(H?Rs$`rj7(OYx1=9b$k5GEQDL93X|QFqd6ZubkqcHPlE_KyI+Xq4hbL#w%X z^Xgx6ZtPG3I!_P!3h+uB>gk^Mk3e@9fugu3p?@Hw#fE9{pm!@;0M|W!0^sW5_8Gbj zT}dzjaAmm4_ynixCdUN8)x+JtBL@PF@fn45f5Gj9<3JE_<#%BoL2$54&AOOwAASRs zW?iIdIVb;C+TON@)-`-Z+uC+ISaUTG9Szw|(=c60qib_6r`s6n*bE*)3KM|F??^`t zt!K#JHvdeWD!Nx4Bs}D$lb`>G;Zs*E9LtZbSAsA26hy+v@B;jar-cC82n~LmCakU+I+a<-?-CBL_~Dr zpymd+=Js~rBqC*tX>{4s8~GORyEWLMc2cdsfySg+8ynr;wwLYlrli?ruDQWfyTSHl zPs6WE9RvNeaWj3<_&v=UK7;PfpA{Y?;(uH){E}st!8QJSFk+FSJBr%VLmkz$uKp|f zvE?`N^X(q5;DNY!NaEI-7Svp)1d&;(-wzgJjxAQLFfp~dFjE4s#nsImL-E5m( zmIil^buUpM{852CNBrt?HZIEiw^1M~XaE43f&^4j_@=nr4a&ky3xS{hv3 ziX+nUV=4xF`U%_9FB>bVHQMmR%Tw;M; zxMIA7Lc4HhG`RnC9yJvt*$xwij5nzuQ%`Adcl!46?0Wu6)j?A1on+7k`aw@w zA`NbHpp~jS|C6P+aJsO!#a`o=JT{$)|(K`|A5buCb-wu224TkkAKu$rkBw@ieY%*($pn5!1(a zVT(r2HTgjwnXLppL+Nml(ff59lTv%<6qM7r^f7u_wIgaR^sz&1fm?I>1g)w2)Q z; z$^6Q(RnlLHvr~rBGlffNcuKCJGZQ#jPlOoC?#; zlR{w*kiVT`D_j(8b?rLZ-1Kw8%Q!Y;G`)DvGAbH!t|@Kgx4Le<$z8UCw9fKaimy2K z7Uic7qY?{Kq++F``(2hjg_ewbki6U>A1-*sMSjT;+iNZg3A^{)(bwqjQ%Cw$W<0o- zjaf{~MlXs87U?4&JMC;}fs#^lp@1*gPD_tGPfw0|h%Oyk9I8{F=Vztm(M`k3vUZhv64U=_M7IPXw}KJ)DUQj71TaI z<_fcLl9em4FW0_vnvgM$7LK@wCVPwe42+pUD^IK;OlNhHaOJ3FvoQZ}woK!lq?oP? zx(my)r_)v0(+uk`V0{J539f9}LVMc}>cJg#P7B>+r9k$a^1r{#zkPczKsE5_rS#;Y~tgZ2apgKa09>4nCxbhuDyBMD|rmO)ac{> zi%P)C-aLqm&3mwkk6VMIqBMAbQ>0Cg5XfK_^jH8qb<)_GY@nZYA96Ys;}KlI2b{up zlhQ+)%!2NrAKjh&bP~Ns?XK?6PJX>fb)C6;P)gW?G8{6$+8K6F#j5gMi^^+jpJ(&n zmzVp82j=dns){uadD5~{kA7}+$8s%=d-v4;0j6GDFYF<(@&Et;07*qoM6N<$f(+QB AB>(^b literal 0 HcmV?d00001 diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index a478856b50..5eeda93104 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ D007019E2029EFDD006B9E34 /* ICloudResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D007019D2029EFDD006B9E34 /* ICloudResources.swift */; }; D00701A12029F6D0006B9E34 /* TGMimeTypeMap.h in Headers */ = {isa = PBXBuildFile; fileRef = D007019F2029F6D0006B9E34 /* TGMimeTypeMap.h */; }; D00701A22029F6D0006B9E34 /* TGMimeTypeMap.m in Sources */ = {isa = PBXBuildFile; fileRef = D00701A02029F6D0006B9E34 /* TGMimeTypeMap.m */; }; + D00781052084DFB100369A39 /* UrlEscaping.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00781042084DFB100369A39 /* UrlEscaping.swift */; }; D00ACA4B20222C280045D427 /* libtgvoip.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D00ACA4C20222C280045D427 /* libtgvoip.framework */; }; D00ACA58202285090045D427 /* ChatRestrictedNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00ACA57202285090045D427 /* ChatRestrictedNode.swift */; }; D00ACA5A2022897D0045D427 /* ProcessedPeerRestrictionText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00ACA592022897D0045D427 /* ProcessedPeerRestrictionText.swift */; }; @@ -35,6 +36,9 @@ D01776BE1F1E76920044446D /* PeerMediaCollectionSectionsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01776BD1F1E76920044446D /* PeerMediaCollectionSectionsNode.swift */; }; D018477E1FFBC01E00075256 /* TimestampStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D018477D1FFBC01E00075256 /* TimestampStrings.swift */; }; D01847801FFBD12E00075256 /* ChatListPresentationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D018477F1FFBD12E00075256 /* ChatListPresentationData.swift */; }; + D0185E882089ED5F005E1A6C /* ProxyListSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0185E872089ED5F005E1A6C /* ProxyListSettingsController.swift */; }; + D0185E8A208A01AF005E1A6C /* ProxySettingsActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0185E89208A01AF005E1A6C /* ProxySettingsActionItem.swift */; }; + D0185E8C208A025A005E1A6C /* ProxySettingsServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0185E8B208A025A005E1A6C /* ProxySettingsServerItem.swift */; }; D01A21AF1F39EA2E00DDA104 /* InstantPageTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01A21AE1F39EA2E00DDA104 /* InstantPageTheme.swift */; }; D01A21B11F3A050E00DDA104 /* InstantPageNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01A21B01F3A050E00DDA104 /* InstantPageNavigationBar.swift */; }; D01BAA181ECC8E0000295217 /* CallListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01BAA171ECC8E0000295217 /* CallListController.swift */; }; @@ -65,6 +69,8 @@ D025A4231F79344500563950 /* FetchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025A4221F79344500563950 /* FetchManager.swift */; }; D025A4261F79428E00563950 /* FetchManagerLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D025A4251F79428E00563950 /* FetchManagerLocation.swift */; }; D02660941F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02660931F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift */; }; + D02B2B9820810DA00062476B /* StickerPaneSearchStickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02B2B9720810DA00062476B /* StickerPaneSearchStickerItem.swift */; }; + D02B676320800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02B676220800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift */; }; D02D60AE206BD47300FEFE1E /* SecureIdDocumentTypeSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D60AD206BD47300FEFE1E /* SecureIdDocumentTypeSelectionController.swift */; }; D02D60B1206C189900FEFE1E /* SecureIdPlaintextFormController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D60B0206C189900FEFE1E /* SecureIdPlaintextFormController.swift */; }; D02D60B3206C18A600FEFE1E /* SecureIdPlaintextFormControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D60B2206C18A600FEFE1E /* SecureIdPlaintextFormControllerNode.swift */; }; @@ -173,6 +179,8 @@ D07ABBA5202A14BC003671DE /* LegacyImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07ABBA4202A14BC003671DE /* LegacyImagePicker.swift */; }; D07ABBAB202A1BD1003671DE /* LegacyWallpaperEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07ABBAA202A1BD1003671DE /* LegacyWallpaperEditor.swift */; }; D07BCBFE1F2B792300ED97AA /* LegacyComponents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D07BCBFD1F2B792300ED97AA /* LegacyComponents.framework */; }; + D07E413B208A432100FCA8F0 /* ChatListTitleProxyNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07E413A208A432100FCA8F0 /* ChatListTitleProxyNode.swift */; }; + D07E413D208A494D00FCA8F0 /* ProxyServerActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07E413C208A494D00FCA8F0 /* ProxyServerActionSheetController.swift */; }; D080B27F1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */; }; D087BFAD1F741B9D003FD209 /* ShareContentContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFAC1F741B9D003FD209 /* ShareContentContainerNode.swift */; }; D087BFAF1F741BB7003FD209 /* ShareLoadingContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087BFAE1F741BB7003FD209 /* ShareLoadingContainerNode.swift */; }; @@ -227,6 +235,9 @@ D0AD02E81FFFDE5F00C1DCFF /* ChatMessageLiveLocationTimerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AD02E71FFFDE5F00C1DCFF /* ChatMessageLiveLocationTimerNode.swift */; }; D0AD02EA1FFFEBEF00C1DCFF /* ChatMessageLiveLocationTextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AD02E91FFFEBEF00C1DCFF /* ChatMessageLiveLocationTextNode.swift */; }; D0AD02EC20000D0100C1DCFF /* ChatMessageLiveLocationPositionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AD02EB20000D0100C1DCFF /* ChatMessageLiveLocationPositionNode.swift */; }; + D0AEAE252080D6830013176E /* StickerPaneSearchContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AEAE242080D6830013176E /* StickerPaneSearchContainerNode.swift */; }; + D0AEAE272080D6970013176E /* StickerPaneSearchBarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AEAE262080D6970013176E /* StickerPaneSearchBarNode.swift */; }; + D0AEAE292080FD660013176E /* StickerPaneSearchGlobaltem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AEAE282080FD660013176E /* StickerPaneSearchGlobaltem.swift */; }; D0AF323A1FB1D8D60097362B /* ChatOverlayNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AF32391FB1D8D60097362B /* ChatOverlayNavigationBar.swift */; }; D0AF7C461ED84BC500CD8E0F /* LanguageSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AF7C451ED84BC500CD8E0F /* LanguageSelectionController.swift */; }; D0AF7C4A1ED84CE000CD8E0F /* LanguageSelectionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0AF7C491ED84CE000CD8E0F /* LanguageSelectionControllerNode.swift */; }; @@ -283,7 +294,7 @@ D0CFBB911FD881A600B65C0D /* AudioRecordningToneData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CFBB901FD881A600B65C0D /* AudioRecordningToneData.swift */; }; D0CFBB951FD8B05000B65C0D /* OverlayInstantVideoDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CFBB941FD8B05000B65C0D /* OverlayInstantVideoDecoration.swift */; }; D0CFBB971FD8B0F700B65C0D /* ChatBubbleInstantVideoDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CFBB961FD8B0F700B65C0D /* ChatBubbleInstantVideoDecoration.swift */; }; - D0D4345C1F97CEAA00CC1806 /* ProxySettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4345B1F97CEAA00CC1806 /* ProxySettingsController.swift */; }; + D0D4345C1F97CEAA00CC1806 /* ProxyServerSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4345B1F97CEAA00CC1806 /* ProxyServerSettingsController.swift */; }; D0DE5803205AEB7600C356A8 /* include in Resources */ = {isa = PBXBuildFile; fileRef = D0DE5802205AEB7600C356A8 /* include */; }; D0DE5805205B202500C356A8 /* ScreenCaptureDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DE5804205B202500C356A8 /* ScreenCaptureDetection.swift */; }; D0DE66061F9A51E200EF4AE9 /* GalleryHiddenMediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DE66051F9A51E200EF4AE9 /* GalleryHiddenMediaManager.swift */; }; @@ -953,6 +964,7 @@ D007019D2029EFDD006B9E34 /* ICloudResources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICloudResources.swift; sourceTree = ""; }; D007019F2029F6D0006B9E34 /* TGMimeTypeMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMimeTypeMap.h; sourceTree = ""; }; D00701A02029F6D0006B9E34 /* TGMimeTypeMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMimeTypeMap.m; sourceTree = ""; }; + D00781042084DFB100369A39 /* UrlEscaping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlEscaping.swift; sourceTree = ""; }; D00ACA4C20222C280045D427 /* libtgvoip.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libtgvoip.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D00ACA57202285090045D427 /* ChatRestrictedNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRestrictedNode.swift; sourceTree = ""; }; D00ACA592022897D0045D427 /* ProcessedPeerRestrictionText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedPeerRestrictionText.swift; sourceTree = ""; }; @@ -1008,6 +1020,9 @@ D0177B831DFB095000A5083A /* FileMediaResourceStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileMediaResourceStatus.swift; sourceTree = ""; }; D018477D1FFBC01E00075256 /* TimestampStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimestampStrings.swift; sourceTree = ""; }; D018477F1FFBD12E00075256 /* ChatListPresentationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListPresentationData.swift; sourceTree = ""; }; + D0185E872089ED5F005E1A6C /* ProxyListSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyListSettingsController.swift; sourceTree = ""; }; + D0185E89208A01AF005E1A6C /* ProxySettingsActionItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxySettingsActionItem.swift; sourceTree = ""; }; + D0185E8B208A025A005E1A6C /* ProxySettingsServerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxySettingsServerItem.swift; sourceTree = ""; }; D018D3311E6460B300C5E089 /* ChatUnblockInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatUnblockInputPanelNode.swift; sourceTree = ""; }; D018D3341E6489EC00C5E089 /* CreateChannelController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateChannelController.swift; sourceTree = ""; }; D01A21AE1F39EA2E00DDA104 /* InstantPageTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageTheme.swift; sourceTree = ""; }; @@ -1095,6 +1110,8 @@ D025A4251F79428E00563950 /* FetchManagerLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchManagerLocation.swift; sourceTree = ""; }; D02660931F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LegacyLocationVenueIconDataSource.swift; path = "../third-party/RMIntro/LegacyLocationVenueIconDataSource.swift"; sourceTree = ""; }; D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapLongTapOrDoubleTapGestureRecognizer.swift; sourceTree = ""; }; + D02B2B9720810DA00062476B /* StickerPaneSearchStickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPaneSearchStickerItem.swift; sourceTree = ""; }; + D02B676220800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPaneSearchBarPlaceholderItem.swift; sourceTree = ""; }; D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = ""; }; D02BE0761D9190EF000889C2 /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.swift; sourceTree = ""; }; D02D60AD206BD47300FEFE1E /* SecureIdDocumentTypeSelectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdDocumentTypeSelectionController.swift; sourceTree = ""; }; @@ -1375,6 +1392,8 @@ D07CFF7C1DCA273400761F81 /* ChatListViewTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListViewTransition.swift; sourceTree = ""; }; D07CFF7E1DCA308500761F81 /* ChatListNodeLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListNodeLocation.swift; sourceTree = ""; }; D07CFF861DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForwardAccessoryPanelNode.swift; sourceTree = ""; }; + D07E413A208A432100FCA8F0 /* ChatListTitleProxyNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListTitleProxyNode.swift; sourceTree = ""; }; + D07E413C208A494D00FCA8F0 /* ProxyServerActionSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyServerActionSheetController.swift; sourceTree = ""; }; D080B27E1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageManagedMediaId.swift; sourceTree = ""; }; D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListEditableDeleteControlNode.swift; sourceTree = ""; }; D08774F91E3E2A5600A97350 /* ItemListCheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListCheckboxItem.swift; sourceTree = ""; }; @@ -1468,6 +1487,9 @@ D0AD02E71FFFDE5F00C1DCFF /* ChatMessageLiveLocationTimerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageLiveLocationTimerNode.swift; sourceTree = ""; }; D0AD02E91FFFEBEF00C1DCFF /* ChatMessageLiveLocationTextNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageLiveLocationTextNode.swift; sourceTree = ""; }; D0AD02EB20000D0100C1DCFF /* ChatMessageLiveLocationPositionNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageLiveLocationPositionNode.swift; sourceTree = ""; }; + D0AEAE242080D6830013176E /* StickerPaneSearchContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPaneSearchContainerNode.swift; sourceTree = ""; }; + D0AEAE262080D6970013176E /* StickerPaneSearchBarNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPaneSearchBarNode.swift; sourceTree = ""; }; + D0AEAE282080FD660013176E /* StickerPaneSearchGlobaltem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPaneSearchGlobaltem.swift; sourceTree = ""; }; D0AF32391FB1D8D60097362B /* ChatOverlayNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatOverlayNavigationBar.swift; sourceTree = ""; }; D0AF7C451ED84BC500CD8E0F /* LanguageSelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageSelectionController.swift; sourceTree = ""; }; D0AF7C491ED84CE000CD8E0F /* LanguageSelectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageSelectionControllerNode.swift; sourceTree = ""; }; @@ -1590,7 +1612,7 @@ D0D2686B1D788F8200C422DA /* ChatTitleAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTitleAccessoryPanelNode.swift; sourceTree = ""; }; D0D2686D1D7898A900C422DA /* ChatMessageSelectionNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionNode.swift; sourceTree = ""; }; D0D268991D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPanelInterfaceInteraction.swift; sourceTree = ""; }; - D0D4345B1F97CEAA00CC1806 /* ProxySettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxySettingsController.swift; sourceTree = ""; }; + D0D4345B1F97CEAA00CC1806 /* ProxyServerSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyServerSettingsController.swift; sourceTree = ""; }; D0D748051E7AF63800F4B1F6 /* StickerPackPreviewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPackPreviewController.swift; sourceTree = ""; }; D0D748071E7AF64400F4B1F6 /* StickerPackPreviewControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPackPreviewControllerNode.swift; sourceTree = ""; }; D0D7480E1E7B1BD600F4B1F6 /* StickerPackPreviewGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPackPreviewGridItem.swift; sourceTree = ""; }; @@ -2146,6 +2168,11 @@ D002A0DA1E9C190700A81812 /* SoftwareVideoThumbnailLayer.swift */, D0575AEC1E9FF1AD006F2541 /* ChatMediaInputTrendingPane.swift */, D04203142037162700490EA5 /* MediaInputPaneTrendingItem.swift */, + D02B676220800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift */, + D0AEAE262080D6970013176E /* StickerPaneSearchBarNode.swift */, + D0AEAE242080D6830013176E /* StickerPaneSearchContainerNode.swift */, + D0AEAE282080FD660013176E /* StickerPaneSearchGlobaltem.swift */, + D02B2B9720810DA00062476B /* StickerPaneSearchStickerItem.swift */, ); name = Media; sourceTree = ""; @@ -3018,10 +3045,14 @@ D0223A951EA54D0D00211D94 /* VoiceCallDataSavingController.swift */, D0223A9D1EA5732300211D94 /* NetworkUsageStatsController.swift */, D0FA35001EA6127000E56FFA /* StorageUsageController.swift */, - D0D4345B1F97CEAA00CC1806 /* ProxySettingsController.swift */, D0DFD5E11FCE2BA50039B3B1 /* CalculatingCacheSizeItem.swift */, D06D37A82077DDF3009219B6 /* AutodownloadMediaCategoryController.swift */, D06D37B12077E77F009219B6 /* AutodownloadSizeLimitItem.swift */, + D0185E872089ED5F005E1A6C /* ProxyListSettingsController.swift */, + D0D4345B1F97CEAA00CC1806 /* ProxyServerSettingsController.swift */, + D0185E89208A01AF005E1A6C /* ProxySettingsActionItem.swift */, + D0185E8B208A025A005E1A6C /* ProxySettingsServerItem.swift */, + D07E413C208A494D00FCA8F0 /* ProxyServerActionSheetController.swift */, ); name = "Data and Storage"; sourceTree = ""; @@ -3668,6 +3699,7 @@ D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */, D01749611E11DB240057C89A /* NetworkStatusTitleView.swift */, D0575AEA1E9FD579006F2541 /* ChatListTitleLockView.swift */, + D07E413A208A432100FCA8F0 /* ChatListTitleProxyNode.swift */, D0F69E051D6B8A8B0046BCD6 /* Search */, ); name = "Chat List"; @@ -3986,6 +4018,7 @@ D0FA08BD20481EA300DD23FC /* Locale.swift */, D0DE5804205B202500C356A8 /* ScreenCaptureDetection.swift */, D0BE3036206139F500FBE6D8 /* ImageCompression.swift */, + D00781042084DFB100369A39 /* UrlEscaping.swift */, ); name = Utils; sourceTree = ""; @@ -4433,6 +4466,7 @@ D0EC6CEC1EB9F58800EBF1C3 /* PresentationThemeEssentialGraphics.swift in Sources */, D01BAA1E1ECC931D00295217 /* CallListNodeEntries.swift in Sources */, D0EC6CED1EB9F58800EBF1C3 /* StringPluralization.swift in Sources */, + D02B2B9820810DA00062476B /* StickerPaneSearchStickerItem.swift in Sources */, D020A9DC1FEAE6E7008C66F7 /* OverlayPlayerControllerNode.swift in Sources */, D04B26EC20082EB50053A58C /* LocationBroadcastPanelWavesNode.swift in Sources */, D0EC6CEE1EB9F58800EBF1C3 /* InAppNotificationSettings.swift in Sources */, @@ -4477,6 +4511,7 @@ D093D7DF2062F3F000BC3599 /* SecureIdDocumentFormController.swift in Sources */, D0E9BA371F05585000F079A4 /* STPPhoneNumberValidator.m in Sources */, D0EC6D041EB9F58800EBF1C3 /* opusenc.m in Sources */, + D0185E8A208A01AF005E1A6C /* ProxySettingsActionItem.swift in Sources */, D0EC6D051EB9F58800EBF1C3 /* picture.c in Sources */, D0EC6D061EB9F58800EBF1C3 /* wav_io.c in Sources */, D0EC6D071EB9F58800EBF1C3 /* bitwise.c in Sources */, @@ -4567,6 +4602,7 @@ D0EC6D361EB9F58800EBF1C3 /* SearchBarPlaceholderNode.swift in Sources */, D0E8B8B9204477B600605593 /* SecretChatKeyVisualization.swift in Sources */, D0EC6D371EB9F58800EBF1C3 /* SearchDisplayController.swift in Sources */, + D0185E8C208A025A005E1A6C /* ProxySettingsServerItem.swift in Sources */, D04281ED200E3B28009DDE36 /* ItemListControllerSearch.swift in Sources */, D0EC6D381EB9F58800EBF1C3 /* SearchDisplayControllerContentNode.swift in Sources */, D06F1EA41F6C0A5D00FE8B74 /* ChatHistorySearchContainerNode.swift in Sources */, @@ -4597,6 +4633,7 @@ D0EC6D4D1EB9F58800EBF1C3 /* ChatListHoleItem.swift in Sources */, D0EC6D4E1EB9F58800EBF1C3 /* ChatListItem.swift in Sources */, D0B2F76A2052920D00D3BFB9 /* UserInfoEditingPhoneItem.swift in Sources */, + D0AEAE272080D6970013176E /* StickerPaneSearchBarNode.swift in Sources */, D0EC6D4F1EB9F58800EBF1C3 /* ChatListSearchItem.swift in Sources */, D0EC6D501EB9F58800EBF1C3 /* ChatListNodeEntries.swift in Sources */, D0EC6D511EB9F58800EBF1C3 /* ChatListViewTransition.swift in Sources */, @@ -4638,6 +4675,7 @@ D0CE8CEC1F6FCCA300AA2DB0 /* TransformImageArguments.swift in Sources */, D0EC6D661EB9F58800EBF1C3 /* ContactsSectionHeaderAccessoryItem.swift in Sources */, D0EC6D671EB9F58800EBF1C3 /* ContactListNameIndexHeader.swift in Sources */, + D07E413D208A494D00FCA8F0 /* ProxyServerActionSheetController.swift in Sources */, D0EC6D681EB9F58800EBF1C3 /* AuthorizationSequenceController.swift in Sources */, D0EC6D691EB9F58800EBF1C3 /* AuthorizationSequenceSplashController.swift in Sources */, D0EC6D6A1EB9F58800EBF1C3 /* AuthorizationSequenceSplashControllerNode.swift in Sources */, @@ -4702,6 +4740,7 @@ D01776BC1F1E21AF0044446D /* RadialStatusBackgroundNode.swift in Sources */, D0FE4DE61F0BA58A00E8A0B3 /* OverlayMediaItemNode.swift in Sources */, D0E8B8A72044339500605593 /* PresentationCallToneData.swift in Sources */, + D0AEAE252080D6830013176E /* StickerPaneSearchContainerNode.swift in Sources */, D0EC6D901EB9F58900EBF1C3 /* ChatMessageBubbleContentNode.swift in Sources */, D0EC6D911EB9F58900EBF1C3 /* ChatMessageBubbleItemNode.swift in Sources */, D0E8B8BD204479A500605593 /* SecretChatKeyController.swift in Sources */, @@ -4881,6 +4920,7 @@ D0EC6DF81EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceState.swift in Sources */, D033C60B1F0D306E0044EABA /* TelegramVideoNode.swift in Sources */, D0EC6DF91EB9F58900EBF1C3 /* PeerMediaCollectionInterfaceStateButtons.swift in Sources */, + D07E413B208A432100FCA8F0 /* ChatListTitleProxyNode.swift in Sources */, D080B27F1F4C7C6000AA3847 /* InstantPageManagedMediaId.swift in Sources */, D087BFAD1F741B9D003FD209 /* ShareContentContainerNode.swift in Sources */, D0EC6DFB1EB9F58900EBF1C3 /* AvatarGalleryController.swift in Sources */, @@ -4921,7 +4961,7 @@ D0E9BA211F05577700F079A4 /* STPCardParams.m in Sources */, D0EC6E111EB9F58900EBF1C3 /* InstantPageNode.swift in Sources */, D0EC6E121EB9F58900EBF1C3 /* InstantPageLayout.swift in Sources */, - D0D4345C1F97CEAA00CC1806 /* ProxySettingsController.swift in Sources */, + D0D4345C1F97CEAA00CC1806 /* ProxyServerSettingsController.swift in Sources */, D08BDF641FA37BEA009D08E1 /* ChatRecordingPreviewInputPanelNode.swift in Sources */, D0943B071FDEC529001522CC /* InstantVideoRadialStatusNode.swift in Sources */, D0EC6E131EB9F58900EBF1C3 /* InstantPageItem.swift in Sources */, @@ -4964,6 +5004,7 @@ D099D74D1EEFEE1500A3128C /* GameController.swift in Sources */, D0EC6E2C1EB9F58900EBF1C3 /* ComposeControllerNode.swift in Sources */, D0EC6E2D1EB9F58900EBF1C3 /* CounterContollerTitleView.swift in Sources */, + D0AEAE292080FD660013176E /* StickerPaneSearchGlobaltem.swift in Sources */, D0EC6E2E1EB9F58900EBF1C3 /* ContactMultiselectionController.swift in Sources */, D0EC6E2F1EB9F58900EBF1C3 /* ContactMultiselectionControllerNode.swift in Sources */, D0EC6E301EB9F58900EBF1C3 /* ContactSelectionController.swift in Sources */, @@ -5015,6 +5056,7 @@ D0EC6E4D1EB9F58900EBF1C3 /* PeerInfoController.swift in Sources */, D0EC6E4E1EB9F58900EBF1C3 /* GroupInfoController.swift in Sources */, D0380DAD204ED434000414AB /* LegacyLiveUploadInterface.swift in Sources */, + D0185E882089ED5F005E1A6C /* ProxyListSettingsController.swift in Sources */, D0E9BA331F05583A00F079A4 /* STPPostalCodeValidator.m in Sources */, D0EC6E4F1EB9F58900EBF1C3 /* ChannelVisibilityController.swift in Sources */, D09250061FE5371D003F693F /* GlobalExperimentalSettings.swift in Sources */, @@ -5025,6 +5067,7 @@ D0EC6E511EB9F58900EBF1C3 /* ChannelBlacklistController.swift in Sources */, D0EC6E521EB9F58900EBF1C3 /* ChannelInfoController.swift in Sources */, D0EC6E531EB9F58900EBF1C3 /* ChannelMembersController.swift in Sources */, + D02B676320800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift in Sources */, D093D8242069A06600BC3599 /* FormControllerScrollerNode.swift in Sources */, D093D7E72063E57F00BC3599 /* BotPaymentActionItemNode.swift in Sources */, D01C06BA1FBBB076001561AB /* ItemListSelectableControlNode.swift in Sources */, @@ -5070,6 +5113,7 @@ D0EC6E711EB9F58900EBF1C3 /* ThemeGalleryController.swift in Sources */, D0C0B5B11EE1C421000F4D2C /* ChatDateSelectionSheet.swift in Sources */, D0EC6E721EB9F58900EBF1C3 /* ThemeGalleryItem.swift in Sources */, + D00781052084DFB100369A39 /* UrlEscaping.swift in Sources */, D0471B581EFE6D020074D609 /* BotCheckoutInfoController.swift in Sources */, D0EC6E731EB9F58900EBF1C3 /* ThemeGalleryToolbarNode.swift in Sources */, D025A4261F79428E00563950 /* FetchManagerLocation.swift in Sources */, diff --git a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift index 65669b87f9..560d405603 100644 --- a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift +++ b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift @@ -120,6 +120,7 @@ private final class InnerCoutrySearchResultsController: UIViewController, UITabl let label = UILabel() label.font = Font.medium(17.0) cell.accessoryView = label + cell.selectedBackgroundView = UIView() } cell.textLabel?.text = self.searchResults[indexPath.row].0.1 cell.detailTextLabel?.text = self.searchResults[indexPath.row].0.0 @@ -129,6 +130,7 @@ private final class InnerCoutrySearchResultsController: UIViewController, UITabl label.textColor = self.theme.primaryColor } cell.textLabel?.textColor = self.theme.primaryColor + cell.detailTextLabel?.textColor = self.theme.primaryColor cell.backgroundColor = self.theme.backgroundColor cell.selectedBackgroundView?.backgroundColor = self.theme.itemHighlightedBackgroundColor return cell @@ -293,6 +295,7 @@ private final class InnerCountrySelectionController: UIViewController, UITableVi let label = UILabel() label.font = Font.medium(17.0) cell.accessoryView = label + cell.selectedBackgroundView = UIView() } cell.textLabel?.text = self.sections[indexPath.section].1[indexPath.row].0.1 cell.detailTextLabel?.text = self.sections[indexPath.section].1[indexPath.row].0.0 diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index 79d80b0d44..d8ef0c9684 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -661,14 +661,18 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } - let notificationAction: (Int32) -> Void = { muteUntil in + let notificationAction: (Int32?) -> Void = { muteUntil in let muteInterval: Int32? - if muteUntil <= 0 { - muteInterval = nil - } else if muteUntil == Int32.max { - muteInterval = Int32.max + if let muteUntil = muteUntil { + if muteUntil <= 0 { + muteInterval = 0 + } else if muteUntil == Int32.max { + muteInterval = Int32.max + } else { + muteInterval = muteUntil + } } else { - muteInterval = muteUntil + muteInterval = nil } changeMuteSettingsDisposable.set(updatePeerMuteSetting(account: account, peerId: peerId, muteInterval: muteInterval).start()) @@ -678,13 +682,20 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr dismissAction() notificationAction(0) })) - let intervals: [Int32] = [ + let intervals: [Int32?] = [ + nil, 1 * 60 * 60, 8 * 60 * 60, 2 * 24 * 60 * 60 ] for value in intervals { - items.append(ActionSheetButtonItem(title: muteForIntervalString(strings: presentationData.strings, value: value), action: { + let title: String + if let value = value { + title = muteForIntervalString(strings: presentationData.strings, value: value) + } else { + title = "Default" + } + items.append(ActionSheetButtonItem(title: title, action: { dismissAction() notificationAction(value) })) diff --git a/TelegramUI/ChatButtonKeyboardInputNode.swift b/TelegramUI/ChatButtonKeyboardInputNode.swift index ad06c67e83..efac3122ed 100644 --- a/TelegramUI/ChatButtonKeyboardInputNode.swift +++ b/TelegramUI/ChatButtonKeyboardInputNode.swift @@ -64,7 +64,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, maximumHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel))) if self.theme !== interfaceState.theme { diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index ab67d0e5be..6bfcc8902d 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -349,12 +349,11 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) - - }.updatedInputMode { current in - if case let .media(mode, expanded) = current, expanded { - return .media(mode: mode, expanded: false) - } - return current + }.updatedInputMode { current in + if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil) + } + return current } }) } @@ -367,8 +366,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }.updatedInputMode { current in - if case let .media(mode, expanded) = current, expanded { - return .media(mode: mode, expanded: false) + if case let .media(mode, maybeExpanded) = current, let expanded = maybeExpanded, case .content = expanded { + return .media(mode: mode, expanded: nil) } return current } @@ -556,6 +555,10 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin } }) } + }, updateInputMode: { [weak self] f in + self?.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedInputMode(f) + }) }, openMessageShareMenu: { [weak self] id in if let strongSelf = self, let messages = strongSelf.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id) { let shareController = ShareController(account: strongSelf.account, subject: .messages(messages)) @@ -2075,8 +2078,8 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) }.updatedInputMode { current in - if case let .media(mode, expanded) = current, expanded { - return .media(mode: mode, expanded: false) + if case let .media(mode, maybeExpanded) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil) } return current } @@ -2425,7 +2428,7 @@ public final class ChatController: TelegramController, UIViewControllerPreviewin return false } - if case .media(_, true) = strongSelf.presentationInterfaceState.inputMode { + if case let .media(_, expanded) = strongSelf.presentationInterfaceState.inputMode, expanded != nil { return false } diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index c35017e2e7..d7a1052e27 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -55,6 +55,7 @@ public final class ChatControllerInteraction { let openInstantPage: (Message) -> Void let openHashtag: (String?, String) -> Void let updateInputState: ((ChatTextInputState) -> ChatTextInputState) -> Void + let updateInputMode: ((ChatInputMode) -> ChatInputMode) -> Void let openMessageShareMenu: (MessageId) -> Void let presentController: (ViewController, Any?) -> Void let presentGlobalOverlayController: (ViewController, Any?) -> Void @@ -73,7 +74,7 @@ public final class ChatControllerInteraction { var contextHighlightedState: ChatInterfaceHighlightedState? var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings - public init(openMessage: @escaping (Message) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { + init(openMessage: @escaping (Message) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, requestMessageUpdate: @escaping (MessageId) -> Void, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention @@ -92,6 +93,7 @@ public final class ChatControllerInteraction { self.openInstantPage = openInstantPage self.openHashtag = openHashtag self.updateInputState = updateInputState + self.updateInputMode = updateInputMode self.openMessageShareMenu = openMessageShareMenu self.presentController = presentController self.presentGlobalOverlayController = presentGlobalOverlayController diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 788e9529a0..cc5fd735ac 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -481,7 +481,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputPanelNodeBaseHeight = inputPanelNode.minimalHeight(interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) } - var maximumInputNodeHeight = layout.size.height - max(navigationBarHeight, layout.safeInsets.top) - inputPanelNodeBaseHeight + let maximumInputNodeHeight = layout.size.height - max(navigationBarHeight, layout.safeInsets.top) - inputPanelNodeBaseHeight var dismissedInputNode: ChatInputNode? var immediatelyLayoutInputNodeAndAnimateAppearance = false @@ -505,7 +505,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.insertSubnode(inputNode, aboveSubnode: self.inputPanelBackgroundNode) } } - inputNodeHeightAndOverflow = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, maximumHeight: maximumInputNodeHeight, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) + inputNodeHeightAndOverflow = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelNodeBaseHeight, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) } else if let inputNode = self.inputNode { dismissedInputNode = inputNode self.inputNode = nil @@ -570,7 +570,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } if let inputMediaNode = self.inputMediaNode, inputMediaNode != self.inputNode { - let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, maximumHeight: maximumInputNodeHeight, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelSize?.height ?? 0.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 56.0))) @@ -822,17 +822,15 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } var expandTopDimNode = false - if case let .media(_, expanded) = self.chatPresentationInterfaceState.inputMode { - if expanded { - displayTopDimNode = true - expandTopDimNode = true - } + if case let .media(_, expanded) = self.chatPresentationInterfaceState.inputMode, expanded != nil { + displayTopDimNode = true + expandTopDimNode = true } if displayTopDimNode { var topInset = listInsets.bottom + UIScreenPixel - if let titleAccessoryPanelHeight = titleAccessoryPanelHeight { - topInset -= titleAccessoryPanelHeight + if let _ = titleAccessoryPanelHeight { + topInset -= UIScreenPixel } let topFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: max(0.0, topInset))) @@ -1306,7 +1304,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { inputNode.interfaceInteraction = interfaceInteraction self.inputMediaNode = inputNode if let (validLayout, _) = self.validLayout { - let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, standardInputHeight: validLayout.standardInputHeight, maximumHeight: validLayout.standardInputHeight, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) + let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, standardInputHeight: validLayout.standardInputHeight, maximumHeight: validLayout.standardInputHeight, inputPanelHeight: 44.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } } } @@ -1570,8 +1568,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { @objc func topDimNodeTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { state in - if case let .media(mode, true) = state.inputMode { - return (.media(mode: mode, expanded: false), nil) + if case let .media(mode, expanded) = state.inputMode, expanded != nil { + return (.media(mode: mode, expanded: nil), nil) } else { return (state.inputMode, nil) } @@ -1580,10 +1578,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } func scrollToTop() { - if case .media(_, true) = self.chatPresentationInterfaceState.inputMode { + if case let .media(_, maybeExpanded) = self.chatPresentationInterfaceState.inputMode, maybeExpanded != nil { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { state in - if case let .media(mode, true) = state.inputMode { - return (.media(mode: mode, expanded: false), nil) + if case let .media(mode, expanded) = state.inputMode, expanded != nil { + return (.media(mode: mode, expanded: expanded), nil) } else { return (state.inputMode, nil) } diff --git a/TelegramUI/ChatInputNode.swift b/TelegramUI/ChatInputNode.swift index b532c15a0e..2e42c8d8ae 100644 --- a/TelegramUI/ChatInputNode.swift +++ b/TelegramUI/ChatInputNode.swift @@ -5,7 +5,7 @@ import AsyncDisplayKit class ChatInputNode: ASDisplayNode { var interfaceInteraction: ChatPanelInterfaceInteraction? - func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, maximumHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { return (0.0, 0.0) } } diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index 283accb0fd..32ba2eebff 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -46,7 +46,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD if groupId == nil { self.navigationBar?.item = nil - self.titleView.title = NetworkStatusTitle(text: self.presentationData.strings.DialogList_Title, activity: false) + self.titleView.title = NetworkStatusTitle(text: self.presentationData.strings.DialogList_Title, activity: false, hasProxy: false, connectsViaProxy: false) self.navigationItem.titleView = self.titleView self.tabBarItem.title = self.presentationData.strings.DialogList_Title self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconChats") @@ -67,17 +67,30 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } } - self.titleDisposable = (account.networkState |> deliverOnMainQueue).start(next: { [weak self] state in + let hasProxy = account.postbox.preferencesView(keys: [PreferencesKeys.proxySettings]) + |> map { preferences -> (Bool, Bool) in + if let settings = preferences.values[PreferencesKeys.proxySettings] as? ProxySettings { + return (!settings.servers.isEmpty, settings.enabled) + } else { + return (false, false) + } + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs == rhs + }) + + self.titleDisposable = (combineLatest(account.networkState |> deliverOnMainQueue, hasProxy |> deliverOnMainQueue)).start(next: { [weak self] state, proxy in if let strongSelf = self { + let (hasProxy, connectsViaProxy) = proxy switch state { case .waitingForNetwork: - strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true) - case let .connecting(toProxy): - strongSelf.titleView.title = NetworkStatusTitle(text: toProxy ? strongSelf.presentationData.strings.State_ConnectingToProxy : strongSelf.presentationData.strings.State_Connecting, activity: true) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: hasProxy, connectsViaProxy: connectsViaProxy) + case .connecting: + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Connecting, activity: true, hasProxy: hasProxy, connectsViaProxy: connectsViaProxy) case .updating: - strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true, hasProxy: hasProxy, connectsViaProxy: connectsViaProxy) case .online: - strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.DialogList_Title, activity: false) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.DialogList_Title, activity: false, hasProxy: hasProxy, connectsViaProxy: connectsViaProxy) } } }) @@ -119,6 +132,12 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } } + self.titleView.openProxySettings = { [weak self] in + if let strongSelf = self { + (strongSelf.navigationController as? NavigationController)?.pushViewController(proxySettingsController(account: account)) + } + } + self.presentationDataDisposable = (account.telegramApplicationContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { diff --git a/TelegramUI/ChatListTitleProxyNode.swift b/TelegramUI/ChatListTitleProxyNode.swift new file mode 100644 index 0000000000..409f9831eb --- /dev/null +++ b/TelegramUI/ChatListTitleProxyNode.swift @@ -0,0 +1,110 @@ +import Foundation +import Display +import AsyncDisplayKit + +enum ChatTitleProxyStatus { + case connecting + case connected + case available +} + +/* + + + + Created with Sketch. + + + + + + + + + + */ + +private func generateIcon(color: UIColor, connected: Bool, off: Bool) -> UIImage? { + return generateImage(CGSize(width: 18.0, height: 22.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setStrokeColor(color.cgColor) + context.setFillColor(color.cgColor) + context.scaleBy(x: 0.3333, y: 0.3333) + context.setLineWidth(3.0) + + let _ = try? drawSvgPath(context, path: "M27,1.6414763 L1.5,12.9748096 L1.5,30 C1.5,45.9171686 12.4507463,60.7063193 27,64.4535514 C41.5492537,60.7063193 52.5,45.9171686 52.5,30 L52.5,12.9748096 L27,1.6414763 S") + + if connected { + let _ = try? drawSvgPath(context, path: "M15.5769231,34.1735387 L23.5896918,42.2164446 C23.6840928,42.3112006 23.8352513,42.30478 23.9262955,42.2032393 L40.5,23.71875 S") + } else if off { + let _ = try? drawSvgPath(context, path: "M27.5,15 C28.3284271,15 29,15.6715729 29,16.5 L29,28.5 C29,29.3284271 28.3284271,30 27.5,30 C26.6715729,30 26,29.3284271 26,28.5 L26,16.5 C26,15.6715729 26.6715729,15 27.5,15 Z") + context.translateBy(x: 27.0, y: 33.0) + context.rotate(by: 2.35619) + context.translateBy(x: -27.0, y: -33.0) + let _ = try? drawSvgPath(context, path: "M27,47 C34.7319865,47 41,40.7319865 41,33 C41,25.2680135 34.7319865,19 27,19 C19.2680135,19 13,25.2680135 13,33 S") + } + }) +} + +final class ChatTitleProxyNode: ASDisplayNode { + private let iconNode: ASImageNode + private let activityIndicator: ActivityIndicator + + var theme: PresentationTheme { + didSet { + if self.theme !== oldValue { + switch self.status { + case .connecting: + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: false) + case .connected: + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: true, off: false) + case .available: + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: true) + } + self.activityIndicator.type = .custom(theme.rootController.navigationBar.accentTextColor, 10.0, 1.0) + } + } + } + + var status: ChatTitleProxyStatus = .connected { + didSet { + if self.status != oldValue { + switch status { + case .connecting: + self.activityIndicator.isHidden = false + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: false) + case .connected: + self.activityIndicator.isHidden = true + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: true, off: false) + case .available: + self.activityIndicator.isHidden = true + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: true) + } + } + } + } + + init(theme: PresentationTheme) { + self.theme = theme + + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displayWithoutProcessing = true + self.iconNode.displaysAsynchronously = false + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: true) + + self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.accentTextColor, 10.0, 1.0), speed: .slow) + + super.init() + + self.addSubnode(self.iconNode) + self.addSubnode(self.activityIndicator) + + let iconFrame = CGRect(origin: CGPoint(), size: CGSize(width: 18.0, height: 22.0)) + self.iconNode.frame = iconFrame + self.activityIndicator.frame = CGRect(origin: CGPoint(x: floor(iconFrame.midX - 5.0), y: 6.0), size: CGSize(width: 10.0, height: 10.0)) + + self.frame = CGRect(origin: CGPoint(), size: CGSize(width: 18.0, height: 22.0)) + } +} diff --git a/TelegramUI/ChatMediaInputGridEntries.swift b/TelegramUI/ChatMediaInputGridEntries.swift index 5d1b7dbd1c..85e4de10c1 100644 --- a/TelegramUI/ChatMediaInputGridEntries.swift +++ b/TelegramUI/ChatMediaInputGridEntries.swift @@ -3,31 +3,93 @@ import TelegramCore import SwiftSignalKit import Display -struct ChatMediaInputGridEntryStableId: Hashable { - let collectionId: ItemCollectionId - let itemId: ItemCollectionItemIndex.Id +enum ChatMediaInputGridEntryStableId: Equatable, Hashable { + case search + case sticker(ItemCollectionId, ItemCollectionItemIndex.Id) +} + +enum ChatMediaInputGridEntryIndex: Equatable, Comparable { + case search + case collectionIndex(ItemCollectionViewEntryIndex) - static func ==(lhs: ChatMediaInputGridEntryStableId, rhs: ChatMediaInputGridEntryStableId) -> Bool { - return lhs.collectionId == rhs.collectionId && lhs.itemId == rhs.itemId + var stableId: ChatMediaInputGridEntryStableId { + switch self { + case .search: + return .search + case let .collectionIndex(index): + return .sticker(index.collectionId, index.itemIndex.id) + } } - var hashValue: Int { - return self.itemId.hashValue + static func <(lhs: ChatMediaInputGridEntryIndex, rhs: ChatMediaInputGridEntryIndex) -> Bool { + switch lhs { + case .search: + if case .search = rhs { + return false + } else { + return true + } + case let .collectionIndex(lhsIndex): + switch rhs { + case .search: + return false + case let .collectionIndex(rhsIndex): + return lhsIndex < rhsIndex + } + } } } -struct ChatMediaInputGridEntry: Comparable, Identifiable { - let index: ItemCollectionViewEntryIndex - let stickerItem: StickerPackItem - let stickerPackInfo: StickerPackCollectionInfo? - let theme: PresentationTheme +enum ChatMediaInputGridEntry: Equatable, Comparable, Identifiable { + case search(theme: PresentationTheme, strings: PresentationStrings) + case sticker(index: ItemCollectionViewEntryIndex, stickerItem: StickerPackItem, stickerPackInfo: StickerPackCollectionInfo?, theme: PresentationTheme) + + var index: ChatMediaInputGridEntryIndex { + switch self { + case .search: + return .search + case let .sticker(index, _, _, _): + return .collectionIndex(index) + } + } var stableId: ChatMediaInputGridEntryStableId { - return ChatMediaInputGridEntryStableId(collectionId: self.index.collectionId, itemId: self.stickerItem.index.id) + return self.index.stableId } static func ==(lhs: ChatMediaInputGridEntry, rhs: ChatMediaInputGridEntry) -> Bool { - return lhs.index == rhs.index && lhs.stickerItem == rhs.stickerItem && lhs.stickerPackInfo?.id == rhs.stickerPackInfo?.id && lhs.theme === rhs.theme + switch lhs { + case let .search(lhsTheme, lhsStrings): + if case let .search(rhsTheme, rhsStrings) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + return true + } else { + return false + } + case let .sticker(lhsIndex, lhsStickerItem, lhsStickerPackInfo, lhsTheme): + if case let .sticker(rhsIndex, rhsStickerItem, rhsStickerPackInfo, rhsTheme) = rhs { + if lhsIndex != rhsIndex { + return false + } + if lhsStickerItem != rhsStickerItem { + return false + } + if lhsStickerPackInfo != rhsStickerPackInfo { + return false + } + if lhsTheme !== rhsTheme { + return false + } + return true + } else { + return false + } + } } static func <(lhs: ChatMediaInputGridEntry, rhs: ChatMediaInputGridEntry) -> Bool { @@ -35,6 +97,13 @@ struct ChatMediaInputGridEntry: Comparable, Identifiable { } func item(account: Account, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> GridItem { - return ChatMediaInputStickerGridItem(account: account, collectionId: self.index.collectionId, stickerPackInfo: self.stickerPackInfo, index: self.index, stickerItem: self.stickerItem, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction, theme: self.theme, selected: { }) + switch self { + case let .search(theme, strings): + return StickerPaneSearchBarPlaceholderItem(theme: theme, strings: strings, activate: { + inputNodeInteraction.toggleSearch(true) + }) + case let .sticker(index, stickerItem, stickerPackInfo, theme): + return ChatMediaInputStickerGridItem(account: account, collectionId: index.collectionId, stickerPackInfo: stickerPackInfo, index: index, stickerItem: stickerItem, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { }) + } } } diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index f712d43f33..b2a9d77e4e 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -36,6 +36,8 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr var scrollToItem: GridNodeScrollToItem? var animated = false switch update { + case .initial: + break case .generic: animated = true case .scroll: @@ -53,7 +55,7 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr } stationaryItems = .indices(indices) case let .navigate(index, collectionId): - if let index = index { + if let index = index.flatMap({ ChatMediaInputGridEntryIndex.collectionIndex($0) }) { for i in 0 ..< toEntries.count { if toEntries[i].index >= index { var directionHint: GridNodePreviousItemsTransitionDirectionHint = .up @@ -67,7 +69,7 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr } else if !toEntries.isEmpty { if let collectionId = collectionId { for i in 0 ..< toEntries.count { - if toEntries[i].index.collectionId == collectionId { + if case let .collectionIndex(collectionIndex) = toEntries[i].index, collectionIndex.collectionId == collectionId { var directionHint: GridNodePreviousItemsTransitionDirectionHint = .up if !fromEntries.isEmpty && fromEntries[0].index < toEntries[i].index { directionHint = .down @@ -91,7 +93,24 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr var firstIndexInSectionOffset = 0 if !toEntries.isEmpty { - firstIndexInSectionOffset = Int(toEntries[0].index.itemIndex.index) + switch toEntries[0].index { + case .search: + break + case let .collectionIndex(index): + firstIndexInSectionOffset = Int(index.itemIndex.index) + } + } + + if case .initial = update { + switch toEntries[0].index { + case .search: + if toEntries.count > 1 { + //scrollToItem = GridNodeScrollToItem(index: 1, position: .top, transition: .immediate, directionHint: .up, adjustForSection: true) + } + break + default: + break + } } return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, animated: animated) @@ -121,6 +140,10 @@ private func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, strings: PresentationStrings, theme: PresentationTheme) -> [ChatMediaInputGridEntry] { var entries: [ChatMediaInputGridEntry] = [] + if view.lower == nil { + entries.append(.search(theme: theme, strings: strings)) + } + var stickerPackInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:] for (id, info, _) in view.collectionInfos { if let info = info as? StickerPackCollectionInfo { @@ -137,7 +160,7 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: savedStickerIds.insert(item.file.fileId.id) let index = ItemCollectionItemIndex(index: Int32(i), id: item.file.fileId.id) let stickerItem = StickerPackItem(index: index, file: item.file, indexKeys: []) - entries.append(ChatMediaInputGridEntry(index: ItemCollectionViewEntryIndex(collectionIndex: -2, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) + entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -2, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) } } } @@ -153,7 +176,7 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: if !savedStickerIds.contains(mediaId.id) { let index = ItemCollectionItemIndex(index: Int32(i), id: mediaId.id) let stickerItem = StickerPackItem(index: index, file: file, indexKeys: []) - entries.append(ChatMediaInputGridEntry(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) + entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) addedCount += 1 } } @@ -163,7 +186,7 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: for entry in view.entries { if let item = entry.item as? StickerPackItem { - entries.append(ChatMediaInputGridEntry(index: entry.index, stickerItem: item, stickerPackInfo: stickerPackInfos[entry.index.collectionId], theme: theme)) + entries.append(.sticker(index: entry.index, stickerItem: item, stickerPackInfo: stickerPackInfos[entry.index.collectionId], theme: theme)) } } return entries @@ -195,6 +218,7 @@ private enum StickerPacksCollectionPosition: Equatable { } private enum StickerPacksCollectionUpdate { + case initial case generic case scroll case navigate(ItemCollectionViewEntryIndex?, ItemCollectionId?) @@ -203,15 +227,17 @@ private enum StickerPacksCollectionUpdate { final class ChatMediaInputNodeInteraction { let navigateToCollectionId: (ItemCollectionId) -> Void let openSettings: () -> Void + let toggleSearch: (Bool) -> Void var highlightedStickerItemCollectionId: ItemCollectionId? var highlightedItemCollectionId: ItemCollectionId? - var previewedStickerPackItem: StickerPackItem? + var previewedStickerPackItem: StickerPreviewPeekItem? var appearanceTransition: CGFloat = 1.0 - init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void) { + init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool) -> Void) { self.navigateToCollectionId = navigateToCollectionId self.openSettings = openSettings + self.toggleSearch = toggleSearch } } @@ -247,6 +273,17 @@ private struct ChatMediaInputPaneArrangement { } } +private final class CollectionListContainerNode: ASDisplayNode { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for subview in self.view.subviews { + if let result = subview.hitTest(point.offsetBy(dx: -subview.frame.minX, dy: -subview.frame.minY), with: event) { + return result + } + } + return nil + } +} + final class ChatMediaInputNode: ChatInputNode { private let account: Account private let controllerInteraction: ChatControllerInteraction @@ -257,10 +294,12 @@ final class ChatMediaInputNode: ChatInputNode { private let collectionListPanel: ASDisplayNode private var collectionListPanelOffset: CGFloat = 0.0 private let collectionListSeparator: ASDisplayNode + private let collectionListContainer: CollectionListContainerNode private let disposable = MetaDisposable() private let listView: ListView + private var stickerSearchContainerNode: StickerPaneSearchContainerNode? private let stickerPane: ChatMediaInputStickerPane private var animatingStickerPaneOut = false @@ -273,7 +312,7 @@ final class ChatMediaInputNode: ChatInputNode { private var currentStickerPacksCollectionPosition: StickerPacksCollectionPosition? private var currentView: ItemCollectionsView? - private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState)? + private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState)? private var paneArrangement: ChatMediaInputPaneArrangement private var theme: PresentationTheme @@ -297,13 +336,16 @@ final class ChatMediaInputNode: ChatInputNode { self.collectionListSeparator.isLayerBacked = true self.collectionListSeparator.backgroundColor = theme.chat.inputMediaPanel.panelSerapatorColor + self.collectionListContainer = CollectionListContainerNode() + self.collectionListContainer.clipsToBounds = true + self.listView = ListView() self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) var paneDidScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void)? var fixPaneScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void)? - self.stickerPane = ChatMediaInputStickerPane(paneDidScroll: { pane, state, transition in + self.stickerPane = ChatMediaInputStickerPane(theme: theme, strings: strings, paneDidScroll: { pane, state, transition in paneDidScrollImpl?(pane, state, transition) }, fixPaneScroll: { pane, state in fixPaneScrollImpl?(pane, state) @@ -349,23 +391,46 @@ final class ChatMediaInputNode: ChatInputNode { if let strongSelf = self { strongSelf.controllerInteraction.presentController(installedStickerPacksController(account: account, mode: .modal), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } + }, toggleSearch: { [weak self] value in + if let strongSelf = self { + strongSelf.controllerInteraction.updateInputMode { current in + switch current { + case let .media(mode, _): + if value { + return .media(mode: mode, expanded: .search) + } else { + return .media(mode: mode, expanded: nil) + } + default: + return current + } + } + } }) - self.clipsToBounds = true self.backgroundColor = theme.chat.inputMediaPanel.gifsBackgroundColor self.collectionListPanel.addSubnode(self.listView) - self.addSubnode(self.collectionListPanel) - self.addSubnode(self.collectionListSeparator) + self.collectionListContainer.addSubnode(self.collectionListPanel) + self.collectionListContainer.addSubnode(self.collectionListSeparator) + self.addSubnode(self.collectionListContainer) let itemCollectionsView = self.itemCollectionsViewPosition.get() |> distinctUntilChanged |> mapToSignal { position -> Signal<(ItemCollectionsView, StickerPacksCollectionUpdate), NoError> in switch position { case .initial: + var firstTime = true return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50) |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in - return (view, .generic) + let update: StickerPacksCollectionUpdate + if firstTime { + firstTime = false + update = .initial + } else { + update = .generic + } + return (view, update) } case let .scroll(aroundIndex): var firstTime = true @@ -495,7 +560,64 @@ final class ChatMediaInputNode: ChatInputNode { self.view.disablesInteractiveTransitionGestureRecognizer = true self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in if let strongSelf = self { - let panes: [ASDisplayNode] = [strongSelf.gifPane, strongSelf.stickerPane, strongSelf.stickerPane] + let panes: [ASDisplayNode] + if let stickerSearchContainerNode = strongSelf.stickerSearchContainerNode { + panes = [] + if let (itemNode, item) = stickerSearchContainerNode.itemAt(point: point.offsetBy(dx: -stickerSearchContainerNode.frame.minX, dy: -stickerSearchContainerNode.frame.minY)) { + return strongSelf.account.postbox.modify { modifier -> Bool in + return getIsStickerSaved(modifier: modifier, fileId: item.file.fileId) + } + |> deliverOnMainQueue + |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in + if let strongSelf = self { + var menuItems: [PeekControllerMenuItem] = [] + menuItems = [ + PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, action: { + if let strongSelf = self { + strongSelf.controllerInteraction.sendSticker(item.file) + } + }), + PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { + if let strongSelf = self { + if isStarred { + let _ = removeSavedSticker(postbox: strongSelf.account.postbox, mediaId: item.file.fileId).start() + } else { + let _ = addSavedSticker(postbox: strongSelf.account.postbox, network: strongSelf.account.network, file: item.file).start() + } + } + }), + PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: { + if let strongSelf = self { + loop: for attribute in item.file.attributes { + switch attribute { + case let .Sticker(_, packReference, _): + if let packReference = packReference { + let controller = StickerPackPreviewController(account: strongSelf.account, stickerPack: packReference) + controller.sendSticker = { file in + if let strongSelf = self { + strongSelf.controllerInteraction.sendSticker(file) + } + } + strongSelf.controllerInteraction.presentController(controller, nil) + } + break loop + default: + break + } + } + } + }), + PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: {}) + ] + return (itemNode, StickerPreviewPeekContent(account: strongSelf.account, item: .found(item), menu: menuItems)) + } else { + return nil + } + } + } + } else { + panes = [strongSelf.gifPane, strongSelf.stickerPane, strongSelf.stickerPane] + } for pane in panes { if pane.supernode != nil, pane.frame.contains(point) { if let pane = pane as? ChatMediaInputGifPane { @@ -560,7 +682,7 @@ final class ChatMediaInputNode: ChatInputNode { }), PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: {}) ] - return (itemNode, StickerPreviewPeekContent(account: strongSelf.account, item: item, menu: menuItems)) + return (itemNode, StickerPreviewPeekContent(account: strongSelf.account, item: .pack(item), menu: menuItems)) } else { return nil } @@ -582,7 +704,7 @@ final class ChatMediaInputNode: ChatInputNode { return nil }, updateContent: { [weak self] content in if let strongSelf = self { - var item: StickerPackItem? + var item: StickerPreviewPeekItem? if let content = content as? StickerPreviewPeekContent { item = content.item } @@ -596,8 +718,8 @@ final class ChatMediaInputNode: ChatInputNode { if let index = self.paneArrangement.panes.index(of: pane), index != self.paneArrangement.currentIndex { let previousGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs self.paneArrangement = self.paneArrangement.withIndexTransition(0.0).withCurrentIndex(index) - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, interfaceState) = self.validLayout { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, inputPanelHeight, interfaceState) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) } let updatedGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs if updatedGifPanelWasActive != previousGifPanelWasActive { @@ -614,8 +736,8 @@ final class ChatMediaInputNode: ChatInputNode { self.setHighlightedItemCollectionId(ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue, id: 0)) } } else { - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, interfaceState) = self.validLayout { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, inputPanelHeight, interfaceState) = self.validLayout { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) } } } @@ -696,21 +818,30 @@ final class ChatMediaInputNode: ChatInputNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, maximumHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { - self.validLayout = (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, interfaceState) + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> (CGFloat, CGFloat) { + self.validLayout = (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, inputPanelHeight, interfaceState) if self.theme !== interfaceState.theme || self.strings !== interfaceState.strings { self.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings) } + var displaySearch = false + let separatorHeight = UIScreenPixel let panelHeight: CGFloat - if case .media(_, true) = interfaceState.inputMode { - panelHeight = maximumHeight + if case let .media(_, maybeExpanded) = interfaceState.inputMode, let expanded = maybeExpanded { + switch expanded { + case .content: + panelHeight = maximumHeight + case .search: + panelHeight = maximumHeight + displaySearch = true + } } else { panelHeight = standardInputHeight } + transition.updateFrame(node: self.collectionListContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: max(0.0, 41.0 + self.collectionListPanelOffset + UIScreenPixel)))) transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: self.collectionListPanelOffset), size: CGSize(width: width, height: 41.0))) transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + self.collectionListPanelOffset), size: CGSize(width: width, height: separatorHeight))) @@ -759,7 +890,7 @@ final class ChatMediaInputNode: ChatInputNode { switch pane { case .gifs: if self.gifPane.supernode == nil { - self.insertSubnode(self.gifPane, belowSubnode: self.collectionListPanel) + self.insertSubnode(self.gifPane, belowSubnode: self.collectionListContainer) self.gifPane.frame = CGRect(origin: CGPoint(x: -width, y: 41.0), size: CGSize(width: width, height: panelHeight - 41.0)) } if self.gifPane.frame != paneFrame { @@ -768,7 +899,7 @@ final class ChatMediaInputNode: ChatInputNode { } case .stickers: if self.stickerPane.supernode == nil { - self.insertSubnode(self.stickerPane, belowSubnode: self.collectionListPanel) + self.insertSubnode(self.stickerPane, belowSubnode: self.collectionListContainer) self.stickerPane.frame = CGRect(origin: CGPoint(x: width, y: 41.0), size: CGSize(width: width, height: panelHeight - 41.0)) } if self.stickerPane.frame != paneFrame { @@ -777,7 +908,7 @@ final class ChatMediaInputNode: ChatInputNode { } case .trending: if self.trendingPane.supernode == nil { - self.insertSubnode(self.trendingPane, belowSubnode: self.collectionListPanel) + self.insertSubnode(self.trendingPane, belowSubnode: self.collectionListContainer) self.trendingPane.frame = CGRect(origin: CGPoint(x: width, y: 41.0), size: CGSize(width: width, height: panelHeight - 41.0)) } if self.trendingPane.frame != paneFrame { @@ -866,6 +997,48 @@ final class ChatMediaInputNode: ChatInputNode { self.animatingTrendingPaneOut = false } + if displaySearch { + let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: -inputPanelHeight), size: CGSize(width: width, height: panelHeight + inputPanelHeight)) + if let stickerSearchContainerNode = self.stickerSearchContainerNode { + transition.updateFrame(node: stickerSearchContainerNode, frame: containerFrame) + stickerSearchContainerNode.updateLayout(size: containerFrame.size, transition: transition) + } else { + let stickerSearchContainerNode = StickerPaneSearchContainerNode(account: self.account, theme: self.theme, strings: self.strings, controllerInteraction: self.controllerInteraction, inputNodeInteraction: self.inputNodeInteraction, cancel: { [weak self] in + self?.stickerSearchContainerNode?.deactivate() + self?.inputNodeInteraction.toggleSearch(false) + }) + self.stickerSearchContainerNode = stickerSearchContainerNode + self.addSubnode(stickerSearchContainerNode) + stickerSearchContainerNode.frame = containerFrame + stickerSearchContainerNode.updateLayout(size: containerFrame.size, transition: .immediate) + var placeholderNode: StickerPaneSearchBarPlaceholderNode? + self.stickerPane.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? StickerPaneSearchBarPlaceholderNode { + placeholderNode = itemNode + } + } + if let placeholderNode = placeholderNode { + stickerSearchContainerNode.animateIn(from: placeholderNode, transition: transition) + } + } + } else if let stickerSearchContainerNode = self.stickerSearchContainerNode { + self.stickerSearchContainerNode = nil + + var placeholderNode: StickerPaneSearchBarPlaceholderNode? + self.stickerPane.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? StickerPaneSearchBarPlaceholderNode { + placeholderNode = itemNode + } + } + if let placeholderNode = placeholderNode { + stickerSearchContainerNode.animateOut(to: placeholderNode, transition: transition, completion: { [weak stickerSearchContainerNode] in + stickerSearchContainerNode?.removeFromSupernode() + }) + } else { + stickerSearchContainerNode.removeFromSupernode() + } + } + return (standardInputHeight, max(0.0, panelHeight - standardInputHeight)) } @@ -892,7 +1065,7 @@ final class ChatMediaInputNode: ChatInputNode { self.stickerPane.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: itemTransition, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in }) } - private func updatePreviewingItem(item: StickerPackItem?, animated: Bool) { + private func updatePreviewingItem(item: StickerPreviewPeekItem?, animated: Bool) { if self.inputNodeInteraction.previewedStickerPackItem != item { self.inputNodeInteraction.previewedStickerPackItem = item @@ -901,6 +1074,8 @@ final class ChatMediaInputNode: ChatInputNode { itemNode.updatePreviewing(animated: animated) } } + + self.stickerSearchContainerNode?.updatePreviewing(animated: animated) } } @@ -909,7 +1084,7 @@ final class ChatMediaInputNode: ChatInputNode { case .began: break case .changed: - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, interfaceState) = self.validLayout { + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, inputPanelHeight, interfaceState) = self.validLayout { let translationX = -recognizer.translation(in: self.view).x var indexTransition = translationX / width if self.paneArrangement.currentIndex == 0 { @@ -918,10 +1093,10 @@ final class ChatMediaInputNode: ChatInputNode { indexTransition = min(0.0, indexTransition) } self.paneArrangement = self.paneArrangement.withIndexTransition(indexTransition) - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, transition: .immediate, interfaceState: interfaceState) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState) } case .ended: - if let (width, _, _, _, _, _, _) = self.validLayout { + if let (width, _, _, _, _, _, _, _) = self.validLayout { var updatedIndex = self.paneArrangement.currentIndex if abs(self.paneArrangement.indexTransition * width) > 30.0 { if self.paneArrangement.indexTransition < 0.0 { @@ -934,9 +1109,9 @@ final class ChatMediaInputNode: ChatInputNode { self.setCurrentPane(self.paneArrangement.panes[updatedIndex], transition: .animated(duration: 0.25, curve: .spring)) } case .cancelled: - if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, interfaceState) = self.validLayout { + if let (width, leftInset, rightInset, bottomInset, standardInputHeight, maximumHeight, inputPanelHeight, interfaceState) = self.validLayout { self.paneArrangement = self.paneArrangement.withIndexTransition(0.0) - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState) } default: break @@ -982,4 +1157,13 @@ final class ChatMediaInputNode: ChatInputNode { transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + self.collectionListPanelOffset), size: self.collectionListSeparator.bounds.size)) transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - self.collectionListPanelOffset) / 2.0)) } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let stickerSearchContainerNode = self.stickerSearchContainerNode { + if let result = stickerSearchContainerNode.hitTest(point.offsetBy(dx: -stickerSearchContainerNode.frame.minX, dy: -stickerSearchContainerNode.frame.minY), with: event) { + return result + } + } + return super.hitTest(point, with: event) + } } diff --git a/TelegramUI/ChatMediaInputStickerGridItem.swift b/TelegramUI/ChatMediaInputStickerGridItem.swift index b2b96d1ac8..f7344deae0 100644 --- a/TelegramUI/ChatMediaInputStickerGridItem.swift +++ b/TelegramUI/ChatMediaInputStickerGridItem.swift @@ -78,7 +78,11 @@ final class ChatMediaInputStickerGridItem: GridItem { self.interfaceInteraction = interfaceInteraction self.inputNodeInteraction = inputNodeInteraction self.selected = selected - self.section = ChatMediaInputStickerGridSection(collectionId: collectionId, collectionInfo: stickerPackInfo, theme: theme) + if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue { + self.section = nil + } else { + self.section = ChatMediaInputStickerGridSection(collectionId: collectionId, collectionInfo: stickerPackInfo, theme: theme) + } } func node(layout: GridNodeLayout) -> GridItemNode { @@ -177,7 +181,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { func updatePreviewing(animated: Bool) { var isPreviewing = false if let (_, item, _) = self.currentState, let interaction = self.inputNodeInteraction { - isPreviewing = interaction.previewedStickerPackItem == item + isPreviewing = interaction.previewedStickerPackItem == .pack(item) } if self.currentIsPreviewing != isPreviewing { self.currentIsPreviewing = isPreviewing diff --git a/TelegramUI/ChatMediaInputStickerPane.swift b/TelegramUI/ChatMediaInputStickerPane.swift index 3b615c6ae8..ae21d6a66e 100644 --- a/TelegramUI/ChatMediaInputStickerPane.swift +++ b/TelegramUI/ChatMediaInputStickerPane.swift @@ -12,8 +12,9 @@ final class ChatMediaInputStickerPane: ChatMediaInputPane { private var didScrollPreviousOffset: CGFloat? private var didScrollPreviousState: ChatMediaInputPaneScrollState? - init(paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void) { self.gridNode = GridNode() + self.gridNode.initialOffset = 54.0 self.paneDidScroll = paneDidScroll self.fixPaneScroll = fixPaneScroll diff --git a/TelegramUI/ChatMediaInputTrendingPane.swift b/TelegramUI/ChatMediaInputTrendingPane.swift index 82abd86720..e96f0397bc 100644 --- a/TelegramUI/ChatMediaInputTrendingPane.swift +++ b/TelegramUI/ChatMediaInputTrendingPane.swift @@ -144,6 +144,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { } }, openPack: { [weak self] info in if let strongSelf = self, let info = info as? StickerPackCollectionInfo { + strongSelf.view.window?.endEditing(true) strongSelf.controllerInteraction.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash)), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } }) diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 65675a37d3..b466f62472 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -415,10 +415,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let contentNode = (contentNodeClass as! ChatMessageBubbleContentNode.Type).init() contentPropertiesAndPrepareLayouts.append((contentNodeMessage, contentNode.supportsMosaic, contentNode.asyncLayoutContent())) if addedContentNodes == nil { - addedContentNodes = [contentNode] - } else { - addedContentNodes!.append(contentNode) + addedContentNodes = [] } + addedContentNodes!.append(contentNode) } } diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index f77fc8d74e..efa2e29840 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -193,40 +193,16 @@ enum ChatMediaInputMode { case other } +enum ChatMediaInputExpanded { + case content + case search +} + enum ChatInputMode: Equatable { case none case text - case media(mode: ChatMediaInputMode, expanded: Bool) + case media(mode: ChatMediaInputMode, expanded: ChatMediaInputExpanded?) case inputButtons - - static func ==(lhs: ChatInputMode, rhs: ChatInputMode) -> Bool { - switch lhs { - case .none: - if case .none = rhs { - return true - } else { - return false - } - case .text: - if case .text = rhs { - return true - } else { - return false - } - case let .media(mode, expanded): - if case .media(mode, expanded) = rhs { - return true - } else { - return false - } - case .inputButtons: - if case .inputButtons = rhs { - return true - } else { - return false - } - } - } } enum ChatTitlePanelContext: Comparable { diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift index 6e683e8bf9..2017663d2a 100644 --- a/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -207,7 +207,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { strongSelf.pushController(searchController) } })) - }, updateInputState: { _ in }, openMessageShareMenu: { _ in + }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action in if let strongSelf = self { switch action { diff --git a/TelegramUI/ChatTextInputActionButtonsNode.swift b/TelegramUI/ChatTextInputActionButtonsNode.swift index c3b1580f8b..b9a4cbeb0a 100644 --- a/TelegramUI/ChatTextInputActionButtonsNode.swift +++ b/TelegramUI/ChatTextInputActionButtonsNode.swift @@ -32,7 +32,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(origin: CGPoint(), size: size)) transition.updateFrame(node: self.expandMediaInputButton, frame: CGRect(origin: CGPoint(), size: size)) var expanded = false - if case .media(_, true) = interfaceState.inputMode { + if case let .media(_, maybeExpanded) = interfaceState.inputMode, maybeExpanded != nil { expanded = true } transition.updateSublayerTransformScale(node: self.expandMediaInputButton, scale: CGPoint(x: 1.0, y: expanded ? 1.0 : -1.0)) diff --git a/TelegramUI/ChatTextInputAttributes.swift b/TelegramUI/ChatTextInputAttributes.swift index 58d595b3b9..d2171026c2 100644 --- a/TelegramUI/ChatTextInputAttributes.swift +++ b/TelegramUI/ChatTextInputAttributes.swift @@ -367,11 +367,23 @@ func breakChatInputText(_ text: NSAttributedString) -> [NSAttributedString] { if text.length <= 4000 { return [text] } else { + let rawText: NSString = text.string as NSString var result: [NSAttributedString] = [] var offset = 0 while offset < text.length { - result.append(text.attributedSubstring(from: NSRange(location: offset, length: min(text.length - offset, 4000)))) - offset += 4000 + var range = NSRange(location: offset, length: min(text.length - offset, 4000)) + if range.upperBound < text.length { + inner: for i in (range.lowerBound ..< range.upperBound).reversed() { + let c = rawText.character(at: i) + let uc = UnicodeScalar(c) + if uc == "\n" as UnicodeScalar || uc == "." as UnicodeScalar { + range.length = i + 1 - range.location + break inner + } + } + } + result.append(trimChatInputText(text.attributedSubstring(from: range))) + offset = range.upperBound } return result } diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 39d46b2411..2a1ad740fe 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -1290,7 +1290,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func expandButtonPressed() { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in if case let .media(mode, expanded) = state.inputMode { - return (.media(mode: mode, expanded: !expanded), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + if let _ = expanded { + return (.media(mode: mode, expanded: nil), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + } else { + return (.media(mode: mode, expanded: .content), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + } } else { return (state.inputMode, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) } @@ -1303,7 +1307,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { switch item { case .stickers: self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in - return (.media(mode: .other, expanded: false), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + return (.media(mode: .other, expanded: nil), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) }) case .keyboard: self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in diff --git a/TelegramUI/ChatTitleView.swift b/TelegramUI/ChatTitleView.swift index e0b73b6076..7b68c85b10 100644 --- a/TelegramUI/ChatTitleView.swift +++ b/TelegramUI/ChatTitleView.swift @@ -178,7 +178,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } - var networkState: AccountNetworkState = .online { + var networkState: AccountNetworkState = .online(proxy: nil) { didSet { if self.networkState != oldValue { if case .online = self.networkState { @@ -200,8 +200,8 @@ final class ChatTitleView: UIView, NavigationBarTitleView { switch self.networkState { case .waitingForNetwork: statusNode.title = self.strings.State_WaitingForNetwork - case let .connecting(toProxy): - statusNode.title = toProxy ? self.strings.State_ConnectingToProxy : self.strings.State_Connecting + case let .connecting(proxy): + statusNode.title = proxy != nil ? self.strings.State_ConnectingToProxy : self.strings.State_Connecting case .updating: statusNode.title = self.strings.State_Updating case .online: @@ -289,7 +289,13 @@ final class ChatTitleView: UIView, NavigationBarTitleView { shouldUpdateLayout = true } } else if let user = peer as? TelegramUser { - if let _ = user.botInfo { + if user.id.namespace == Namespaces.Peer.CloudUser && user.id.id == 777000 { + let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { + self.infoNode.attributedText = string + shouldUpdateLayout = true + } + } else if let _ = user.botInfo { let string = NSAttributedString(string: self.strings.Bot_GenericBotStatus, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { self.infoNode.attributedText = string diff --git a/TelegramUI/ComponentsThemes.swift b/TelegramUI/ComponentsThemes.swift index 32adb6ac27..c3a4afbe1e 100644 --- a/TelegramUI/ComponentsThemes.swift +++ b/TelegramUI/ComponentsThemes.swift @@ -63,9 +63,3 @@ public extension NavigationControllerTheme { self.init(navigationBar: NavigationBarTheme(rootControllerTheme: presentationTheme), emptyAreaColor: presentationTheme.chatList.backgroundColor, emptyDetailIcon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/EmptyMasterDetailIcon"), color: presentationTheme.chatList.messageTextColor.withAlphaComponent(0.2))) } } - -public extension StatusBarVolumeColors { - convenience init(presentationTheme: PresentationTheme) { - self.init(background: presentationTheme.rootController.navigationBar.secondaryTextColor, foreground: presentationTheme.rootController.navigationBar.primaryTextColor) - } -} diff --git a/TelegramUI/DataAndStorageSettingsController.swift b/TelegramUI/DataAndStorageSettingsController.swift index 14e93fcee2..b36221e39b 100644 --- a/TelegramUI/DataAndStorageSettingsController.swift +++ b/TelegramUI/DataAndStorageSettingsController.swift @@ -361,7 +361,7 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat /*entries.append(.autoplayGifs(presentationData.theme, presentationData.strings.ChatSettings_AutoPlayAnimations, data.automaticMediaDownloadSettings.categories.gif.privateChats))*/ let proxyValue: String - if let _ = data.proxySettings { + if let proxySettings = data.proxySettings, let _ = proxySettings.activeServer, proxySettings.enabled { proxyValue = presentationData.strings.ChatSettings_ConnectionType_UseSocks5 } else { proxyValue = presentationData.strings.GroupInfo_SharedMediaNone @@ -418,11 +418,7 @@ func dataAndStorageController(account: Account) -> ViewController { }, openNetworkUsage: { pushControllerImpl?(networkUsageStatsController(account: account)) }, openProxy: { - let _ = (account.postbox.modify { modifier -> ProxySettings? in - return modifier.getPreferencesEntry(key: PreferencesKeys.proxySettings) as? ProxySettings - } |> deliverOnMainQueue).start(next: { settings in - pushControllerImpl?(proxySettingsController(account: account, currentSettings: settings)) - }) + pushControllerImpl?(proxySettingsController(account: account)) }, toggleAutomaticDownloadMaster: { value in let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { settings in var settings = settings diff --git a/TelegramUI/DefaultDarkAccentPresentationTheme.swift b/TelegramUI/DefaultDarkAccentPresentationTheme.swift index a710d56fc8..ac427410d8 100644 --- a/TelegramUI/DefaultDarkAccentPresentationTheme.swift +++ b/TelegramUI/DefaultDarkAccentPresentationTheme.swift @@ -233,6 +233,10 @@ private let inputMediaPanel = PresentationThemeInputMediaPanel( panelHighlightedIconBackgroundColor: UIColor(rgb: 0x131C26), //!!! stickersBackgroundColor: UIColor(rgb: 0x131C26), stickersSectionTextColor: UIColor(rgb: 0xDBF5FF, alpha: 0.5), + stickersSearchBackgroundColor: UIColor(rgb: 0x151c25), + stickersSearchPlaceholderColor: UIColor(rgb: 0x7b8995), + stickersSearchPrimaryColor: .white, + stickersSearchControlColor: UIColor(rgb: 0x7b8995), gifsBackgroundColor: UIColor(rgb: 0x131C26) ) diff --git a/TelegramUI/DefaultDarkPresentationTheme.swift b/TelegramUI/DefaultDarkPresentationTheme.swift index d2656e7951..73cede6a6a 100644 --- a/TelegramUI/DefaultDarkPresentationTheme.swift +++ b/TelegramUI/DefaultDarkPresentationTheme.swift @@ -233,6 +233,10 @@ private let inputMediaPanel = PresentationThemeInputMediaPanel( panelHighlightedIconBackgroundColor: UIColor(rgb: 0x000000), //!!! stickersBackgroundColor: UIColor(rgb: 0x000000), stickersSectionTextColor: UIColor(rgb: 0x7b7b7b), + stickersSearchBackgroundColor: UIColor(rgb: 0x1c1c1d), + stickersSearchPlaceholderColor: UIColor(rgb: 0x8e8e93), + stickersSearchPrimaryColor: .white, + stickersSearchControlColor: UIColor(rgb: 0x8e8e93), gifsBackgroundColor: UIColor(rgb: 0x000000) ) diff --git a/TelegramUI/DefaultPresentationTheme.swift b/TelegramUI/DefaultPresentationTheme.swift index 5523e15f6f..2da2477601 100644 --- a/TelegramUI/DefaultPresentationTheme.swift +++ b/TelegramUI/DefaultPresentationTheme.swift @@ -332,6 +332,10 @@ private let inputMediaPanel = PresentationThemeInputMediaPanel( panelHighlightedIconBackgroundColor: UIColor(rgb: 0x9099A2, alpha: 0.2), stickersBackgroundColor: UIColor(rgb: 0xE8EBF0), stickersSectionTextColor: UIColor(rgb: 0x9099A2), + stickersSearchBackgroundColor: UIColor(rgb: 0xd9dbe1), + stickersSearchPlaceholderColor: UIColor(rgb: 0x8e8e93), + stickersSearchPrimaryColor: .black, + stickersSearchControlColor: UIColor(rgb: 0x8e8e93), gifsBackgroundColor: .white ) diff --git a/TelegramUI/GalleryControllerNode.swift b/TelegramUI/GalleryControllerNode.swift index cba14cecac..0079966fa9 100644 --- a/TelegramUI/GalleryControllerNode.swift +++ b/TelegramUI/GalleryControllerNode.swift @@ -87,38 +87,59 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog self.scrollView.addSubview(self.pager.view) self.addSubnode(self.footerNode) - self.pager.centralItemIndexOffsetUpdated = { [weak self] indexAndProgress in + var previousIndex: Int? + self.pager.centralItemIndexOffsetUpdated = { [weak self] itemsIndexAndProgress in if let strongSelf = self { var node: GalleryThumbnailContainerNode? - if let (index, progress) = indexAndProgress { + if let (updatedItems, index, progress) = itemsIndexAndProgress { if let (centralId, centralItem) = strongSelf.pager.items[index].thumbnailItem() { - var items: [GalleryThumbnailItem] = [centralItem] - for i in (0 ..< index).reversed() { - if let (id, item) = strongSelf.pager.items[i].thumbnailItem(), id == centralId { - items.insert(item, at: 0) - } else { - break + var items: [GalleryThumbnailItem] + if updatedItems != nil || strongSelf.currentThumbnailContainerNode == nil { + items = [centralItem] + for i in (0 ..< index).reversed() { + if let (id, item) = strongSelf.pager.items[i].thumbnailItem(), id == centralId { + items.insert(item, at: 0) + } else { + break + } } - } - for i in (index + 1) ..< strongSelf.pager.items.count { - if let (id, item) = strongSelf.pager.items[i].thumbnailItem(), id == centralId { - items.append(item) - } else { - break + for i in (index + 1) ..< strongSelf.pager.items.count { + if let (id, item) = strongSelf.pager.items[i].thumbnailItem(), id == centralId { + items.append(item) + } else { + break + } } - } - let convertedIndex = (items.index(where: { $0.isEqual(to: centralItem) })!, progress) - if strongSelf.currentThumbnailContainerNode?.groupId != centralId { - node = GalleryThumbnailContainerNode(groupId: centralId) - node?.updateItems(items, centralIndex: convertedIndex.0, progress: convertedIndex.1) + } else if let currentThumbnailContainerNode = strongSelf.currentThumbnailContainerNode { + items = currentThumbnailContainerNode.items } else { - node = strongSelf.currentThumbnailContainerNode - node?.updateItems(items, centralIndex: convertedIndex.0, progress: convertedIndex.1) + items = [] + assertionFailure() + } + + if let index = items.index(where: { $0.isEqual(to: centralItem) }) { + let convertedIndex = (index, progress) + if strongSelf.currentThumbnailContainerNode?.groupId != centralId { + node = GalleryThumbnailContainerNode(groupId: centralId) + node?.updateItems(items, centralIndex: convertedIndex.0, progress: convertedIndex.1) + } else { + node = strongSelf.currentThumbnailContainerNode + node?.updateItems(items, centralIndex: convertedIndex.0, progress: convertedIndex.1) + } } } } + let previous = previousIndex + previousIndex = itemsIndexAndProgress?.1 if node !== strongSelf.currentThumbnailContainerNode { + let fromLeft: Bool + if let previous = previous, let index = itemsIndexAndProgress?.1 { + fromLeft = index > previous + } else { + fromLeft = true + } if let current = strongSelf.currentThumbnailContainerNode { + current.animateOut(toRight: fromLeft) current.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak current] _ in current?.removeFromSupernode() }) @@ -129,6 +150,7 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog if let (navigationHeight, layout) = strongSelf.containerLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + node.animateIn(fromLeft: fromLeft) } } } @@ -154,7 +176,7 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecog var thumbnailPanelHeight: CGFloat = 0.0 if let currentThumbnailContainerNode = self.currentThumbnailContainerNode { thumbnailPanelHeight = 40.0 - let thumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - 40.0 - thumbnailPanelHeight + 4.0), size: CGSize(width: layout.size.width, height: thumbnailPanelHeight - 4.0)) + let thumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - 40.0 - thumbnailPanelHeight + 4.0 - layout.intrinsicInsets.bottom), size: CGSize(width: layout.size.width, height: thumbnailPanelHeight - 4.0)) transition.updateFrame(node: currentThumbnailContainerNode, frame: thumbnailsFrame) currentThumbnailContainerNode.updateLayout(size: thumbnailsFrame.size, transition: transition) } diff --git a/TelegramUI/GalleryPagerNode.swift b/TelegramUI/GalleryPagerNode.swift index 04920f4867..2a90ea85a8 100644 --- a/TelegramUI/GalleryPagerNode.swift +++ b/TelegramUI/GalleryPagerNode.swift @@ -55,7 +55,8 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { private var containerLayout: (ContainerViewLayout, CGFloat)? var centralItemIndexUpdated: (Int?) -> Void = { _ in } - var centralItemIndexOffsetUpdated: ((Int, CGFloat)?) -> Void = { _ in } + private var invalidatedItems = false + var centralItemIndexOffsetUpdated: (([GalleryItem]?, Int, CGFloat)?) -> Void = { _ in } var toggleControlsVisibility: () -> Void = { } var beginCustomDismiss: () -> Void = { } var completeCustomDismiss: () -> Void = { } @@ -206,6 +207,7 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { itemNode.index = itemNode.index + indexOffset } + self.invalidatedItems = true if let focusOnItem = transaction.focusOnItem { self.centralItemIndex = focusOnItem } @@ -396,11 +398,12 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { private func updateCentralIndexOffset(transition: ContainedViewLayoutTransition) { if let centralIndex = self.centralItemIndex, let itemNode = self.visibleItemNode(at: centralIndex) { let offset: CGFloat = self.scrollView.contentOffset.x + self.pageGap - itemNode.frame.minX - var progress = offset / self.scrollView.bounds.size.height + var progress = offset / self.scrollView.bounds.size.width progress = min(1.0, progress) progress = max(-1.0, progress) - self.centralItemIndexOffsetUpdated((centralIndex, progress)) + self.centralItemIndexOffsetUpdated((self.invalidatedItems ? self.items : nil, centralIndex, progress)) } else { + self.invalidatedItems = false self.centralItemIndexOffsetUpdated(nil) } } diff --git a/TelegramUI/GalleryThumbnailContainerNode.swift b/TelegramUI/GalleryThumbnailContainerNode.swift index 1c7678f186..5e01ac4716 100644 --- a/TelegramUI/GalleryThumbnailContainerNode.swift +++ b/TelegramUI/GalleryThumbnailContainerNode.swift @@ -18,7 +18,7 @@ private final class GalleryThumbnailItemNode: ASDisplayNode { self.imageNode = TransformImageNode() self.imageContainerNode = ASDisplayNode() self.imageContainerNode.clipsToBounds = true - self.imageContainerNode.cornerRadius = 4.0 + self.imageContainerNode.cornerRadius = 2.0 let (signal, imageSize) = item.image self.imageSize = imageSize @@ -48,7 +48,7 @@ final class GalleryThumbnailContainerNode: ASDisplayNode { let groupId: Int64 private let contentNode: ASDisplayNode - private var items: [GalleryThumbnailItem] = [] + private(set) var items: [GalleryThumbnailItem] = [] private var itemNodes: [GalleryThumbnailItemNode] = [] private var centralIndexAndProgress: (Int, CGFloat)? private var currentLayout: CGSize? @@ -103,6 +103,13 @@ final class GalleryThumbnailContainerNode: ASDisplayNode { } } + func updateCentralIndexAndProgress(centralIndex: Int, progress: CGFloat) { + self.centralIndexAndProgress = (centralIndex, progress) + if let size = self.currentLayout { + self.updateLayout(size: size, transition: .immediate) + } + } + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { self.currentLayout = size if let (centralIndex, progress) = self.centralIndexAndProgress { @@ -116,32 +123,84 @@ final class GalleryThumbnailContainerNode: ASDisplayNode { let spacing: CGFloat = 2.0 let centralSpacing: CGFloat = 6.0 let itemHeight: CGFloat = 30.0 - let centralProgress: CGFloat = 1.0 - abs(progress * 2.0) - let leftProgress: CGFloat = max(0.0, -progress * 2.0) - let rightProgress: CGFloat = max(0.0, progress * 2.0) - let centralWidth = self.itemNodes[centralIndex].updateLayout(height: itemHeight, progress: centralProgress, transition: transition) - var centralFrame = CGRect(origin: CGPoint(x: ((size.width - centralWidth) / 2.0), y: 0.0), size: CGSize(width: centralWidth, height: itemHeight)) - centralFrame.origin.x += -progress * 2.0 * centralFrame.width - let currentCentralSpacing: CGFloat = centralProgress * centralSpacing + (1.0 - centralProgress) * spacing - var leftOffset = centralFrame.minX - currentCentralSpacing - var rightOffset = centralFrame.maxX + currentCentralSpacing - transition.updateFrame(node: self.itemNodes[centralIndex], frame: centralFrame) - - for i in (0 ..< centralIndex).reversed() { - let progress: CGFloat = i == centralIndex - 1 ? leftProgress : 0.0 - let itemSpacing: CGFloat = progress * centralSpacing + (1.0 - progress) * spacing - let itemWidth = self.itemNodes[i].updateLayout(height: itemHeight, progress: progress, transition: transition) - transition.updateFrame(node: self.itemNodes[i], frame: CGRect(origin: CGPoint(x: leftOffset - itemWidth, y: 0.0), size: CGSize(width: itemWidth, height: itemHeight))) - leftOffset -= itemSpacing + itemWidth + var itemFrames: [CGRect] = [] + var lastTrailingSpacing: CGFloat = 0.0 + for i in 0 ..< self.itemNodes.count { + let itemProgress: CGFloat + if i == centralIndex { + itemProgress = 1.0 - abs(progress) + } else if i == centralIndex - 1 { + itemProgress = max(0.0, -progress) + } else if i == centralIndex + 1 { + itemProgress = max(0.0, progress) + } else { + itemProgress = 0.0 + } + let itemSpacing = itemProgress * centralSpacing + (1.0 - itemProgress) * spacing + let itemX: CGFloat + if i == 0 { + itemX = lastTrailingSpacing + } else { + itemX = lastTrailingSpacing + itemFrames[itemFrames.count - 1].maxX + itemSpacing * 0.5 + } + if i == self.itemNodes.count - 1 { + lastTrailingSpacing = 0.0 + } else { + lastTrailingSpacing = itemSpacing * 0.5 + } + let itemWidth = self.itemNodes[i].updateLayout(height: itemHeight, progress: itemProgress, transition: transition) + itemFrames.append(CGRect(origin: CGPoint(x: itemX, y: 0.0), size: CGSize(width: itemWidth, height: itemHeight))) } - for i in (centralIndex + 1) ..< self.itemNodes.count { - let progress = i == centralIndex + 1 ? rightProgress : 0.0 - let itemSpacing: CGFloat = progress * centralSpacing + (1.0 - progress) * spacing - let itemWidth = self.itemNodes[i].updateLayout(height: itemHeight, progress: progress, transition: transition) - transition.updateFrame(node: self.itemNodes[i], frame: CGRect(origin: CGPoint(x: rightOffset, y: 0.0), size: CGSize(width: itemWidth, height: itemHeight))) - rightOffset += itemSpacing + itemWidth + for i in 0 ..< itemFrames.count { + if i == centralIndex { + var midX = itemFrames[i].midX + if progress < 0.0 { + if i != 0 { + midX = midX * (1.0 - abs(progress)) + itemFrames[i - 1].midX * abs(progress) + } else { + midX = midX * (1.0 - abs(progress)) + itemFrames[i].offsetBy(dx: -itemFrames[i].width, dy: 0.0).midX * abs(progress) + } + } else if progress > 0.0 { + if i != itemFrames.count - 1 { + midX = midX * (1.0 - abs(progress)) + itemFrames[i + 1].midX * abs(progress) + } else { + midX = midX * (1.0 - abs(progress)) + itemFrames[i].offsetBy(dx: itemFrames[i].width, dy: 0.0).midX * abs(progress) + } + } + let offset = size.width / 2.0 - midX + for j in 0 ..< itemFrames.count { + itemFrames[j].origin.x += offset + } + break + } + } + + for i in 0 ..< self.itemNodes.count { + transition.updateFrame(node: self.itemNodes[i], frame: itemFrames[i]) + } + } + + func animateIn(fromLeft: Bool) { + let collection = fromLeft ? self.itemNodes : self.itemNodes.reversed() + let offset: CGFloat = fromLeft ? 15.0 : -15.0 + var delay: Double = 0.0 + for itemNode in collection { + itemNode.layer.animateScale(from: 0.9, to: 1.0, duration: 0.15 + delay) + itemNode.layer.animatePosition(from: CGPoint(x: offset, y: 0.0), to: CGPoint(), duration: 0.15 + delay, additive: true) + delay += 0.01 + } + } + + func animateOut(toRight: Bool) { + let collection = toRight ? self.itemNodes : self.itemNodes.reversed() + let offset: CGFloat = toRight ? -15.0 : 15.0 + var delay: Double = 0.0 + for itemNode in collection { + itemNode.layer.animateScale(from: 1.0, to: 0.9, duration: 0.15 + delay, removeOnCompletion: false) + itemNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: offset, y: 0.0), duration: 0.15 + delay, removeOnCompletion: false, additive: true) + delay += 0.01 } } } diff --git a/TelegramUI/GenerateTextEntities.swift b/TelegramUI/GenerateTextEntities.swift index 031429770b..68918c9e36 100644 --- a/TelegramUI/GenerateTextEntities.swift +++ b/TelegramUI/GenerateTextEntities.swift @@ -35,20 +35,20 @@ private enum CurrentEntityType { } } -struct EnabledEntityTypes: OptionSet { - var rawValue: Int32 +public struct EnabledEntityTypes: OptionSet { + public var rawValue: Int32 - init(rawValue: Int32) { + public init(rawValue: Int32) { self.rawValue = rawValue } - static let command = EnabledEntityTypes(rawValue: 1 << 0) - static let mention = EnabledEntityTypes(rawValue: 1 << 1) - static let hashtag = EnabledEntityTypes(rawValue: 1 << 2) - static let url = EnabledEntityTypes(rawValue: 1 << 3) - static let phoneNumber = EnabledEntityTypes(rawValue: 1 << 4) + public static let command = EnabledEntityTypes(rawValue: 1 << 0) + public static let mention = EnabledEntityTypes(rawValue: 1 << 1) + public static let hashtag = EnabledEntityTypes(rawValue: 1 << 2) + public static let url = EnabledEntityTypes(rawValue: 1 << 3) + public static let phoneNumber = EnabledEntityTypes(rawValue: 1 << 4) - static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .url, .phoneNumber] + public static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .url, .phoneNumber] } private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, _ range: Range, _ enabledTypes: EnabledEntityTypes, _ entities: inout [MessageTextEntity]) { @@ -95,7 +95,7 @@ func generateChatInputTextEntities(_ text: NSAttributedString) -> [MessageTextEn return entities } -func generateTextEntities(_ text: String, enabledTypes: EnabledEntityTypes, currentEntities: [MessageTextEntity] = []) -> [MessageTextEntity] { +public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityTypes, currentEntities: [MessageTextEntity] = []) -> [MessageTextEntity] { var entities: [MessageTextEntity] = currentEntities let utf16 = text.utf16 diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index 8e9b21deac..43629af69e 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -9,19 +9,6 @@ struct ItemListPeerItemEditing: Equatable { let editable: Bool let editing: Bool let revealed: Bool - - static func ==(lhs: ItemListPeerItemEditing, rhs: ItemListPeerItemEditing) -> Bool { - if lhs.editable != rhs.editable { - return false - } - if lhs.editing != rhs.editing { - return false - } - if lhs.revealed != rhs.revealed { - return false - } - return true - } } enum ItemListPeerItemText { diff --git a/TelegramUI/ItemListSingleLineInputItem.swift b/TelegramUI/ItemListSingleLineInputItem.swift index f680065a2b..b811ca9668 100644 --- a/TelegramUI/ItemListSingleLineInputItem.swift +++ b/TelegramUI/ItemListSingleLineInputItem.swift @@ -181,7 +181,7 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It capitalizationType = .none keyboardType = UIKeyboardType.default case .number: - secureEntry = true + secureEntry = false capitalizationType = .none keyboardType = UIKeyboardType.numberPad } @@ -230,7 +230,7 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It } strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) - strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) if strongSelf.textNode.textField.attributedPlaceholder == nil || !strongSelf.textNode.textField.attributedPlaceholder!.isEqual(to: attributedPlaceholderText) { strongSelf.textNode.textField.attributedPlaceholder = attributedPlaceholderText diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift index 2787cc0a05..a1e8830531 100644 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ b/TelegramUI/MediaNavigationAccessoryItemListNode.swift @@ -65,7 +65,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { } } return false - }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in + }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false }, requestMessageUpdate: { _ in }, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings.defaultSettings) let listNode = ChatHistoryListNode(account: account, chatLocation: .peer(updatedPlaylistPeerId), tagMask: .music, messageId: nil, controllerInteraction: controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: false)) diff --git a/TelegramUI/NetworkStatusTitleView.swift b/TelegramUI/NetworkStatusTitleView.swift index ef63f53099..85304908da 100644 --- a/TelegramUI/NetworkStatusTitleView.swift +++ b/TelegramUI/NetworkStatusTitleView.swift @@ -6,25 +6,32 @@ import Display struct NetworkStatusTitle: Equatable { let text: String let activity: Bool - - static func ==(lhs: NetworkStatusTitle, rhs: NetworkStatusTitle) -> Bool { - return lhs.text == rhs.text && lhs.activity == rhs.activity - } + let hasProxy: Bool + let connectsViaProxy: Bool } final class NetworkStatusTitleView: UIView, NavigationBarTitleView, NavigationBarTitleTransitionNode { - private let titleNode: ASTextNode + private let titleNode: ImmediateTextNode private let lockView: ChatListTitleLockView private let activityIndicator: ActivityIndicator private let buttonView: HighlightTrackingButton + private let proxyNode: ChatTitleProxyNode + private let proxyButton: HighlightTrackingButton private var validLayout: (CGSize, CGRect)? - var title: NetworkStatusTitle = NetworkStatusTitle(text: "", activity: false) { + var title: NetworkStatusTitle = NetworkStatusTitle(text: "", activity: false, hasProxy: false, connectsViaProxy: false) { didSet { if self.title != oldValue { self.titleNode.attributedText = NSAttributedString(string: title.text, font: Font.bold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) self.activityIndicator.isHidden = !self.title.activity + if self.title.connectsViaProxy { + self.proxyNode.status = self.title.activity ? .connecting : .connected + } else { + self.proxyNode.status = .available + } + self.proxyNode.isHidden = !self.title.hasProxy + self.proxyButton.isHidden = !self.title.hasProxy self.setNeedsLayout() } @@ -32,6 +39,7 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleView, NavigationBa } var toggleIsLocked: (() -> Void)? + var openProxySettings: (() -> Void)? private var isPasscodeSet = false private var isManuallyLocked = false @@ -45,16 +53,19 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleView, NavigationBa } else { self.lockView.setIsLocked(false, theme: self.theme, animated: false) } + + self.activityIndicator.type = .custom(self.theme.rootController.navigationBar.primaryTextColor, 22.0, 1.5) + self.proxyNode.theme = self.theme } } init(theme: PresentationTheme) { self.theme = theme - self.titleNode = ASTextNode() + self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false self.titleNode.maximumNumberOfLines = 1 - self.titleNode.truncationMode = .byTruncatingTail + self.titleNode.truncationType = .end self.titleNode.isOpaque = false self.titleNode.isUserInteractionEnabled = false @@ -66,14 +77,21 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleView, NavigationBa self.lockView.isHidden = true self.lockView.isUserInteractionEnabled = false + self.proxyNode = ChatTitleProxyNode(theme: self.theme) + self.proxyNode.isHidden = true + self.buttonView = HighlightTrackingButton() + self.proxyButton = HighlightTrackingButton() + self.proxyButton.isHidden = true super.init(frame: CGRect()) self.addSubnode(self.activityIndicator) - self.addSubview(self.buttonView) self.addSubnode(self.titleNode) + self.addSubnode(self.proxyNode) self.addSubview(self.lockView) + self.addSubview(self.buttonView) + self.addSubview(self.proxyButton) self.buttonView.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { @@ -82,16 +100,36 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleView, NavigationBa strongSelf.lockView.layer.removeAnimation(forKey: "opacity") strongSelf.titleNode.alpha = 0.4 strongSelf.lockView.alpha = 0.4 - } else if !strongSelf.titleNode.alpha.isEqual(to: 1.0) { - strongSelf.titleNode.alpha = 1.0 - strongSelf.lockView.alpha = 1.0 - strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - strongSelf.lockView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } else { + if !strongSelf.titleNode.alpha.isEqual(to: 1.0) { + strongSelf.titleNode.alpha = 1.0 + strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + if !strongSelf.lockView.alpha.isEqual(to: 1.0) { + strongSelf.lockView.alpha = 1.0 + strongSelf.lockView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } } } } self.buttonView.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + + self.proxyButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.proxyNode.layer.removeAnimation(forKey: "opacity") + strongSelf.proxyNode.alpha = 0.4 + } else { + if !strongSelf.proxyNode.alpha.isEqual(to: 1.0) { + strongSelf.proxyNode.alpha = 1.0 + strongSelf.proxyNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + self.proxyButton.addTarget(self, action: #selector(self.proxyButtonPressed), for: .touchUpInside) } required init?(coder aDecoder: NSCoder) { @@ -115,15 +153,31 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleView, NavigationBa if !self.activityIndicator.isHidden { indicatorPadding = indicatorSize.width + 6.0 } + var maxTitleWidth = clearBounds.size.width - indicatorPadding + var alignedTitleWidth = size.width - indicatorPadding + var proxyPadding: CGFloat = 0.0 + if !self.proxyNode.isHidden { + maxTitleWidth -= 20.0 + alignedTitleWidth -= 20.0 + proxyPadding += 36.0 + } + + let titleSize = self.titleNode.updateLayout(CGSize(width: max(1.0, maxTitleWidth), height: size.height)) - let titleSize = self.titleNode.measure(CGSize(width: max(1.0, size.width - indicatorPadding), height: size.height)) let combinedHeight = titleSize.height - let titleFrame = CGRect(origin: CGPoint(x: indicatorPadding + floor((size.width - titleSize.width - indicatorPadding) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) + var titleContentRect = CGRect(origin: CGPoint(x: indicatorPadding + floor((size.width - titleSize.width - indicatorPadding) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) + titleContentRect.origin.x = min(titleContentRect.origin.x, clearBounds.maxX - proxyPadding - titleContentRect.width) + + let titleFrame = titleContentRect self.titleNode.frame = titleFrame + let proxyFrame = CGRect(origin: CGPoint(x: clearBounds.maxX - 8.0 - self.proxyNode.bounds.width, y: 1.0 + floor((size.height - proxyNode.bounds.height) / 2.0)), size: proxyNode.bounds.size) + self.proxyNode.frame = proxyFrame + self.proxyButton.frame = proxyFrame.insetBy(dx: -2.0, dy: -2.0) + let buttonX = max(0.0, titleFrame.minX - 10.0) - self.buttonView.frame = CGRect(origin: CGPoint(x: buttonX, y: 0.0), size: CGSize(width: min(titleFrame.maxX + 28.0, size.width) - buttonX, height: size.height)) + self.buttonView.frame = CGRect(origin: CGPoint(x: buttonX, y: 0.0), size: CGSize(width: min(titleFrame.maxX + 28.0, size.width) - buttonX, height: titleFrame.maxY)) self.lockView.frame = CGRect(x: titleFrame.maxX + 6.0, y: titleFrame.minY + 4.0, width: 2.0, height: 2.0) @@ -149,10 +203,14 @@ final class NetworkStatusTitleView: UIView, NavigationBarTitleView, NavigationBa } } - @objc func buttonPressed() { + @objc private func buttonPressed() { self.toggleIsLocked?() } + @objc private func proxyButtonPressed() { + self.openProxySettings?() + } + func makeTransitionMirrorNode() -> ASDisplayNode { let view = NetworkStatusTitleView(theme: self.theme) view.title = self.title diff --git a/TelegramUI/OngoingCallContext.swift b/TelegramUI/OngoingCallContext.swift index 0a4f654940..5dd67e5a2c 100644 --- a/TelegramUI/OngoingCallContext.swift +++ b/TelegramUI/OngoingCallContext.swift @@ -70,7 +70,7 @@ final class OngoingCallContext { private let audioSessionDisposable = MetaDisposable() - init(callSessionManager: CallSessionManager, internalId: CallSessionInternalId) { + init(callSessionManager: CallSessionManager, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?) { let _ = setupLogs self.internalId = internalId @@ -78,7 +78,7 @@ final class OngoingCallContext { let queue = self.queue self.queue.async { - let context = OngoingCallThreadLocalContext(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue)) + let context = OngoingCallThreadLocalContext(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: proxyServer.flatMap { VoipProxyServer(host: $0.host, port: $0.port, username: $0.username, password: $0.password) }) self.contextRef = Unmanaged.passRetained(context) context.stateChanged = { [weak self] state in self?.contextState.set(.single(state)) diff --git a/TelegramUI/OngoingCallThreadLocalContext.h b/TelegramUI/OngoingCallThreadLocalContext.h index 021d2bf60e..1e1c98965b 100644 --- a/TelegramUI/OngoingCallThreadLocalContext.h +++ b/TelegramUI/OngoingCallThreadLocalContext.h @@ -28,13 +28,24 @@ typedef NS_ENUM(int32_t, OngoingCallState) { @end +@interface VoipProxyServer : NSObject + +@property (nonatomic, strong, readonly) NSString * _Nonnull host; +@property (nonatomic, readonly) int32_t port; +@property (nonatomic, strong, readonly) NSString * _Nullable username; +@property (nonatomic, strong, readonly) NSString * _Nullable password; + +- (instancetype _Nonnull)initWithHost:(NSString * _Nonnull)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password; + +@end + @interface OngoingCallThreadLocalContext : NSObject + (void)setupLoggingFunction:(void (* _Nullable)(NSString * _Nullable))loggingFunction; @property (nonatomic, copy) void (^ _Nullable stateChanged)(OngoingCallState); -- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue; +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy; - (void)startWithKey:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescription * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer; - (void)stop; diff --git a/TelegramUI/OngoingCallThreadLocalContext.mm b/TelegramUI/OngoingCallThreadLocalContext.mm index d56a4286de..1fba90c4cc 100644 --- a/TelegramUI/OngoingCallThreadLocalContext.mm +++ b/TelegramUI/OngoingCallThreadLocalContext.mm @@ -144,13 +144,28 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat }); } +@implementation VoipProxyServer + +- (instancetype _Nonnull)initWithHost:(NSString * _Nonnull)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password { + self = [super init]; + if (self != nil) { + _host = host; + _port = port; + _username = username; + _password = password; + } + return self; +} + +@end + @implementation OngoingCallThreadLocalContext + (void)setupLoggingFunction:(void (*)(NSString *))loggingFunction { TGVoipLoggingFunction = loggingFunction; } -- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue { +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy { self = [super init]; if (self != nil) { _queue = queue; @@ -167,6 +182,10 @@ static void controllerStateCallback(tgvoip::VoIPController *controller, int stat _controller = new tgvoip::VoIPController(); _controller->implData = (void *)((intptr_t)_contextId); + if (proxy != nil) { + _controller->SetProxy(tgvoip::PROXY_SOCKS5, proxy.host.UTF8String, (uint16_t)proxy.port, proxy.username.UTF8String ?: "", proxy.password.UTF8String ?: ""); + } + /*releasable*/ //_controller->SetStateCallback(&controllerStateCallback); diff --git a/TelegramUI/OpenResolvedUrl.swift b/TelegramUI/OpenResolvedUrl.swift index 23e279180a..9750f1e47a 100644 --- a/TelegramUI/OpenResolvedUrl.swift +++ b/TelegramUI/OpenResolvedUrl.swift @@ -25,14 +25,6 @@ func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, navigationCon }), nil) case let .proxy(host, port, username, password): let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - let alertText: String - if let username = username, let password = password { - alertText = presentationData.strings.Settings_ApplyProxyAlertCredentials(host, "\(port)", username, password).0 - } else { - alertText = presentationData.strings.Settings_ApplyProxyAlert(host, "\(port)").0 - } - present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: alertText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - let _ = applyProxySettings(postbox: account.postbox, network: account.network, settings: ProxySettings(host: host, port: port, username: username, password: password, useForCalls: false)).start() - })]), nil) + present(ProxyServerActionSheetController(account: account, theme: presentationData.theme, strings: presentationData.strings, server: ProxyServerSettings(host: host, port: port, username: username, password: password)), nil) } } diff --git a/TelegramUI/OpenUrl.swift b/TelegramUI/OpenUrl.swift index 0bf56a0b7b..10ded0b2c0 100644 --- a/TelegramUI/OpenUrl.swift +++ b/TelegramUI/OpenUrl.swift @@ -217,9 +217,9 @@ public func openExternalUrl(account: Account, url: String, presentationData: Pre if let server = server, !server.isEmpty, let port = port, let _ = Int32(port) { var result = "https://t.me/proxy?proxy=\(server)&port=\(port)" if let user = user { - result += "&user=\(user)" + result += "&user=\((user as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" if let pass = pass { - result += "&pass=\(pass)" + result += "&pass=\((pass as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" } } convertedUrl = result diff --git a/TelegramUI/OverlayPlayerControllerNode.swift b/TelegramUI/OverlayPlayerControllerNode.swift index 914c00492f..defff8a2a8 100644 --- a/TelegramUI/OverlayPlayerControllerNode.swift +++ b/TelegramUI/OverlayPlayerControllerNode.swift @@ -54,7 +54,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec } else { return false } - }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in + }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index dc9472e863..cecff4871f 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -170,6 +170,7 @@ public class PeerMediaCollectionController: TelegramController { } }, openHashtag: { _, _ in }, updateInputState: { _ in + }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in diff --git a/TelegramUI/PresentationCall.swift b/TelegramUI/PresentationCall.swift index 42b22529ff..36dd018461 100644 --- a/TelegramUI/PresentationCall.swift +++ b/TelegramUI/PresentationCall.swift @@ -230,7 +230,7 @@ public final class PresentationCall { private var droppedCall = false private var dropCallKitCallTimer: SwiftSignalKit.Timer? - init(audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, internalId: CallSessionInternalId, peerId: PeerId, isOutgoing: Bool, peer: Peer?) { + init(audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, internalId: CallSessionInternalId, peerId: PeerId, isOutgoing: Bool, peer: Peer?, proxyServer: ProxyServerSettings?) { self.audioSession = audioSession self.callSessionManager = callSessionManager self.callKitIntegration = callKitIntegration @@ -240,7 +240,7 @@ public final class PresentationCall { self.isOutgoing = isOutgoing self.peer = peer - self.ongoingGontext = OngoingCallContext(callSessionManager: self.callSessionManager, internalId: self.internalId) + self.ongoingGontext = OngoingCallContext(callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer) self.sessionStateDisposable = (callSessionManager.callState(internalId: internalId) |> deliverOnMainQueue).start(next: { [weak self] sessionState in diff --git a/TelegramUI/PresentationCallManager.swift b/TelegramUI/PresentationCallManager.swift index 6314db545c..be87c1f5e5 100644 --- a/TelegramUI/PresentationCallManager.swift +++ b/TelegramUI/PresentationCallManager.swift @@ -48,6 +48,9 @@ public final class PresentationCallManager { private let startCallDisposable = MetaDisposable() + private var proxyServer: ProxyServerSettings? + private var proxyServerDisposable: Disposable? + public init(postbox: Postbox, audioSession: ManagedAudioSession, callSessionManager: CallSessionManager) { self.postbox = postbox self.audioSession = audioSession @@ -128,18 +131,30 @@ public final class PresentationCallManager { self?.audioSession.callKitDeactivatedAudioSession() } } + + self.proxyServerDisposable = (postbox.preferencesView(keys: [PreferencesKeys.proxySettings]) + |> deliverOnMainQueue).start(next: { [weak self] preferences in + if let strongSelf = self, let settings = preferences.values[PreferencesKeys.proxySettings] as? ProxySettings { + if settings.enabled && settings.useForCalls { + strongSelf.proxyServer = settings.activeServer + } else { + strongSelf.proxyServer = nil + } + } + }) } deinit { self.ringingStatesDisposable?.dispose() self.removeCurrentCallDisposable.dispose() self.startCallDisposable.dispose() + self.proxyServerDisposable?.dispose() } private func ringingStatesUpdated(_ ringingStates: [(Peer, CallSessionRingingState)]) { if let firstState = ringingStates.first { if self.currentCall == nil { - let call = PresentationCall(audioSession: self.audioSession, callSessionManager: self.callSessionManager, callKitIntegration: self.callKitIntegration, internalId: firstState.1.id, peerId: firstState.1.peerId, isOutgoing: false, peer: firstState.0) + let call = PresentationCall(audioSession: self.audioSession, callSessionManager: self.callSessionManager, callKitIntegration: self.callKitIntegration, internalId: firstState.1.id, peerId: firstState.1.peerId, isOutgoing: false, peer: firstState.0, proxyServer: self.proxyServer) self.currentCall = call self.currentCallPromise.set(.single(call)) self.hasActiveCallsPromise.set(true) @@ -181,7 +196,7 @@ public final class PresentationCallManager { if let currentCall = strongSelf.currentCall { currentCall.rejectBusy() } - let call = PresentationCall(audioSession: strongSelf.audioSession, callSessionManager: strongSelf.callSessionManager, callKitIntegration: strongSelf.callKitIntegration, internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil) + let call = PresentationCall(audioSession: strongSelf.audioSession, callSessionManager: strongSelf.callSessionManager, callKitIntegration: strongSelf.callKitIntegration, internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil, proxyServer: strongSelf.proxyServer) strongSelf.currentCall = call strongSelf.currentCallPromise.set(.single(call)) strongSelf.hasActiveCallsPromise.set(true) diff --git a/TelegramUI/PresentationStrings.swift b/TelegramUI/PresentationStrings.swift index ec5481f206..efd5b909f4 100644 --- a/TelegramUI/PresentationStrings.swift +++ b/TelegramUI/PresentationStrings.swift @@ -837,6 +837,7 @@ public final class PresentationStrings { public let Settings_About_Help: String public let Watch_Conversation_Reply: String public let ShareMenu_CopyShareLink: String + public let Stickers_Search: String public let Channel_Setup_TypePrivateHelp: String public let PhotoEditor_GrainTool: String public let Conversation_SearchByName_Placeholder: String @@ -2051,6 +2052,7 @@ public final class PresentationStrings { public let Map_YouAreHere: String public let PhotoEditor_CurvesTool: String public let Map_LiveLocationFor1Hour: String + public let Stickers_NoStickersFound: String private let _Notification_JoinedChannel: String private let _Notification_JoinedChannel_r: [(Int, NSRange)] public func Notification_JoinedChannel(_ _0: String) -> (String, [(Int, NSRange)]) { @@ -2312,7 +2314,7 @@ public final class PresentationStrings { private let _Checkout_LiabilityAlert: String private let _Checkout_LiabilityAlert_r: [(Int, NSRange)] public func Checkout_LiabilityAlert(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { - return formatWithArgumentRanges(_Checkout_LiabilityAlert, self._Checkout_LiabilityAlert_r, [_1, _1, _1, _2]) + return formatWithArgumentRanges(_Checkout_LiabilityAlert, self._Checkout_LiabilityAlert_r, [_1, _2]) } public let Channel_Info_BlackList: String public let Profile_BotInfo: String @@ -5343,6 +5345,7 @@ public final class PresentationStrings { self.Settings_About_Help = getValue(dict, "Settings.About.Help") self.Watch_Conversation_Reply = getValue(dict, "Watch.Conversation.Reply") self.ShareMenu_CopyShareLink = getValue(dict, "ShareMenu.CopyShareLink") + self.Stickers_Search = getValue(dict, "Stickers.Search") self.Channel_Setup_TypePrivateHelp = getValue(dict, "Channel.Setup.TypePrivateHelp") self.PhotoEditor_GrainTool = getValue(dict, "PhotoEditor.GrainTool") self.Conversation_SearchByName_Placeholder = getValue(dict, "Conversation.SearchByName.Placeholder") @@ -6158,6 +6161,7 @@ public final class PresentationStrings { self.Map_YouAreHere = getValue(dict, "Map.YouAreHere") self.PhotoEditor_CurvesTool = getValue(dict, "PhotoEditor.CurvesTool") self.Map_LiveLocationFor1Hour = getValue(dict, "Map.LiveLocationFor1Hour") + self.Stickers_NoStickersFound = getValue(dict, "Stickers.NoStickersFound") self._Notification_JoinedChannel = getValue(dict, "Notification.JoinedChannel") self._Notification_JoinedChannel_r = extractArgumentRanges(self._Notification_JoinedChannel) self.GroupInfo_ActionRestrict = getValue(dict, "GroupInfo.ActionRestrict") diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift index 23a4fcd3ec..6233511d4b 100644 --- a/TelegramUI/PresentationTheme.swift +++ b/TelegramUI/PresentationTheme.swift @@ -610,14 +610,22 @@ public final class PresentationThemeInputMediaPanel { public let panelHighlightedIconBackgroundColor: UIColor public let stickersBackgroundColor: UIColor public let stickersSectionTextColor: UIColor + public let stickersSearchBackgroundColor: UIColor + public let stickersSearchPlaceholderColor: UIColor + public let stickersSearchPrimaryColor: UIColor + public let stickersSearchControlColor: UIColor public let gifsBackgroundColor: UIColor - public init(panelSerapatorColor: UIColor, panelIconColor: UIColor, panelHighlightedIconBackgroundColor: UIColor, stickersBackgroundColor: UIColor, stickersSectionTextColor: UIColor, gifsBackgroundColor: UIColor) { + public init(panelSerapatorColor: UIColor, panelIconColor: UIColor, panelHighlightedIconBackgroundColor: UIColor, stickersBackgroundColor: UIColor, stickersSectionTextColor: UIColor, stickersSearchBackgroundColor: UIColor, stickersSearchPlaceholderColor: UIColor, stickersSearchPrimaryColor: UIColor, stickersSearchControlColor: UIColor, gifsBackgroundColor: UIColor) { self.panelSerapatorColor = panelSerapatorColor self.panelIconColor = panelIconColor self.panelHighlightedIconBackgroundColor = panelHighlightedIconBackgroundColor self.stickersBackgroundColor = stickersBackgroundColor self.stickersSectionTextColor = stickersSectionTextColor + self.stickersSearchBackgroundColor = stickersSearchBackgroundColor + self.stickersSearchPlaceholderColor = stickersSearchPlaceholderColor + self.stickersSearchPrimaryColor = stickersSearchPrimaryColor + self.stickersSearchControlColor = stickersSearchControlColor self.gifsBackgroundColor = gifsBackgroundColor } } diff --git a/TelegramUI/PresentationThemeSettings.swift b/TelegramUI/PresentationThemeSettings.swift index 13b9df8e2e..12d7b29d1c 100644 --- a/TelegramUI/PresentationThemeSettings.swift +++ b/TelegramUI/PresentationThemeSettings.swift @@ -17,7 +17,7 @@ public enum PresentationThemeReference: PostboxCoding, Equatable { case 0: self = .builtin(PresentationBuiltinThemeReference(rawValue: decoder.decodeInt32ForKey("t", orElse: 0))!) default: - //assertionFailure() + assertionFailure() self = .builtin(.dayClassic) } } @@ -65,7 +65,7 @@ public struct PresentationThemeSettings: PreferencesEntry { } public static var defaultSettings: PresentationThemeSettings { - return PresentationThemeSettings(chatWallpaper: .color(0x000000), theme: .builtin(.nightAccent), fontSize: .regular) + return PresentationThemeSettings(chatWallpaper: .color(0x18222D), theme: .builtin(.nightAccent), fontSize: .regular) } public init(chatWallpaper: TelegramWallpaper, theme: PresentationThemeReference, fontSize: PresentationFontSize) { diff --git a/TelegramUI/ProxyListSettingsController.swift b/TelegramUI/ProxyListSettingsController.swift new file mode 100644 index 0000000000..284a4fa31f --- /dev/null +++ b/TelegramUI/ProxyListSettingsController.swift @@ -0,0 +1,420 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class ProxySettingsControllerArguments { + let toggleEnabled: (Bool) -> Void + let addNewServer: () -> Void + let activateServer: (ProxyServerSettings) -> Void + let editServer: (ProxyServerSettings) -> Void + let removeServer: (ProxyServerSettings) -> Void + let setServerWithRevealedOptions: (ProxyServerSettings?, ProxyServerSettings?) -> Void + let toggleUseForCalls: (Bool) -> Void + + init(toggleEnabled: @escaping (Bool) -> Void, addNewServer: @escaping () -> Void, activateServer: @escaping (ProxyServerSettings) -> Void, editServer: @escaping (ProxyServerSettings) -> Void, removeServer: @escaping (ProxyServerSettings) -> Void, setServerWithRevealedOptions: @escaping (ProxyServerSettings?, ProxyServerSettings?) -> Void, toggleUseForCalls: @escaping (Bool) -> Void) { + self.toggleEnabled = toggleEnabled + self.addNewServer = addNewServer + self.activateServer = activateServer + self.editServer = editServer + self.removeServer = removeServer + self.setServerWithRevealedOptions = setServerWithRevealedOptions + self.toggleUseForCalls = toggleUseForCalls + } +} + +private enum ProxySettingsControllerSection: Int32 { + case enabled + case servers + case calls +} + +private enum ProxyServerAvailabilityStatus: Equatable { + case checking + case notAvailable + case available(Int32) +} + +private struct DisplayProxyServerStatus: Equatable { + let activity: Bool + let text: String + let textActive: Bool +} + +private enum ProxySettingsControllerEntryId: Equatable, Hashable { + case index(Int) + case server(String, Int32, String, String) +} + +private enum ProxySettingsControllerEntry: ItemListNodeEntry { + case enabled(PresentationTheme, String, Bool, Bool) + case serversHeader(PresentationTheme, String) + case addServer(PresentationTheme, String, Bool) + case server(Int, PresentationTheme, PresentationStrings, ProxyServerSettings, Bool, DisplayProxyServerStatus, ProxySettingsServerItemEditing) + case useForCalls(PresentationTheme, String, Bool) + case useForCallsInfo(PresentationTheme, String) + + var section: ItemListSectionId { + switch self { + case .enabled: + return ProxySettingsControllerSection.enabled.rawValue + case .serversHeader, .addServer, .server: + return ProxySettingsControllerSection.servers.rawValue + case .useForCalls, .useForCallsInfo: + return ProxySettingsControllerSection.calls.rawValue + } + } + + var stableId: ProxySettingsControllerEntryId { + switch self { + case .enabled: + return .index(0) + case .serversHeader: + return .index(1) + case .addServer: + return .index(2) + case let .server(_, _, _, settings, _, _, _): + return .server(settings.host, settings.port, settings.username ?? "", settings.password ?? "") + case .useForCalls: + return .index(3) + case .useForCallsInfo: + return .index(4) + } + } + + static func ==(lhs: ProxySettingsControllerEntry, rhs: ProxySettingsControllerEntry) -> Bool { + switch lhs { + case let .enabled(lhsTheme, lhsText, lhsValue, lhsCreatesNew): + if case let .enabled(rhsTheme, rhsText, rhsValue, rhsCreatesNew) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsCreatesNew == rhsCreatesNew { + return true + } else { + return false + } + case let .serversHeader(lhsTheme, lhsText): + if case let .serversHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .addServer(lhsTheme, lhsText, lhsEditing): + if case let .addServer(rhsTheme, rhsText, rhsEditing) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEditing == rhsEditing { + return true + } else { + return false + } + case let .server(lhsIndex, lhsTheme, lhsStrings, lhsSettings, lhsActive, lhsStatus, lhsEditing): + if case let .server(rhsIndex, rhsTheme, rhsStrings, rhsSettings, rhsActive, rhsStatus, rhsEditing) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsSettings == rhsSettings, lhsActive == rhsActive, lhsStatus == rhsStatus, lhsEditing == rhsEditing { + return true + } else { + return false + } + case let .useForCalls(lhsTheme, lhsText, lhsValue): + if case let .useForCalls(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .useForCallsInfo(lhsTheme, lhsText): + if case let .useForCallsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + } + } + + static func <(lhs: ProxySettingsControllerEntry, rhs: ProxySettingsControllerEntry) -> Bool { + switch lhs { + case .enabled: + switch rhs { + case .enabled: + return false + default: + return true + } + case .serversHeader: + switch rhs { + case .enabled, .serversHeader: + return false + default: + return true + } + case .addServer: + switch rhs { + case .enabled, .serversHeader, .addServer: + return false + default: + return true + } + case let .server(lhsIndex, _, _, _, _, _, _): + switch rhs { + case .enabled, .serversHeader, .addServer: + return false + case let .server(rhsIndex, _, _, _, _, _, _): + return lhsIndex < rhsIndex + default: + return true + } + case .useForCalls: + switch rhs { + case .enabled, .serversHeader, .addServer, .server, .useForCalls: + return false + default: + return true + } + case .useForCallsInfo: + return false + } + } + + func item(_ arguments: ProxySettingsControllerArguments) -> ListViewItem { + switch self { + case let .enabled(theme, text, value, createsNew): + return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: !createsNew, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + if createsNew { + arguments.addNewServer() + } else { + arguments.toggleEnabled(value) + } + }) + case let .serversHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .addServer(theme, text, editing): + return ProxySettingsActionItem(theme: theme, title: text, sectionId: self.section, editing: editing, action: { + arguments.addNewServer() + }) + case let .server(_, theme, strings, settings, active, status, editing): + return ProxySettingsServerItem(theme: theme, strings: strings, server: settings, activity: status.activity, active: active, label: status.text, labelAccent: status.textActive, editing: editing, sectionId: self.section, action: { + arguments.activateServer(settings) + }, infoAction: { + arguments.editServer(settings) + }, setServerWithRevealedOptions: { lhs, rhs in + arguments.setServerWithRevealedOptions(lhs, rhs) + }, removeServer: { _ in + arguments.removeServer(settings) + }) + case let .useForCalls(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleUseForCalls(value) + }) + case let .useForCallsInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + } + } +} + +private func proxySettingsControllerEntries(presentationData: PresentationData, state: ProxySettingsControllerState, proxySettings: ProxySettings, statuses: [ProxyServerSettings: ProxyServerStatus], connectionStatus: ConnectionStatus) -> [ProxySettingsControllerEntry] { + var entries: [ProxySettingsControllerEntry] = [] + + entries.append(.enabled(presentationData.theme, presentationData.strings.ChatSettings_ConnectionType_UseProxy, proxySettings.enabled, proxySettings.servers.isEmpty)) + entries.append(.serversHeader(presentationData.theme, "SAVED PROXIES")) + entries.append(.addServer(presentationData.theme, "Add Proxy", state.editing)) + var index = 0 + for server in proxySettings.servers { + let status: ProxyServerStatus = statuses[server] ?? .checking + let displayStatus: DisplayProxyServerStatus + if proxySettings.enabled && server == proxySettings.activeServer { + switch connectionStatus { + case .waitingForNetwork: + displayStatus = DisplayProxyServerStatus(activity: true, text: "waiting for network", textActive: false) + case .connecting, .updating: + displayStatus = DisplayProxyServerStatus(activity: true, text: "connecting", textActive: false) + case .online: + displayStatus = DisplayProxyServerStatus(activity: false, text: "online", textActive: true) + } + } else { + switch status { + case .notAvailable: + displayStatus = DisplayProxyServerStatus(activity: false, text: "not available", textActive: false) + case .checking: + displayStatus = DisplayProxyServerStatus(activity: false, text: "checking", textActive: false) + case let .available(rtt): + let pingTime: Int = Int(rtt * 1000.0) + displayStatus = DisplayProxyServerStatus(activity: false, text: "available (ping: \(pingTime) ms)", textActive: false) + } + } + entries.append(.server(index, presentationData.theme, presentationData.strings, server, server == proxySettings.activeServer, displayStatus, ProxySettingsServerItemEditing(editable: true, editing: state.editing, revealed: state.revealedServer == server))) + index += 1 + } + + entries.append(.useForCalls(presentationData.theme, presentationData.strings.SocksProxySetup_UseForCalls, proxySettings.useForCalls)) + entries.append(.useForCallsInfo(presentationData.theme, presentationData.strings.SocksProxySetup_UseForCallsHelp)) + + return entries +} + +private struct ProxySettingsControllerState: Equatable { + var editing: Bool = false + var revealedServer: ProxyServerSettings? = nil +} + +public func proxySettingsController(account: Account) -> ViewController { + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + let stateValue = Atomic(value: ProxySettingsControllerState()) + let statePromise = ValuePromise(stateValue.with { $0 }) + let updateState: ((ProxySettingsControllerState) -> ProxySettingsControllerState) -> Void = { f in + var changed = false + let value = stateValue.modify { current in + let updated = f(current) + if updated != current { + changed = true + } + return updated + } + if changed { + statePromise.set(value) + } + } + + let arguments = ProxySettingsControllerArguments(toggleEnabled: { value in + let _ = updateProxySettingsInteractively(postbox: account.postbox, network: account.network, { current in + var current = current + current.enabled = value + return current + }).start() + }, addNewServer: { + presentControllerImpl?(proxyServerSettingsController(account: account, currentSettings: nil), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, activateServer: { server in + let _ = updateProxySettingsInteractively(postbox: account.postbox, network: account.network, { current in + var current = current + if current.activeServer != server { + if let _ = current.servers.index(of: server) { + current.activeServer = server + current.enabled = true + } + } + return current + }).start() + }, editServer: { server in + presentControllerImpl?(proxyServerSettingsController(account: account, currentSettings: server), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, removeServer: { server in + let _ = updateProxySettingsInteractively(postbox: account.postbox, network: account.network, { current in + var current = current + if let index = current.servers.index(of: server) { + current.servers.remove(at: index) + if current.activeServer == server { + current.activeServer = nil + current.enabled = false + } + } + return current + }).start() + }, setServerWithRevealedOptions: { server, fromServer in + updateState { state in + var state = state + if (server == nil && fromServer == state.revealedServer) || (server != nil && fromServer == nil) { + state.revealedServer = server + } + return state + } + }, toggleUseForCalls: { value in + let _ = updateProxySettingsInteractively(postbox: account.postbox, network: account.network, { current in + var current = current + current.useForCalls = value + return current + }).start() + }) + + let proxySettings = Promise() + proxySettings.set(account.postbox.preferencesView(keys: [PreferencesKeys.proxySettings]) + |> map { preferencesView -> ProxySettings in + if let value = preferencesView.values[PreferencesKeys.proxySettings] as? ProxySettings { + return value + } else { + return ProxySettings.defaultSettings + } + }) + + let statusesContext = ProxyServersStatuses(account: account, servers: proxySettings.get() + |> map { proxySettings -> [ProxyServerSettings] in + return proxySettings.servers + }) + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), proxySettings.get(), statusesContext.statuses(), account.network.connectionStatus) + |> map { presentationData, state, proxySettings, statuses, connectionStatus -> (ItemListControllerState, (ItemListNodeState, ProxySettingsControllerEntry.ItemGenerationArguments)) in + let rightNavigationButton: ItemListNavigationButton? + if proxySettings.servers.isEmpty { + rightNavigationButton = nil + } else if state.editing { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + updateState { state in + var state = state + state.editing = false + return state + } + }) + } else { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { + updateState { state in + var state = state + state.editing = true + return state + } + }) + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.SocksProxySetup_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: proxySettingsControllerEntries(presentationData: presentationData, state: state, proxySettings: proxySettings, statuses: statuses, connectionStatus: connectionStatus), style: .blocks) + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(account: account, state: signal) + presentControllerImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + controller.reorderEntry = { fromIndex, toIndex, entries in + let fromEntry = entries[fromIndex] + guard case let .server(_, _, _, fromServer, _, _, _) = fromEntry else { + return + } + var referenceServer: ProxyServerSettings? + var beforeAll = false + var afterAll = false + if toIndex < entries.count { + switch entries[toIndex] { + case let .server(_, _, _, toServer, _, _, _): + referenceServer = toServer + default: + if entries[toIndex] < fromEntry { + beforeAll = true + } else { + afterAll = true + } + } + } else { + afterAll = true + } + + let _ = updateProxySettingsInteractively(postbox: account.postbox, network: account.network, { current in + var current = current + if let index = current.servers.index(of: fromServer) { + current.servers.remove(at: index) + } + if let referenceServer = referenceServer { + var inserted = false + for i in 0 ..< current.servers.count { + if current.servers[i] == referenceServer { + if fromIndex < toIndex { + current.servers.insert(fromServer, at: i + 1) + } else { + current.servers.insert(fromServer, at: i) + } + inserted = true + break + } + } + if !inserted { + current.servers.append(fromServer) + } + } else if beforeAll { + current.servers.insert(fromServer, at: 0) + } else if afterAll { + current.servers.append(fromServer) + } + return current + }).start() + } + return controller +} diff --git a/TelegramUI/ProxyServerActionSheetController.swift b/TelegramUI/ProxyServerActionSheetController.swift new file mode 100644 index 0000000000..d0a4371bb4 --- /dev/null +++ b/TelegramUI/ProxyServerActionSheetController.swift @@ -0,0 +1,328 @@ +import Foundation +import Display +import TelegramCore +import Postbox +import AsyncDisplayKit +import UIKit +import SwiftSignalKit + +final class ProxyServerActionSheetController: ActionSheetController { + private let theme: PresentationTheme + private let strings: PresentationStrings + + private let _ready = Promise() + override var ready: Promise { + return self._ready + } + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, server: ProxyServerSettings) { + self.theme = theme + self.strings = strings + + let sheetTheme = ActionSheetControllerTheme(presentationTheme: theme) + super.init(theme: sheetTheme) + + self._ready.set(.single(true)) + + var items: [ActionSheetItem] = [] + items.append(ProxyServerInfoItem(strings: strings, server: server)) + items.append(ProxyServerActionItem(account: account, strings: strings, server: server, dismiss: { [weak self] in + self?.dismissAnimated() + }, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + })) + self.setItemGroups([ + ActionSheetItemGroup(items: items), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in + self?.dismissAnimated() + }) + ]) + ]) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class ProxyServerInfoItem: ActionSheetItem { + private let strings: PresentationStrings + private let server: ProxyServerSettings + + init(strings: PresentationStrings, server: ProxyServerSettings) { + self.strings = strings + self.server = server + } + + func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { + return ProxyServerInfoItemNode(theme: theme, strings: self.strings, server: self.server) + } + + func updateNode(_ node: ActionSheetItemNode) { + } +} + +private let textFont = Font.regular(16.0) + +private final class ProxyServerInfoItemNode: ActionSheetItemNode { + private let theme: ActionSheetControllerTheme + private let strings: PresentationStrings + private let server: ProxyServerSettings + + private let fieldNodes: [(ImmediateTextNode, ImmediateTextNode)] + + init(theme: ActionSheetControllerTheme, strings: PresentationStrings, server: ProxyServerSettings) { + self.theme = theme + self.strings = strings + self.server = server + + var fieldNodes: [(ImmediateTextNode, ImmediateTextNode)] = [] + let serverTitleNode = ImmediateTextNode() + serverTitleNode.isLayerBacked = true + serverTitleNode.displaysAsynchronously = false + serverTitleNode.attributedText = NSAttributedString(string: "Server", font: textFont, textColor: theme.secondaryTextColor) + let serverTextNode = ImmediateTextNode() + serverTextNode.isLayerBacked = true + serverTextNode.displaysAsynchronously = false + serverTextNode.attributedText = NSAttributedString(string: server.host, font: textFont, textColor: theme.primaryTextColor) + fieldNodes.append((serverTitleNode, serverTextNode)) + + let portTitleNode = ImmediateTextNode() + portTitleNode.isLayerBacked = true + portTitleNode.displaysAsynchronously = false + portTitleNode.attributedText = NSAttributedString(string: "Port", font: textFont, textColor: theme.secondaryTextColor) + let portTextNode = ImmediateTextNode() + portTextNode.isLayerBacked = true + portTextNode.displaysAsynchronously = false + portTextNode.attributedText = NSAttributedString(string: "\(server.port)", font: textFont, textColor: theme.primaryTextColor) + fieldNodes.append((portTitleNode, portTextNode)) + + if let username = server.username { + let usernameTitleNode = ImmediateTextNode() + usernameTitleNode.isLayerBacked = true + usernameTitleNode.displaysAsynchronously = false + usernameTitleNode.attributedText = NSAttributedString(string: "Username", font: textFont, textColor: theme.secondaryTextColor) + let usernameTextNode = ImmediateTextNode() + usernameTextNode.isLayerBacked = true + usernameTextNode.displaysAsynchronously = false + usernameTextNode.attributedText = NSAttributedString(string: username, font: textFont, textColor: theme.primaryTextColor) + fieldNodes.append((usernameTitleNode, usernameTextNode)) + } + + if let password = server.password { + let passwordTitleNode = ImmediateTextNode() + passwordTitleNode.isLayerBacked = true + passwordTitleNode.displaysAsynchronously = false + passwordTitleNode.attributedText = NSAttributedString(string: "Password", font: textFont, textColor: theme.secondaryTextColor) + let passwordTextNode = ImmediateTextNode() + passwordTextNode.isLayerBacked = true + passwordTextNode.displaysAsynchronously = false + passwordTextNode.attributedText = NSAttributedString(string: password, font: textFont, textColor: theme.primaryTextColor) + fieldNodes.append((passwordTitleNode, passwordTextNode)) + } + + self.fieldNodes = fieldNodes + + super.init(theme: theme) + + for (lhs, rhs) in fieldNodes { + self.addSubnode(lhs) + self.addSubnode(rhs) + } + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 36.0 * CGFloat(self.fieldNodes.count) + 12.0) + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + var offset: CGFloat = 15.0 + for (lhs, rhs) in self.fieldNodes { + let lhsSize = lhs.updateLayout(CGSize(width: size.width - 18.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)) + lhs.frame = CGRect(origin: CGPoint(x: 18, y: offset), size: lhsSize) + + let rhsSize = rhs.updateLayout(CGSize(width: max(1.0, size.width - 18 * 2.0 - lhsSize.width - 4.0), height: CGFloat.greatestFiniteMagnitude)) + rhs.frame = CGRect(origin: CGPoint(x: size.width - 18 - rhsSize.width, y: offset), size: rhsSize) + + offset += 36.0 + } + } +} + +private final class ProxyServerActionItem: ActionSheetItem { + private let account: Account + private let strings: PresentationStrings + private let server: ProxyServerSettings + private let dismiss: () -> Void + private let present: (ViewController, Any?) -> Void + + init(account: Account, strings: PresentationStrings, server: ProxyServerSettings, dismiss: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void) { + self.account = account + self.strings = strings + self.server = server + self.dismiss = dismiss + self.present = present + } + + func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { + return ProxyServerActionItemNode(account: self.account, theme: theme, strings: self.strings, server: self.server, dismiss: self.dismiss, present: self.present) + } + + func updateNode(_ node: ActionSheetItemNode) { + } +} + +private final class ProxyServerActionItemNode: ActionSheetItemNode { + private let account: Account + private let theme: ActionSheetControllerTheme + private let strings: PresentationStrings + private let server: ProxyServerSettings + private let dismiss: () -> Void + private let present: (ViewController, Any?) -> Void + + private let buttonNode: HighlightableButtonNode + private let titleNode: ImmediateTextNode + private let activityIndicator: ActivityIndicator + + private let disposable = MetaDisposable() + private var revertSettings: ProxySettings? + + init(account: Account, theme: ActionSheetControllerTheme, strings: PresentationStrings, server: ProxyServerSettings, dismiss: @escaping () -> Void, present: @escaping (ViewController, Any?) -> Void) { + self.account = account + self.theme = theme + self.strings = strings + self.server = server + self.dismiss = dismiss + self.present = present + + self.titleNode = ImmediateTextNode() + self.titleNode.isLayerBacked = true + self.titleNode.displaysAsynchronously = false + self.titleNode.attributedText = NSAttributedString(string: "Connect", font: Font.regular(20.0), textColor: theme.controlAccentColor) + + self.activityIndicator = ActivityIndicator(type: .custom(theme.controlAccentColor, 24.0, 1.5)) + self.activityIndicator.isHidden = true + + self.buttonNode = HighlightableButtonNode() + + super.init(theme: theme) + + self.addSubnode(self.titleNode) + self.addSubnode(self.activityIndicator) + self.addSubnode(self.buttonNode) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor + } else { + UIView.animate(withDuration: 0.3, animations: { + strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor + }) + } + } + } + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + deinit { + self.disposable.dispose() + if let revertSettings = self.revertSettings { + let _ = updateProxySettingsInteractively(postbox: self.account.postbox, network: self.account.network, { _ in + return revertSettings + }) + } + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 57.0) + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) + + let labelSize = self.titleNode.updateLayout(CGSize(width: max(1.0, size.width - 10.0), height: size.height)) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize) + self.titleNode.frame = titleFrame + self.activityIndicator.frame = CGRect(origin: CGPoint(x: 14.0, y: titleFrame.minY - 0.0), size: CGSize(width: 24.0, height: 24.0)) + } + + @objc private func buttonPressed() { + let proxyServerSettings = self.server + let network = self.account.network + let _ = (self.account.postbox.modify { modifier -> ProxySettings in + var currentSettings: ProxySettings? + updateProxySettingsInteractively(modifier: modifier, network: network, { settings in + currentSettings = settings + var settings = settings + if let index = settings.servers.index(of: proxyServerSettings) { + settings.servers[index] = proxyServerSettings + settings.activeServer = proxyServerSettings + } else { + settings.servers.insert(proxyServerSettings, at: 0) + settings.activeServer = proxyServerSettings + } + settings.enabled = true + return settings + }) + return currentSettings ?? ProxySettings.defaultSettings + } |> deliverOnMainQueue).start(next: { [weak self] previousSettings in + if let strongSelf = self { + strongSelf.revertSettings = previousSettings + strongSelf.buttonNode.isUserInteractionEnabled = false + strongSelf.titleNode.attributedText = NSAttributedString(string: "Connecting...", font: Font.regular(20.0), textColor: strongSelf.theme.primaryTextColor) + strongSelf.activityIndicator.isHidden = false + strongSelf.setNeedsLayout() + + let signal = strongSelf.account.network.connectionStatus + |> filter { status in + switch status { + case let .online(proxyAddress): + if proxyAddress == proxyServerSettings.host { + return true + } else { + return false + } + default: + return false + } + } + |> map { _ -> Bool in + return true + } + |> timeout(15.0, queue: Queue.mainQueue(), alternate: .single(false)) + |> deliverOnMainQueue + strongSelf.disposable.set(signal.start(next: { value in + if let strongSelf = self { + strongSelf.activityIndicator.isHidden = true + strongSelf.revertSettings = nil + if value { + strongSelf.dismiss() + } else { + let _ = updateProxySettingsInteractively(postbox: strongSelf.account.postbox, network: strongSelf.account.network, { _ in + return previousSettings + }) + strongSelf.titleNode.attributedText = NSAttributedString(string: "Connect", font: Font.regular(20.0), textColor: strongSelf.theme.controlAccentColor) + strongSelf.buttonNode.isUserInteractionEnabled = true + strongSelf.setNeedsLayout() + + let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } + + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "Couldn't connect to the proxy.", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } + } + })) + } + }) + } +} diff --git a/TelegramUI/ProxySettingsController.swift b/TelegramUI/ProxyServerSettingsController.swift similarity index 53% rename from TelegramUI/ProxySettingsController.swift rename to TelegramUI/ProxyServerSettingsController.swift index f70450e104..78d53b4cff 100644 --- a/TelegramUI/ProxySettingsController.swift +++ b/TelegramUI/ProxyServerSettingsController.swift @@ -4,28 +4,23 @@ import SwiftSignalKit import Postbox import TelegramCore -private final class ProxySettingsControllerArguments { - let updateState: ((ProxySettingsControllerState) -> ProxySettingsControllerState) -> Void +private final class proxyServerSettingsControllerArguments { + let updateState: ((proxyServerSettingsControllerState) -> proxyServerSettingsControllerState) -> Void let share: () -> Void - init(updateState: @escaping ((ProxySettingsControllerState) -> ProxySettingsControllerState) -> Void, share: @escaping () -> Void) { + init(updateState: @escaping ((proxyServerSettingsControllerState) -> proxyServerSettingsControllerState) -> Void, share: @escaping () -> Void) { self.updateState = updateState self.share = share } } private enum ProxySettingsSection: Int32 { - case mode case connection case credentials - case calls case share } private enum ProxySettingsEntry: ItemListNodeEntry { - case modeDisabled(PresentationTheme, String, Bool) - case modeSocks5(PresentationTheme, String, Bool) - case connectionHeader(PresentationTheme, String) case connectionServer(PresentationTheme, String, String) case connectionPort(PresentationTheme, String, String) @@ -34,21 +29,14 @@ private enum ProxySettingsEntry: ItemListNodeEntry { case credentialsUsername(PresentationTheme, String, String) case credentialsPassword(PresentationTheme, String, String) - case useForCalls(PresentationTheme, String, Bool) - case useForCallsInfo(PresentationTheme, String) - case share(PresentationTheme, String, Bool) var section: ItemListSectionId { switch self { - case .modeDisabled, .modeSocks5: - return ProxySettingsSection.mode.rawValue case .connectionHeader, .connectionServer, .connectionPort: return ProxySettingsSection.connection.rawValue case .credentialsHeader, .credentialsUsername, .credentialsPassword: return ProxySettingsSection.credentials.rawValue - case .useForCalls, .useForCallsInfo: - return ProxySettingsSection.calls.rawValue case .share: return ProxySettingsSection.share.rawValue } @@ -56,10 +44,6 @@ private enum ProxySettingsEntry: ItemListNodeEntry { var stableId: Int32 { switch self { - case .modeDisabled: - return 0 - case .modeSocks5: - return 1 case .connectionHeader: return 2 case .connectionServer: @@ -72,29 +56,13 @@ private enum ProxySettingsEntry: ItemListNodeEntry { return 6 case .credentialsPassword: return 7 - case .useForCalls: - return 8 - case .useForCallsInfo: - return 9 case .share: - return 10 + return 8 } } static func ==(lhs: ProxySettingsEntry, rhs: ProxySettingsEntry) -> Bool { switch lhs { - case let .modeDisabled(lhsTheme, lhsText, lhsValue): - if case let .modeDisabled(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .modeSocks5(lhsTheme, lhsText, lhsValue): - if case let .modeSocks5(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } case let .connectionHeader(lhsTheme, lhsText): if case let .connectionHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -131,18 +99,6 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } else { return false } - case let .useForCalls(lhsTheme, lhsText, lhsValue): - if case let .useForCalls(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .useForCallsInfo(lhsTheme, lhsText): - if case let .useForCallsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } case let .share(lhsTheme, lhsText, lhsValue): if case let .share(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true @@ -156,24 +112,8 @@ private enum ProxySettingsEntry: ItemListNodeEntry { return lhs.stableId < rhs.stableId } - func item(_ arguments: ProxySettingsControllerArguments) -> ListViewItem { + func item(_ arguments: proxyServerSettingsControllerArguments) -> ListViewItem { switch self { - case let .modeDisabled(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { - arguments.updateState { current in - var state = current - state.enabled = false - return state - } - }) - case let .modeSocks5(theme, text, value): - return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { - arguments.updateState { current in - var state = current - state.enabled = true - return state - } - }) case let .connectionHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .connectionServer(theme, placeholder, text): @@ -210,16 +150,6 @@ private enum ProxySettingsEntry: ItemListNodeEntry { return state } }, action: {}) - case let .useForCalls(theme, text, value): - return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: true, sectionId: self.section, style: .blocks, updated: { value in - arguments.updateState { current in - var state = current - state.useForCalls = value - return state - } - }) - case let .useForCallsInfo(theme, text): - return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) case let .share(theme, text, enabled): return ItemListActionItem(theme: theme, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.share() @@ -228,40 +158,13 @@ private enum ProxySettingsEntry: ItemListNodeEntry { } } -private struct ProxySettingsControllerState: Equatable { - var enabled: Bool +private struct proxyServerSettingsControllerState: Equatable { var host: String var port: String var username: String var password: String - var useForCalls: Bool - - static func ==(lhs: ProxySettingsControllerState, rhs: ProxySettingsControllerState) -> Bool { - if lhs.enabled != rhs.enabled { - return false - } - if lhs.host != rhs.host { - return false - } - if lhs.port != rhs.port { - return false - } - if lhs.username != rhs.username { - return false - } - if lhs.password != rhs.password { - return false - } - if lhs.useForCalls != rhs.useForCalls { - return false - } - return true - } var isComplete: Bool { - if !self.enabled { - return false - } if self.host.isEmpty || self.port.isEmpty || Int(self.port) == nil { return false } @@ -269,50 +172,42 @@ private struct ProxySettingsControllerState: Equatable { } } -private func proxySettingsControllerEntries(presentationData: PresentationData, state: ProxySettingsControllerState) -> [ProxySettingsEntry] { +private func proxyServerSettingsControllerEntries(presentationData: PresentationData, state: proxyServerSettingsControllerState) -> [ProxySettingsEntry] { var entries: [ProxySettingsEntry] = [] - entries.append(.modeDisabled(presentationData.theme, presentationData.strings.SocksProxySetup_TypeNone, !state.enabled)) - entries.append(.modeSocks5(presentationData.theme, presentationData.strings.SocksProxySetup_TypeSocks, state.enabled)) + entries.append(.connectionHeader(presentationData.theme, presentationData.strings.SocksProxySetup_Connection.uppercased())) + entries.append(.connectionServer(presentationData.theme, presentationData.strings.SocksProxySetup_Hostname, state.host)) + entries.append(.connectionPort(presentationData.theme, presentationData.strings.SocksProxySetup_Port, state.port)) - if state.enabled { - entries.append(.connectionHeader(presentationData.theme, presentationData.strings.SocksProxySetup_Connection.uppercased())) - entries.append(.connectionServer(presentationData.theme, presentationData.strings.SocksProxySetup_Hostname, state.host)) - entries.append(.connectionPort(presentationData.theme, presentationData.strings.SocksProxySetup_Port, state.port)) - - entries.append(.credentialsHeader(presentationData.theme, presentationData.strings.SocksProxySetup_Credentials)) - entries.append(.credentialsUsername(presentationData.theme, presentationData.strings.SocksProxySetup_Username, state.username)) - entries.append(.credentialsPassword(presentationData.theme, presentationData.strings.SocksProxySetup_Password, state.password)) - - entries.append(.useForCalls(presentationData.theme, presentationData.strings.SocksProxySetup_UseForCalls, state.useForCalls)) - entries.append(.useForCallsInfo(presentationData.theme, presentationData.strings.SocksProxySetup_UseForCallsHelp)) - - entries.append(.share(presentationData.theme, presentationData.strings.Conversation_ContextMenuShare, state.isComplete)) - } + entries.append(.credentialsHeader(presentationData.theme, presentationData.strings.SocksProxySetup_Credentials)) + entries.append(.credentialsUsername(presentationData.theme, presentationData.strings.SocksProxySetup_Username, state.username)) + entries.append(.credentialsPassword(presentationData.theme, presentationData.strings.SocksProxySetup_Password, state.password)) + + entries.append(.share(presentationData.theme, presentationData.strings.Conversation_ContextMenuShare, state.isComplete)) return entries } -func proxySettingsController(account: Account, currentSettings: ProxySettings?) -> ViewController { - let initialState = ProxySettingsControllerState(enabled: currentSettings != nil, host: currentSettings?.host ?? "", port: (currentSettings?.port).flatMap { "\($0)" } ?? "", username: currentSettings?.username ?? "", password: currentSettings?.password ?? "", useForCalls: currentSettings?.useForCalls ?? false) +func proxyServerSettingsController(account: Account, currentSettings: ProxyServerSettings?) -> ViewController { + let initialState = proxyServerSettingsControllerState(host: currentSettings?.host ?? "", port: (currentSettings?.port).flatMap { "\($0)" } ?? "", username: currentSettings?.username ?? "", password: currentSettings?.password ?? "") let stateValue = Atomic(value: initialState) let statePromise = ValuePromise(initialState, ignoreRepeated: true) - let updateState: ((ProxySettingsControllerState) -> ProxySettingsControllerState) -> Void = { f in + let updateState: ((proxyServerSettingsControllerState) -> proxyServerSettingsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var presentImpl: ((ViewController, Any?) -> Void)? var dismissImpl: (() -> Void)? - let arguments = ProxySettingsControllerArguments(updateState: { f in + let arguments = proxyServerSettingsControllerArguments(updateState: { f in updateState(f) }, share: { let state = stateValue.with { $0 } - if state.enabled && state.isComplete { + if state.isComplete { let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } var result = "tg://socks?server=\(state.host)&port=\(state.port)" if !state.username.isEmpty { - result += "&user=\(state.username)&pass=\(state.password)" + result += "&user=\((state.username as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")&pass=\((state.password as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" } UIPasteboard.general.string = result @@ -323,17 +218,39 @@ func proxySettingsController(account: Account, currentSettings: ProxySettings?) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) |> deliverOnMainQueue |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, ProxySettingsEntry.ItemGenerationArguments)) in - let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: !state.enabled || state.isComplete, action: { - var proxySettings: ProxySettings? - if state.enabled && state.isComplete, let port = Int32(state.port) { - proxySettings = ProxySettings(host: state.host, port: port, username: state.username.isEmpty ? nil : state.username, password: state.password.isEmpty ? nil : state.password, useForCalls: state.useForCalls) - } - let _ = applyProxySettings(postbox: account.postbox, network: account.network, settings: proxySettings).start() + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) + let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: { + var proxyServerSettings: ProxyServerSettings? + if state.isComplete, let port = Int32(state.port) { + proxyServerSettings = ProxyServerSettings(host: state.host, port: port, username: state.username.isEmpty ? nil : state.username, password: state.password.isEmpty ? nil : state.password) + } + if let proxyServerSettings = proxyServerSettings { + let _ = (updateProxySettingsInteractively(postbox: account.postbox, network: account.network, { settings in + var settings = settings + if let currentSettings = currentSettings { + if let index = settings.servers.index(of: currentSettings) { + settings.servers[index] = proxyServerSettings + if settings.activeServer == currentSettings { + settings.activeServer = proxyServerSettings + } + } + } else { + settings.servers.append(proxyServerSettings) + if settings.servers.count == 1 { + settings.activeServer = proxyServerSettings + } + } + return settings + }) |> deliverOnMainQueue).start(completed: { + dismissImpl?() + }) + } + }) - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.SocksProxySetup_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: proxySettingsControllerEntries(presentationData: presentationData, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.SocksProxySetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(entries: proxyServerSettingsControllerEntries(presentationData: presentationData, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -343,7 +260,7 @@ func proxySettingsController(account: Account, currentSettings: ProxySettings?) controller?.present(c, in: .window(.root), with: d) } dismissImpl = { [weak controller] in - let _ = (controller?.navigationController as? NavigationController)?.popViewController(animated: true) + let _ = controller?.dismiss() } return controller } diff --git a/TelegramUI/ProxySettingsActionItem.swift b/TelegramUI/ProxySettingsActionItem.swift new file mode 100644 index 0000000000..679a98f5e9 --- /dev/null +++ b/TelegramUI/ProxySettingsActionItem.swift @@ -0,0 +1,242 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit + +class ProxySettingsActionItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let title: String + let editing: Bool + let sectionId: ItemListSectionId + let action: () -> Void + + init(theme: PresentationTheme, title: String, sectionId: ItemListSectionId, editing: Bool, action: @escaping () -> Void) { + self.theme = theme + self.title = title + self.editing = editing + self.sectionId = sectionId + self.action = action + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = ProxySettingsActionItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply(false) }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? ProxySettingsActionItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + var animated = true + if case .None = animation { + animated = false + } + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { + apply(animated) + }) + } + } + } + } + } + + var selectable: Bool = true + + func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action() + } +} + +private let titleFont = Font.regular(17.0) + +class ProxySettingsActionItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + + private let iconNode: ASImageNode + private let titleNode: TextNode + + private var item: ProxySettingsActionItem? + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displayWithoutProcessing = true + self.iconNode.displaysAsynchronously = false + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.iconNode) + self.addSubnode(self.titleNode) + } + + func asyncLayout() -> (_ item: ProxySettingsActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + + let currentItem = self.item + + return { item, params, neighbors in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + let leftInset: CGFloat = 50.0 + params.leftInset + + let editingOffset: CGFloat = (item.editing ? 38.0 : 0.0) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let separatorHeight = UIScreenPixel + + let insets = itemListNeighborsGroupedInsets(neighbors) + let contentSize = CGSize(width: params.width, height: 44.0) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + let icon = PresentationResourcesItemList.plusIconImage(item.theme) + + return (layout, { [weak self] animated in + if let strongSelf = self { + strongSelf.item = item + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + + let _ = titleApply() + + let transition: ContainedViewLayoutTransition + if animated { + transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + + strongSelf.iconNode.image = icon + if let image = icon { + transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + editingOffset + floor((leftInset - params.leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)) + } + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false + } + + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + editingOffset + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + } + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) + + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset, y: 11.0), size: titleLayout.size)) + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 44.0 + UIScreenPixel + UIScreenPixel)) + } + }) + } + } + + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/TelegramUI/ProxySettingsServerItem.swift b/TelegramUI/ProxySettingsServerItem.swift new file mode 100644 index 0000000000..b9e90368ab --- /dev/null +++ b/TelegramUI/ProxySettingsServerItem.swift @@ -0,0 +1,494 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +private let activitySize = CGSize(width: 24.0, height: 24.0) + +struct ProxySettingsServerItemEditing: Equatable { + let editable: Bool + let editing: Bool + let revealed: Bool +} + +final class ProxySettingsServerItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let strings: PresentationStrings + let server: ProxyServerSettings + let activity: Bool + let active: Bool + let label: String + let labelAccent: Bool + let editing: ProxySettingsServerItemEditing + let sectionId: ItemListSectionId + let action: () -> Void + let infoAction: () -> Void + let setServerWithRevealedOptions: (ProxyServerSettings?, ProxyServerSettings?) -> Void + let removeServer: (ProxyServerSettings) -> Void + + init(theme: PresentationTheme, strings: PresentationStrings, server: ProxyServerSettings, activity: Bool, active: Bool, label: String, labelAccent: Bool, editing: ProxySettingsServerItemEditing, sectionId: ItemListSectionId, action: @escaping () -> Void, infoAction: @escaping () -> Void, setServerWithRevealedOptions: @escaping (ProxyServerSettings?, ProxyServerSettings?) -> Void, removeServer: @escaping (ProxyServerSettings) -> Void) { + self.theme = theme + self.strings = strings + self.server = server + self.activity = activity + self.active = active + self.label = label + self.labelAccent = labelAccent + self.editing = editing + self.sectionId = sectionId + self.action = action + self.infoAction = infoAction + self.setServerWithRevealedOptions = setServerWithRevealedOptions + self.removeServer = removeServer + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = ProxySettingsServerItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply(false) }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? ProxySettingsServerItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + var animated = true + if case .None = animation { + animated = false + } + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { + apply(animated) + }) + } + } + } + } + } + + var selectable: Bool = true + + func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action() + } +} + +private let titleFont = Font.regular(17.0) +private let statusFont = Font.regular(14.0) + +class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + + private let titleNode: TextNode + private let infoIconNode: ASImageNode + private let infoButtonNode: HighlightableButtonNode + private let statusNode: TextNode + private let checkNode: ASImageNode + private let activityNode: ActivityIndicator + + private var editableControlNode: ItemListEditableControlNode? + private var reorderControlNode: ItemListEditableReorderControlNode? + + private var item: ProxySettingsServerItem? + private var layoutParams: ListViewItemLayoutParams? + + override var canBeSelected: Bool { + if self.editableControlNode != nil { + return false + } + return true + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.infoIconNode = ASImageNode() + self.infoIconNode.isLayerBacked = true + self.infoIconNode.displayWithoutProcessing = true + self.infoIconNode.displaysAsynchronously = false + + self.checkNode = ASImageNode() + self.checkNode.isLayerBacked = true + self.checkNode.displayWithoutProcessing = true + self.checkNode.displaysAsynchronously = false + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.statusNode = TextNode() + self.statusNode.isLayerBacked = true + self.statusNode.contentMode = .left + self.statusNode.contentsScale = UIScreen.main.scale + + self.activityNode = ActivityIndicator(type: .custom(.blue, activitySize.width, 2.0)) + self.activityNode.isHidden = true + + self.infoButtonNode = HighlightableButtonNode() + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.titleNode) + self.addSubnode(self.statusNode) + self.addSubnode(self.checkNode) + self.addSubnode(self.infoIconNode) + self.addSubnode(self.activityNode) + self.addSubnode(self.infoButtonNode) + + self.infoButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.infoIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.infoIconNode.alpha = 0.4 + } else { + strongSelf.infoIconNode.alpha = 1.0 + strongSelf.infoIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.infoButtonNode.addTarget(self, action: #selector(self.infoButtonPressed), forControlEvents: .touchUpInside) + } + + func asyncLayout() -> (_ item: ProxySettingsServerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeStatusLayout = TextNode.asyncLayout(self.statusNode) + let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) + let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode) + + let currentItem = self.item + + return { item, params, neighbors in + var updateInfoIconImage: UIImage? + var updateCheckImage: UIImage? + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + updateCheckImage = PresentationResourcesItemList.checkIconImage(item.theme) + updateInfoIconImage = PresentationResourcesCallList.infoButton(item.theme) + } + + let peerRevealOptions: [ItemListRevealOption] + if item.editing.editable { + peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)] + } else { + peerRevealOptions = [] + } + + let titleAttributedString = NSMutableAttributedString() + titleAttributedString.append(NSAttributedString(string: item.server.host, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)) + titleAttributedString.append(NSAttributedString(string: ":\(item.server.port)", font: titleFont, textColor: item.theme.list.itemSecondaryTextColor)) + let statusAttributedString = NSAttributedString(string: item.label, font: statusFont, textColor: item.labelAccent ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor) + + var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? + var reorderControlSizeAndApply: (CGSize, () -> ItemListEditableReorderControlNode)? + + let editingOffset: CGFloat + var reorderInset: CGFloat = 0.0 + + if item.editing.editing { + let sizeAndApply = editableControlLayout(48.0, item.theme, false) + editableControlSizeAndApply = sizeAndApply + editingOffset = sizeAndApply.0.width + + let reorderSizeAndApply = reorderControlLayout(65.0, item.theme) + reorderControlSizeAndApply = reorderSizeAndApply + reorderInset = reorderSizeAndApply.0.width + } else { + editingOffset = 0.0 + } + + let leftInset: CGFloat = 50.0 + params.leftInset + let rightInset: CGFloat = params.rightInset + max(reorderInset, 55.0) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let insets = itemListNeighborsGroupedInsets(neighbors) + let contentSize = CGSize(width: params.width, height: 64.0) + let separatorHeight = UIScreenPixel + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] animated in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + if let updateInfoIconImage = updateInfoIconImage { + strongSelf.infoIconNode.image = updateInfoIconImage + } + if let updateCheckImage = updateCheckImage { + strongSelf.checkNode.image = updateCheckImage + } + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + + strongSelf.activityNode.type = .custom(item.theme.list.itemAccentColor, activitySize.width, 2.0) + } + + let revealOffset = strongSelf.revealOffset + + let transition: ContainedViewLayoutTransition + if animated { + transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + + if let editableControlSizeAndApply = editableControlSizeAndApply { + if strongSelf.editableControlNode == nil { + let editableControlNode = editableControlSizeAndApply.1() + editableControlNode.tapped = { + if let strongSelf = self { + strongSelf.setRevealOptionsOpened(true, animated: true) + strongSelf.revealOptionsInteractivelyOpened() + } + } + strongSelf.editableControlNode = editableControlNode + strongSelf.insertSubnode(editableControlNode, aboveSubnode: strongSelf.titleNode) + let editableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + editableControlNode.frame = editableControlFrame + transition.animatePosition(node: editableControlNode, from: CGPoint(x: -editableControlFrame.size.width / 2.0, y: editableControlFrame.midY)) + editableControlNode.alpha = 0.0 + transition.updateAlpha(node: editableControlNode, alpha: 1.0) + } + strongSelf.editableControlNode?.isHidden = !item.editing.editable + } else if let editableControlNode = strongSelf.editableControlNode { + var editableControlFrame = editableControlNode.frame + editableControlFrame.origin.x = -editableControlFrame.size.width + strongSelf.editableControlNode = nil + transition.updateAlpha(node: editableControlNode, alpha: 0.0) + transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in + editableControlNode?.removeFromSupernode() + }) + } + + if let reorderControlSizeAndApply = reorderControlSizeAndApply { + if strongSelf.reorderControlNode == nil { + let reorderControlNode = reorderControlSizeAndApply.1() + strongSelf.reorderControlNode = reorderControlNode + strongSelf.addSubnode(reorderControlNode) + let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0.width, y: 0.0), size: reorderControlSizeAndApply.0) + reorderControlNode.frame = reorderControlFrame + reorderControlNode.alpha = 0.0 + transition.updateAlpha(node: reorderControlNode, alpha: 1.0) + } + } else if let reorderControlNode = strongSelf.reorderControlNode { + strongSelf.reorderControlNode = nil + transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in + reorderControlNode?.removeFromSupernode() + }) + } + + let _ = titleApply() + let _ = statusApply() + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + editingOffset + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + } + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) + transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) + + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 12.0), size: titleLayout.size)) + transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 37.0), size: statusLayout.size)) + + transition.updateAlpha(node: strongSelf.infoIconNode, alpha: item.editing.editing ? 0.0 : 1.0) + + if let checkImage = strongSelf.checkNode.image { + transition.updateFrame(node: strongSelf.checkNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + floor((50.0 - checkImage.size.width) / 2.0), y: floor((layout.contentSize.height - checkImage.size.height) / 2.0)), size: checkImage.size)) + } + transition.updateFrame(node: strongSelf.activityNode, frame: CGRect(origin: CGPoint(x: params.leftInset + revealOffset + editingOffset + floor((50.0 - activitySize.width) / 2.0), y: floor((layout.contentSize.height - activitySize.height) / 2.0)), size: activitySize)) + strongSelf.checkNode.isHidden = !item.active || item.activity + strongSelf.activityNode.isHidden = !item.activity + + if let infoImage = strongSelf.infoIconNode.image { + transition.updateFrame(node: strongSelf.infoIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 55.0 + floor((55.0 - infoImage.size.width) / 2.0), y: floor((layout.contentSize.height - infoImage.size.height) / 2.0)), size: infoImage.size)) + } + + strongSelf.infoButtonNode.isUserInteractionEnabled = revealOffset.isZero + strongSelf.infoButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 55.0, y: 0.0), size: CGSize(width: 55.0, height: layout.contentSize.height)) + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 64.0 + UIScreenPixel + UIScreenPixel)) + + strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + strongSelf.setRevealOptions(peerRevealOptions) + strongSelf.setRevealOptionsOpened(item.editing.revealed, animated: animated) + } + }) + } + } + + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + super.updateRevealOffset(offset: offset, transition: transition) + + guard let params = self.layoutParams else { + return + } + + let leftInset: CGFloat = 50.0 + params.leftInset + + let editingOffset: CGFloat + if let editableControlNode = self.editableControlNode { + editingOffset = editableControlNode.bounds.size.width + var editableControlFrame = editableControlNode.frame + editableControlFrame.origin.x = params.leftInset + offset + transition.updateFrame(node: editableControlNode, frame: editableControlFrame) + } else { + editingOffset = 0.0 + } + + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size)) + transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + offset + editingOffset, y: self.statusNode.frame.minY), size: self.statusNode.bounds.size)) + + var checkFrame = self.checkNode.frame + checkFrame.origin.x = params.leftInset + offset + editingOffset + floor((50.0 - checkFrame.width) / 2.0) + transition.updateFrame(node: self.checkNode, frame: checkFrame) + + var activityFrame = self.activityNode.frame + activityFrame.origin.x = params.leftInset + offset + editingOffset + floor((50.0 - activityFrame.width) / 2.0) + transition.updateFrame(node: self.activityNode, frame: activityFrame) + + var infoIconFrame = self.infoIconNode.frame + infoIconFrame.origin.x = offset + params.width - params.rightInset - 55.0 + floor((55.0 - infoIconFrame.width) / 2.0) + transition.updateFrame(node: self.infoIconNode, frame: infoIconFrame) + + self.infoButtonNode.isUserInteractionEnabled = offset.isZero + } + + override func revealOptionsInteractivelyOpened() { + if let item = self.item { + item.setServerWithRevealedOptions(item.server, nil) + } + } + + override func revealOptionsInteractivelyClosed() { + if let item = self.item { + item.setServerWithRevealedOptions(nil, item.server) + } + } + + override func revealOptionSelected(_ option: ItemListRevealOption) { + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() + + if let item = self.item { + item.removeServer(item.server) + } + } + + override func isReorderable(at point: CGPoint) -> Bool { + if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point), !self.isDisplayingRevealedOptions { + return true + } + return false + } + + @objc private func infoButtonPressed() { + self.item?.infoAction() + } +} diff --git a/TelegramUI/Resources/PhoneCountries.txt b/TelegramUI/Resources/PhoneCountries.txt index 4f541b8866..e1f76c52bd 100644 --- a/TelegramUI/Resources/PhoneCountries.txt +++ b/TelegramUI/Resources/PhoneCountries.txt @@ -93,7 +93,8 @@ 500;FK;Falkland Islands 423;LI;Liechtenstein 421;SK;Slovakia -420;CZ;Czech Republic +420;CZ;Czech Republic +383;XK;Kosovo 389;MK;Macedonia 387;BA;Bosnia & Herzegovina 386;SI;Slovenia @@ -228,4 +229,4 @@ 1;US;USA 1;PR;Puerto Rico 1;DO;Dominican Rep. -1;CA;Canada \ No newline at end of file +1;CA;Canada diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index bda3d6417d..662992ca39 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -6,6 +6,7 @@ import TelegramCore import LegacyComponents private final class SettingsItemIcons { + static let proxy = UIImage(bundleImageName: "Settings/MenuIcons/Proxy")?.precomposed() static let savedMessages = UIImage(bundleImageName: "Settings/MenuIcons/SavedMessages")?.precomposed() static let recentCalls = UIImage(bundleImageName: "Settings/MenuIcons/RecentCalls")?.precomposed() static let stickers = UIImage(bundleImageName: "Settings/MenuIcons/Stickers")?.precomposed() @@ -29,6 +30,7 @@ private struct SettingsItemArguments { let changeProfilePhoto: () -> Void let openUsername: () -> Void + let openProxy: () -> Void let openSavedMessages: () -> Void let openRecentCalls: () -> Void let openPrivacyAndSecurity: () -> Void @@ -44,6 +46,7 @@ private struct SettingsItemArguments { private enum SettingsSection: Int32 { case info + case proxy case media case generalSettings case help @@ -54,6 +57,8 @@ private enum SettingsEntry: ItemListNodeEntry { case setProfilePhoto(PresentationTheme, String) case setUsername(PresentationTheme, String) + case proxy(PresentationTheme, UIImage?, String, String) + case savedMessages(PresentationTheme, UIImage?, String) case recentCalls(PresentationTheme, UIImage?, String) case stickers(PresentationTheme, UIImage?, String) @@ -71,9 +76,11 @@ private enum SettingsEntry: ItemListNodeEntry { switch self { case .userInfo, .setProfilePhoto, .setUsername: return SettingsSection.info.rawValue + case .proxy: + return SettingsSection.proxy.rawValue case .savedMessages, .recentCalls, .stickers: return SettingsSection.media.rawValue - case .notificationsAndSounds, .privacyAndSecurity, .dataAndStorage, .themes, .language: + case .notificationsAndSounds, .privacyAndSecurity, .dataAndStorage, .themes, .language: return SettingsSection.generalSettings.rawValue case .askAQuestion, .faq: return SettingsSection.help.rawValue @@ -88,26 +95,28 @@ private enum SettingsEntry: ItemListNodeEntry { return 1 case .setUsername: return 2 - case .savedMessages: + case .proxy: return 3 - case .recentCalls: + case .savedMessages: return 4 - case .stickers: + case .recentCalls: return 5 - case .notificationsAndSounds: + case .stickers: return 6 - case .privacyAndSecurity: + case .notificationsAndSounds: return 7 - case .dataAndStorage: + case .privacyAndSecurity: return 8 - case .themes: + case .dataAndStorage: return 9 - case .language: + case .themes: return 10 - case .askAQuestion: + case .language: return 11 - case .faq: + case .askAQuestion: return 12 + case .faq: + return 13 } } @@ -157,6 +166,12 @@ private enum SettingsEntry: ItemListNodeEntry { } else { return false } + case let .proxy(lhsTheme, lhsImage, lhsText, lhsValue): + if case let .proxy(rhsTheme, rhsImage, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } case let .savedMessages(lhsTheme, lhsImage, lhsText): if case let .savedMessages(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true @@ -241,6 +256,10 @@ private enum SettingsEntry: ItemListNodeEntry { return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openUsername() }) + case let .proxy(theme, image, text, value): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + arguments.openProxy() + }) case let .savedMessages(theme, image, text): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openSavedMessages() @@ -304,7 +323,7 @@ private struct SettingsState: Equatable { } } -private func settingsEntries(presentationData: PresentationData, state: SettingsState, view: PeerView) -> [SettingsEntry] { +private func settingsEntries(presentationData: PresentationData, state: SettingsState, view: PeerView, proxySettings: ProxySettings) -> [SettingsEntry] { var entries: [SettingsEntry] = [] if let peer = peerViewMainPeer(view) as? TelegramUser { @@ -317,6 +336,10 @@ private func settingsEntries(presentationData: PresentationData, state: Settings entries.append(.setUsername(presentationData.theme, presentationData.strings.Settings_SetUsername)) } + if !proxySettings.servers.isEmpty { + entries.append(.proxy(presentationData.theme, SettingsItemIcons.proxy, "Proxy", proxySettings.enabled ? "Enabled" : "Disabled")) + } + entries.append(.savedMessages(presentationData.theme, SettingsItemIcons.savedMessages, presentationData.strings.Settings_SavedMessages)) entries.append(.recentCalls(presentationData.theme, SettingsItemIcons.recentCalls, presentationData.strings.CallSettings_RecentCalls)) entries.append(.stickers(presentationData.theme, SettingsItemIcons.stickers, presentationData.strings.ChatSettings_Stickers)) @@ -373,7 +396,7 @@ public func settingsController(account: Account, accountManager: AccountManager) let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } var faqUrl = presentationData.strings.Settings_FAQ_URL if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty { - faqUrl = "http://telegram.org/faq#general" + faqUrl = "https://telegram.org/faq#general" } if let applicationContext = account.applicationContext as? TelegramApplicationContext { @@ -412,6 +435,8 @@ public func settingsController(account: Account, accountManager: AccountManager) changeProfilePhotoImpl?() }, openUsername: { presentControllerImpl?(usernameSetupController(account: account), nil) + }, openProxy: { + pushControllerImpl?(proxySettingsController(account: account)) }, openSavedMessages: { openSavedMessagesImpl?() }, openRecentCalls: { @@ -539,17 +564,24 @@ public func settingsController(account: Account, accountManager: AccountManager) let peerView = account.viewTracker.peerView(account.peerId) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView) - |> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView, account.postbox.preferencesView(keys: [PreferencesKeys.proxySettings])) + |> map { presentationData, state, view, preferences -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in + let proxySettings: ProxySettings + if let value = preferences.values[PreferencesKeys.proxySettings] as? ProxySettings { + proxySettings = value + } else { + proxySettings = ProxySettings.defaultSettings + } + let peer = peerViewMainPeer(view) let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { - if let peer = peer as? TelegramUser, let cachedData = view.cachedData as? CachedUserData { + if let _ = peer as? TelegramUser, let _ = view.cachedData as? CachedUserData { arguments.openEditing() } }) let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Settings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: settingsEntries(presentationData: presentationData, state: state, view: view), style: .blocks) + let listState = ItemListNodeState(entries: settingsEntries(presentationData: presentationData, state: state, view: view, proxySettings: proxySettings), style: .blocks) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/StickerPackPreviewControllerNode.swift b/TelegramUI/StickerPackPreviewControllerNode.swift index 4339e8b70b..0fc14b5751 100644 --- a/TelegramUI/StickerPackPreviewControllerNode.swift +++ b/TelegramUI/StickerPackPreviewControllerNode.swift @@ -191,7 +191,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } })) menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: {})) - return (itemNode, StickerPreviewPeekContent(account: strongSelf.account, item: item, menu: menuItems)) + return (itemNode, StickerPreviewPeekContent(account: strongSelf.account, item: .pack(item), menu: menuItems)) } else { return nil } @@ -210,7 +210,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol return nil }, updateContent: { [weak self] content in if let strongSelf = self { - var item: StickerPackItem? + var item: StickerPreviewPeekItem? if let content = content as? StickerPreviewPeekContent { item = content.item } @@ -522,7 +522,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } } - private func updatePreviewingItem(item: StickerPackItem?, animated: Bool) { + private func updatePreviewingItem(item: StickerPreviewPeekItem?, animated: Bool) { if self.interaction.previewedItem != item { self.interaction.previewedItem = item diff --git a/TelegramUI/StickerPackPreviewGridItem.swift b/TelegramUI/StickerPackPreviewGridItem.swift index 079f22bbcb..c08564306b 100644 --- a/TelegramUI/StickerPackPreviewGridItem.swift +++ b/TelegramUI/StickerPackPreviewGridItem.swift @@ -6,7 +6,7 @@ import AsyncDisplayKit import Postbox final class StickerPackPreviewInteraction { - var previewedItem: StickerPackItem? + var previewedItem: StickerPreviewPeekItem? let sendSticker: (StickerPackItem) -> Void @@ -146,7 +146,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { func updatePreviewing(animated: Bool) { var isPreviewing = false if let (_, item, _) = self.currentState, let interaction = self.interaction { - isPreviewing = interaction.previewedItem == item + isPreviewing = interaction.previewedItem == .pack(item) } if self.currentIsPreviewing != isPreviewing { self.currentIsPreviewing = isPreviewing diff --git a/TelegramUI/StickerPaneSearchBarNode.swift b/TelegramUI/StickerPaneSearchBarNode.swift new file mode 100644 index 0000000000..dcd930b390 --- /dev/null +++ b/TelegramUI/StickerPaneSearchBarNode.swift @@ -0,0 +1,455 @@ +import Foundation +import SwiftSignalKit +import UIKit +import AsyncDisplayKit +import Display + +private func generateLoupeIcon(color: UIColor) -> UIImage? { + return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: color) +} + +private func generateClearIcon(color: UIColor) -> UIImage? { + return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) +} + +private func generateBackground(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + let diameter: CGFloat = 10.0 + return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(foregroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + }, opaque: true)?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) +} + +private class StickerPaneSearchBarTextField: UITextField { + public var didDeleteBackwardWhileEmpty: (() -> Void)? + + let placeholderLabel: ImmediateTextNode + var placeholderString: NSAttributedString? { + didSet { + self.placeholderLabel.attributedText = self.placeholderString + } + } + + let prefixLabel: ASTextNode + var prefixString: NSAttributedString? { + didSet { + self.prefixLabel.attributedText = self.prefixString + } + } + + override init(frame: CGRect) { + self.placeholderLabel = ImmediateTextNode() + self.placeholderLabel.isUserInteractionEnabled = false + self.placeholderLabel.displaysAsynchronously = false + self.placeholderLabel.maximumNumberOfLines = 1 + + self.prefixLabel = ASTextNode() + self.prefixLabel.isLayerBacked = true + self.prefixLabel.displaysAsynchronously = false + + super.init(frame: frame) + + self.addSubnode(self.placeholderLabel) + self.addSubnode(self.prefixLabel) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func textRect(forBounds bounds: CGRect) -> CGRect { + if bounds.size.width.isZero { + return CGRect(origin: CGPoint(), size: CGSize()) + } + var rect = bounds.insetBy(dx: 4.0, dy: 4.0) + + let prefixSize = self.prefixLabel.measure(bounds.size) + if !prefixSize.width.isZero { + let prefixOffset = prefixSize.width + rect.origin.x += prefixOffset + rect.size.width -= prefixOffset + } + return rect + } + + override func editingRect(forBounds bounds: CGRect) -> CGRect { + return self.textRect(forBounds: bounds) + } + + override func layoutSubviews() { + super.layoutSubviews() + + let bounds = self.bounds + if bounds.size.width.isZero { + return + } + + let constrainedSize = self.textRect(forBounds: self.bounds).size + let labelSize = self.placeholderLabel.updateLayout(constrainedSize) + self.placeholderLabel.frame = CGRect(origin: CGPoint(x: self.textRect(forBounds: bounds).minX, y: self.textRect(forBounds: bounds).minY + 4.0), size: labelSize) + + let prefixSize = self.prefixLabel.measure(constrainedSize) + let prefixBounds = bounds.insetBy(dx: 4.0, dy: 4.0) + self.prefixLabel.frame = CGRect(origin: CGPoint(x: prefixBounds.minX, y: prefixBounds.minY + 1.0), size: prefixSize) + } + + override func deleteBackward() { + if self.text == nil || self.text!.isEmpty { + self.didDeleteBackwardWhileEmpty?() + } + super.deleteBackward() + } +} + +class StickerPaneSearchBarNode: ASDisplayNode, UITextFieldDelegate { + var cancel: (() -> Void)? + var textUpdated: ((String) -> Void)? + var clearPrefix: (() -> Void)? + + private let backgroundNode: ASDisplayNode + private let separatorNode: ASDisplayNode + private let textBackgroundNode: ASImageNode + private var activityIndicator: ActivityIndicator? + private let iconNode: ASImageNode + private let textField: StickerPaneSearchBarTextField + private let clearButton: HighlightableButtonNode + private let cancelButton: ASButtonNode + + var placeholderString: NSAttributedString? { + get { + return self.textField.placeholderString + } set(value) { + self.textField.placeholderString = value + } + } + + var prefixString: NSAttributedString? { + get { + return self.textField.prefixString + } set(value) { + let previous = self.prefixString + let updated: Bool + if let previous = previous, let value = value { + updated = !previous.isEqual(to: value) + } else { + updated = (previous != nil) != (value != nil) + } + if updated { + self.textField.prefixString = value + self.textField.setNeedsLayout() + self.updateIsEmpty() + } + } + } + + var text: String { + get { + return self.textField.text ?? "" + } set(value) { + if self.textField.text ?? "" != value { + self.textField.text = value + self.textFieldDidChange(self.textField) + } + } + } + + var activity: Bool = false { + didSet { + if self.activity != oldValue { + if self.activity { + if self.activityIndicator == nil { + let activityIndicator = ActivityIndicator(type: .custom(theme.chat.inputMediaPanel.stickersSearchControlColor, 13.0, 1.0)) + self.activityIndicator = activityIndicator + self.addSubnode(activityIndicator) + if let (boundingSize, leftInset, rightInset) = self.validLayout { + self.updateLayout(boundingSize: boundingSize, leftInset: leftInset, rightInset: rightInset, transition: .immediate) + } + } + } else if let activityIndicator = self.activityIndicator { + self.activityIndicator = nil + activityIndicator.removeFromSupernode() + } + self.iconNode.isHidden = self.activity + } + } + } + + private var validLayout: (CGSize, CGFloat, CGFloat)? + + private var theme: PresentationTheme + private var strings: PresentationStrings + + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.backgroundColor = theme.chat.inputMediaPanel.panelSerapatorColor + + self.textBackgroundNode = ASImageNode() + self.textBackgroundNode.isLayerBacked = false + self.textBackgroundNode.displaysAsynchronously = false + self.textBackgroundNode.displayWithoutProcessing = true + self.textBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 33.0, color: theme.chat.inputMediaPanel.stickersSearchBackgroundColor) + + self.iconNode = ASImageNode() + self.iconNode.isUserInteractionEnabled = false + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.image = generateLoupeIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor) + + self.textField = StickerPaneSearchBarTextField() + self.textField.autocorrectionType = .no + self.textField.returnKeyType = .done + self.textField.font = Font.regular(14.0) + self.textField.textColor = theme.chat.inputMediaPanel.stickersSearchPrimaryColor + + self.clearButton = HighlightableButtonNode() + self.clearButton.imageNode.displaysAsynchronously = false + self.clearButton.imageNode.displayWithoutProcessing = true + self.clearButton.displaysAsynchronously = false + self.clearButton.setImage(generateClearIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor), for: []) + self.clearButton.isHidden = true + + switch theme.chatList.searchBarKeyboardColor { + case .light: + self.textField.keyboardAppearance = .default + case .dark: + self.textField.keyboardAppearance = .dark + } + + self.cancelButton = ASButtonNode() + self.cancelButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) + self.cancelButton.setAttributedTitle(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) + self.cancelButton.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + + self.addSubnode(self.textBackgroundNode) + self.view.addSubview(self.textField) + self.addSubnode(self.iconNode) + self.addSubnode(self.clearButton) + self.addSubnode(self.cancelButton) + + self.textField.delegate = self + self.textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged) + + self.textField.didDeleteBackwardWhileEmpty = { [weak self] in + self?.clearPressed() + } + + self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) + self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) + } + + func updateLayout(boundingSize: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (boundingSize, leftInset, rightInset) + + self.backgroundNode.frame = self.bounds + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel))) + + let verticalOffset: CGFloat = -20.0 + + let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: boundingSize.width - leftInset - rightInset, height: boundingSize.height)) + + let cancelButtonSize = self.cancelButton.measure(CGSize(width: 100.0, height: CGFloat.infinity)) + transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: contentFrame.maxX - 8.0 - cancelButtonSize.width, y: verticalOffset + 34.0), size: cancelButtonSize)) + + let textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + 8.0, y: verticalOffset + 28.0), size: CGSize(width: contentFrame.width - 16.0 - cancelButtonSize.width - 11.0, height: 33.0)) + transition.updateFrame(node: self.textBackgroundNode, frame: textBackgroundFrame) + + let textFrame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 27.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 27.0 - 20.0), height: textBackgroundFrame.size.height)) + + if let iconImage = self.iconNode.image { + let iconSize = iconImage.size + transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 11.0, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - iconSize.height) / 2.0)), size: iconSize)) + } + + if let activityIndicator = self.activityIndicator { + let indicatorSize = activityIndicator.measure(CGSize(width: 32.0, height: 32.0)) + transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 11.0, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - indicatorSize.height) / 2.0)), size: indicatorSize)) + } + + let clearSize = self.clearButton.measure(CGSize(width: 100.0, height: 100.0)) + transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.maxX - 8.0 - clearSize.width, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - clearSize.height) / 2.0)), size: clearSize)) + + self.textField.frame = textFrame + self.textField.layoutSubviews() + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let cancel = self.cancel { + cancel() + } + } + } + + func activate() { + self.textField.becomeFirstResponder() + } + + func animateIn(from node: StickerPaneSearchBarPlaceholderNode, duration: Double, timingFunction: String) { + let initialTextBackgroundFrame = node.convert(node.backgroundNode.frame, to: self) + + let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.size.width, height: max(0.0, initialTextBackgroundFrame.maxY + 8.0))) + if let fromBackgroundColor = node.backgroundColor, let toBackgroundColor = self.backgroundNode.backgroundColor { + self.backgroundNode.layer.animate(from: fromBackgroundColor.cgColor, to: toBackgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: duration * 0.7) + } else { + self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + } + self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: timingFunction) + + let initialSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, initialTextBackgroundFrame.maxY + 8.0)), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel)) + self.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.separatorNode.layer.animateFrame(from: initialSeparatorFrame, to: self.separatorNode.frame, duration: duration, timingFunction: timingFunction) + + self.textBackgroundNode.layer.animateFrame(from: initialTextBackgroundFrame, to: self.textBackgroundNode.frame, duration: duration, timingFunction: timingFunction) + + let labelFrame = self.textField.placeholderLabel.frame + let initialLabelNodeFrame = CGRect(origin: node.labelNode.view.convert(node.labelNode.bounds, to: self.textField.superview).origin, size: labelFrame.size) + self.textField.layer.animateFrame(from: CGRect(origin: initialLabelNodeFrame.origin.offsetBy(dx: -labelFrame.minX, dy: -labelFrame.minY), size: self.textField.frame.size), to: self.textField.frame, duration: duration, timingFunction: timingFunction) + + let iconFrame = self.iconNode.frame + let initialIconFrame = CGRect(origin: node.iconNode.view.convert(node.iconNode.bounds, to: self.iconNode.view.superview).origin, size: iconFrame.size) + self.iconNode.layer.animateFrame(from: initialIconFrame, to: self.iconNode.frame, duration: duration, timingFunction: timingFunction) + + let cancelButtonFrame = self.cancelButton.frame + self.cancelButton.layer.animatePosition(from: CGPoint(x: self.bounds.size.width + cancelButtonFrame.size.width / 2.0, y: initialTextBackgroundFrame.minY + 2.0 + cancelButtonFrame.size.height / 2.0), to: self.cancelButton.layer.position, duration: duration, timingFunction: timingFunction) + node.isHidden = true + } + + func deactivate(clear: Bool = true) { + self.textField.resignFirstResponder() + if clear { + self.textField.text = nil + self.textField.placeholderLabel.isHidden = false + } + } + + func transitionOut(to node: StickerPaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + let targetTextBackgroundFrame = node.convert(node.backgroundNode.frame, to: self) + + let duration: Double = 0.5 + let timingFunction = kCAMediaTimingFunctionSpring + + node.isHidden = true + self.clearButton.isHidden = true + self.textField.text = "" + + var backgroundCompleted = false + var separatorCompleted = false + var textBackgroundCompleted = false + let intermediateCompletion: () -> Void = { [weak node] in + if backgroundCompleted && separatorCompleted && textBackgroundCompleted { + completion() + node?.isHidden = false + } + } + + let targetBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.size.width, height: max(0.0, targetTextBackgroundFrame.maxY + 8.0))) + if let toBackgroundColor = node.backgroundColor, let fromBackgroundColor = self.backgroundNode.backgroundColor { + self.backgroundNode.layer.animate(from: fromBackgroundColor.cgColor, to: toBackgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: duration * 0.5, removeOnCompletion: false) + } else { + self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration / 2.0, removeOnCompletion: false) + } + self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: targetBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in + backgroundCompleted = true + intermediateCompletion() + }) + + let targetSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, targetTextBackgroundFrame.maxY + 8.0)), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel)) + self.separatorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration / 2.0, removeOnCompletion: false) + self.separatorNode.layer.animateFrame(from: self.separatorNode.frame, to: targetSeparatorFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in + separatorCompleted = true + intermediateCompletion() + }) + + self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in + textBackgroundCompleted = true + intermediateCompletion() + }) + + let transitionBackgroundNode = ASImageNode() + transitionBackgroundNode.isLayerBacked = true + transitionBackgroundNode.displaysAsynchronously = false + transitionBackgroundNode.displayWithoutProcessing = true + transitionBackgroundNode.image = node.backgroundNode.image + self.insertSubnode(transitionBackgroundNode, aboveSubnode: self.textBackgroundNode) + transitionBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0, removeOnCompletion: false) + transitionBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + + let textFieldFrame = self.textField.frame + let targetLabelNodeFrame = CGRect(origin: node.labelNode.view.convert(node.labelNode.bounds, to: self.textField.superview).origin, size: textFieldFrame.size) + self.textField.layer.animateFrame(from: self.textField.frame, to: CGRect(origin: targetLabelNodeFrame.origin.offsetBy(dx: -self.textField.placeholderLabel.frame.minX, dy: -self.textField.placeholderLabel.frame.minY), size: self.textField.frame.size), duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + if let snapshot = node.labelNode.layer.snapshotContentTree() { + snapshot.frame = CGRect(origin: self.textField.placeholderLabel.frame.origin, size: node.labelNode.frame.size) + self.textField.layer.addSublayer(snapshot) + snapshot.animateAlpha(from: 0.0, to: 1.0, duration: duration * 2.0 / 3.0, timingFunction: kCAMediaTimingFunctionLinear) + self.textField.placeholderLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 3.0 / 2.0, timingFunction: kCAMediaTimingFunctionLinear, removeOnCompletion: false) + + } + + let iconFrame = self.iconNode.frame + let targetIconFrame = CGRect(origin: node.iconNode.view.convert(node.iconNode.bounds, to: self.iconNode.view.superview).origin, size: iconFrame.size) + self.iconNode.image = node.iconNode.image + self.iconNode.layer.animateFrame(from: self.iconNode.frame, to: targetIconFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + + let cancelButtonFrame = self.cancelButton.frame + self.cancelButton.layer.animatePosition(from: self.cancelButton.layer.position, to: CGPoint(x: self.bounds.size.width + cancelButtonFrame.size.width / 2.0, y: targetTextBackgroundFrame.minY + 2.0 + cancelButtonFrame.size.height / 2.0), duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if string.range(of: "\n") != nil { + return false + } + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.textField.resignFirstResponder() + return false + } + + @objc func textFieldDidChange(_ textField: UITextField) { + self.updateIsEmpty() + if let textUpdated = self.textUpdated { + textUpdated(textField.text ?? "") + } + } + + private func updateIsEmpty() { + let isEmpty = !(textField.text?.isEmpty ?? true) + if isEmpty != self.textField.placeholderLabel.isHidden { + self.textField.placeholderLabel.isHidden = isEmpty + } + self.clearButton.isHidden = !isEmpty && self.prefixString == nil + } + + @objc func cancelPressed() { + if let cancel = self.cancel { + cancel() + } + } + + @objc func clearPressed() { + if (self.textField.text?.isEmpty ?? true) { + if self.prefixString != nil { + self.clearPrefix?() + } + } else { + self.textField.text = "" + self.textFieldDidChange(self.textField) + } + } +} diff --git a/TelegramUI/StickerPaneSearchBarPlaceholderItem.swift b/TelegramUI/StickerPaneSearchBarPlaceholderItem.swift new file mode 100644 index 0000000000..5e0bd0fa9e --- /dev/null +++ b/TelegramUI/StickerPaneSearchBarPlaceholderItem.swift @@ -0,0 +1,109 @@ +import Foundation +import AsyncDisplayKit +import UIKit +import Display + +private let templateLoupeIcon = UIImage(bundleImageName: "Components/Search Bar/Loupe") + +private func generateLoupeIcon(color: UIColor) -> UIImage? { + return generateTintedImage(image: templateLoupeIcon, color: color) +} + +final class StickerPaneSearchBarPlaceholderItem: GridItem { + let theme: PresentationTheme + let strings: PresentationStrings + let activate: () -> Void + + let section: GridSection? = nil + let fillsRowWithHeight: CGFloat? = 56.0 + + init(theme: PresentationTheme, strings: PresentationStrings, activate: @escaping () -> Void) { + self.theme = theme + self.strings = strings + self.activate = activate + } + + func node(layout: GridNodeLayout) -> GridItemNode { + let node = StickerPaneSearchBarPlaceholderNode() + node.activate = self.activate + node.setup(theme: self.theme, strings: self.strings) + return node + } + + func update(node: GridItemNode) { + guard let node = node as? StickerPaneSearchBarPlaceholderNode else { + assertionFailure() + return + } + node.activate = self.activate + node.setup(theme: self.theme, strings: self.strings) + } +} + +final class StickerPaneSearchBarPlaceholderNode: GridItemNode { + private var currentState: (PresentationTheme, PresentationStrings)? + var activate: (() -> Void)? + + let backgroundNode: ASImageNode + let labelNode: ImmediateTextNode + let iconNode: ASImageNode + + override init() { + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.isUserInteractionEnabled = false + + self.labelNode = ImmediateTextNode() + self.labelNode.displaysAsynchronously = false + self.labelNode.isUserInteractionEnabled = false + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.labelNode) + self.addSubnode(self.iconNode) + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + func setup(theme: PresentationTheme, strings: PresentationStrings) { + if self.currentState?.0 !== theme || self.currentState?.1 !== strings { + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 33.0, color: theme.chat.inputMediaPanel.stickersSearchBackgroundColor) + self.iconNode.image = generateLoupeIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor) + self.labelNode.attributedText = NSAttributedString(string: strings.Stickers_Search, font: Font.regular(14.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor) + } + } + + override func layout() { + super.layout() + + let bounds = self.bounds + + let backgroundFrame = CGRect(origin: CGPoint(x: 8.0, y: 12.0), size: CGSize(width: bounds.width - 8.0 * 2.0, height: 33.0)) + self.backgroundNode.frame = backgroundFrame + + let textSize = self.labelNode.updateLayout(bounds.size) + let textFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width - textSize.width) / 2.0), y: backgroundFrame.minY + floor((backgroundFrame.height - textSize.height) / 2.0)), size: textSize) + self.labelNode.frame = textFrame + + if let iconImage = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - iconImage.size.width - 5.0, y: floorToScreenPixels(textFrame.midY - iconImage.size.height / 2.0)), size: iconImage.size) + } + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.activate?() + } + } +} diff --git a/TelegramUI/StickerPaneSearchContainerNode.swift b/TelegramUI/StickerPaneSearchContainerNode.swift new file mode 100644 index 0000000000..fd8c363580 --- /dev/null +++ b/TelegramUI/StickerPaneSearchContainerNode.swift @@ -0,0 +1,413 @@ +import Foundation +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +import LegacyComponents +import TelegramUIPrivateModule + +final class StickerPaneSearchInteraction { + let open: (StickerPackCollectionInfo) -> Void + let install: (StickerPackCollectionInfo) -> Void + let sendSticker: (TelegramMediaFile) -> Void + + init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void) { + self.open = open + self.install = install + self.sendSticker = sendSticker + } +} + +private enum StickerSearchEntryId: Equatable, Hashable { + case sticker(String, Int64) + case global(ItemCollectionId) +} + +private enum StickerSearchEntry: Identifiable, Comparable { + case sticker(index: Int, code: String, stickerItem: FoundStickerItem, theme: PresentationTheme) + case global(index: Int, info: StickerPackCollectionInfo, topItems: [StickerPackItem], installed: Bool) + + var stableId: StickerSearchEntryId { + switch self { + case let .sticker(index, code, stickerItem, _): + return .sticker(code, stickerItem.file.fileId.id) + case let .global(_, info, _, _): + return .global(info.id) + } + } + + static func ==(lhs: StickerSearchEntry, rhs: StickerSearchEntry) -> Bool { + switch lhs { + case let .sticker(lhsIndex, lhsCode, lhsStickerItem, lhsTheme): + if case let .sticker(rhsIndex, rhsCode, rhsStickerItem, rhsTheme) = rhs { + if lhsIndex != rhsIndex { + return false + } + if lhsCode != rhsCode { + return false + } + if lhsStickerItem != rhsStickerItem { + return false + } + if lhsTheme !== rhsTheme { + return false + } + return true + } else { + return false + } + case let .global(index, info, topItems, installed): + if case .global(index, info, topItems, installed) = rhs { + return true + } else { + return false + } + } + } + + static func <(lhs: StickerSearchEntry, rhs: StickerSearchEntry) -> Bool { + switch lhs { + case let .sticker(lhsIndex, _, _, _): + switch rhs { + case let .sticker(rhsIndex, _, _, _): + return lhsIndex < rhsIndex + default: + return true + } + case let .global(lhsIndex, _, _, _): + switch rhs { + case .sticker: + return false + case let .global(rhsIndex, _, _, _): + return lhsIndex < rhsIndex + } + } + } + + func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: StickerPaneSearchInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> GridItem { + switch self { + case let .sticker(_, code, stickerItem, theme): + return StickerPaneSearchStickerItem(account: account, code: code, stickerItem: stickerItem, inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { + interaction.sendSticker(stickerItem.file) + }) + case let .global(_, info, topItems, installed): + return StickerPaneSearchGlobalItem(account: account, theme: theme, strings: strings, info: info, topItems: topItems, installed: installed, unread: false, open: { + interaction.open(info) + }, install: { + interaction.install(info) + }) + } + } +} + +private struct StickerPaneSearchGridTransition { + let deletions: [Int] + let insertions: [GridNodeInsertItem] + let updates: [GridNodeUpdateItem] + let updateFirstIndexInSectionOffset: Int? + let stationaryItems: GridNodeStationaryItems + let scrollToItem: GridNodeScrollToItem? + let animated: Bool +} + +private func preparedChatMediaInputGridEntryTransition(account: Account, theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [StickerSearchEntry], to toEntries: [StickerSearchEntry], interaction: StickerPaneSearchInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> StickerPaneSearchGridTransition { + let stationaryItems: GridNodeStationaryItems = .none + let scrollToItem: GridNodeScrollToItem? = nil + var animated = false + animated = true + + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices + let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction), previousIndex: $0.2) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction)) } + + let firstIndexInSectionOffset = 0 + + return StickerPaneSearchGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, animated: animated) +} + +final class StickerPaneSearchContainerNode: ASDisplayNode { + private let account: Account + private let theme: PresentationTheme + private let strings: PresentationStrings + private let controllerInteraction: ChatControllerInteraction + private let inputNodeInteraction: ChatMediaInputNodeInteraction + + private let backgroundNode: ASDisplayNode + private let searchBar: StickerPaneSearchBarNode + private let trendingPane: ChatMediaInputTrendingPane + private let gridNode: GridNode + + private var validLayout: CGSize? + + private var enqueuedTransitions: [StickerPaneSearchGridTransition] = [] + + private let searchDisposable = MetaDisposable() + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, cancel: @escaping () -> Void) { + self.account = account + self.theme = theme + self.strings = strings + self.controllerInteraction = controllerInteraction + self.inputNodeInteraction = inputNodeInteraction + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor + + self.trendingPane = ChatMediaInputTrendingPane(account: account, controllerInteraction: controllerInteraction) + + self.searchBar = StickerPaneSearchBarNode(theme: theme, strings: strings) + + self.gridNode = GridNode() + + self.gridNode.isHidden = true + self.trendingPane.isHidden = false + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.trendingPane) + self.addSubnode(self.gridNode) + self.addSubnode(self.searchBar) + + self.gridNode.scrollView.alwaysBounceVertical = true + self.gridNode.scrollingInitiated = { [weak self] in + self?.searchBar.deactivate(clear: false) + } + + self.searchBar.placeholderString = NSAttributedString(string: strings.Stickers_Search, font: Font.regular(14.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor) + self.searchBar.cancel = { + cancel() + } + self.searchBar.activate() + + let interaction = StickerPaneSearchInteraction(open: { [weak self] info in + if let strongSelf = self { + strongSelf.controllerInteraction.presentController(StickerPackPreviewController(account: strongSelf.account, stickerPack: .id(id: info.id.id, accessHash: info.accessHash)), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + }, install: { [weak self] info in + if let strongSelf = self { + let _ = (loadedStickerPack(postbox: strongSelf.account.postbox, network: strongSelf.account.network, reference: .id(id: info.id.id, accessHash: info.accessHash)) + |> mapToSignal { result -> Signal in + switch result { + case let .result(info, items, installed): + if installed { + return .complete() + } else { + return addStickerPackInteractively(postbox: strongSelf.account.postbox, info: info, items: items) + } + case .fetching: + break + case .none: + break + } + return .complete() + }).start() + } + }, sendSticker: { [weak self] file in + if let strongSelf = self { + strongSelf.controllerInteraction.sendSticker(file) + } + }) + + let queue = Queue() + let currentEntries = Atomic<[StickerSearchEntry]?>(value: nil) + + self.searchBar.textUpdated = { [weak self] text in + guard let strongSelf = self else { + return + } + + let signal: Signal<([(String, FoundStickerItem)], FoundStickerSets, Bool)?, NoError> + if !text.isEmpty { + let stickers: Signal<[(String, FoundStickerItem)], NoError> = Signal { subscriber in + var signals: [Signal<(String, [FoundStickerItem]), NoError>] = [] + for entry in TGEmojiSuggestions.suggestions(forQuery: text.lowercased()) { + if let entry = entry as? TGAlphacodeEntry { + signals.append(searchStickers(account: account, query: entry.emoji) + |> take(1) + |> map { (entry.emoji, $0) }) + } + } + + return combineLatest(signals).start(next: { results in + var result: [(String, FoundStickerItem)] = [] + for (emoji, stickers) in results { + for sticker in stickers { + result.append((emoji, sticker)) + } + } + subscriber.putNext(result) + }, completed: { + subscriber.putCompletion() + }) + } + + let local = searchStickerSets(postbox: account.postbox, query: text) + let remote = searchStickerSetsRemotely(network: account.network, query: text) + let packs = local + |> mapToSignal { result -> Signal<(FoundStickerSets, Bool), NoError> in + return .single((result, false)) + |> then(remote |> map { remote -> (FoundStickerSets, Bool) in + return (result.merge(with: remote), true) + }) + } + signal = combineLatest(stickers, packs) + |> map { stickers, packs -> ([(String, FoundStickerItem)], FoundStickerSets, Bool)? in + return (stickers, packs.0, packs.1) + } + strongSelf.searchBar.activity = true + } else { + signal = .single(nil) + strongSelf.searchBar.activity = false + } + + strongSelf.searchDisposable.set((signal + |> deliverOn(queue)).start(next: { result in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + + var entries: [StickerSearchEntry] = [] + if let (stickers, packs, final) = result { + strongSelf.gridNode.isHidden = false + strongSelf.trendingPane.isHidden = true + + if final { + strongSelf.searchBar.activity = false + } + + var index = 0 + for (code, sticker) in stickers { + entries.append(StickerSearchEntry.sticker(index: index, code: code, stickerItem: sticker, theme: theme)) + index += 1 + } + for (collectionId, info, _, installed) in packs.infos { + if let info = info as? StickerPackCollectionInfo { + var topItems: [StickerPackItem] = [] + for e in packs.entries { + if let item = e.item as? StickerPackItem { + if e.index.collectionId == collectionId { + topItems.append(item) + } + } + } + entries.append(.global(index: index, info: info, topItems: topItems, installed: installed)) + index += 1 + } + } + } else { + strongSelf.searchBar.activity = false + strongSelf.gridNode.isHidden = true + strongSelf.trendingPane.isHidden = false + } + + let previousEntries = currentEntries.swap(entries) + let transition = preparedChatMediaInputGridEntryTransition(account: account, theme: theme, strings: strings, from: previousEntries ?? [], to: entries, interaction: interaction, inputNodeInteraction: strongSelf.inputNodeInteraction) + strongSelf.enqueueTransition(transition) + } + })) + } + } + + deinit { + self.searchDisposable.dispose() + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + let firstLayout = self.validLayout == nil + self.validLayout = size + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + + let searchBarHeight: CGFloat = 48.0 + transition.updateFrame(node: self.searchBar, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: searchBarHeight))) + self.searchBar.updateLayout(boundingSize: CGSize(width: size.width, height: searchBarHeight), leftInset: 0.0, rightInset: 0.0, transition: transition) + + let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: searchBarHeight), size: CGSize(width: size.width, height: size.height - searchBarHeight)) + + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: contentFrame.size, insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), lineSpacing: 0.0)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + + transition.updateFrame(node: self.trendingPane, frame: contentFrame) + self.trendingPane.updateLayout(size: contentFrame.size, topInset: 0.0, bottomInset: 0.0, transition: transition) + + transition.updateFrame(node: self.gridNode, frame: contentFrame) + if firstLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + func deactivate() { + self.searchBar.deactivate(clear: true) + } + + func animateIn(from placeholder: StickerPaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition) { + self.backgroundNode.alpha = 0.0 + transition.updateAlpha(node: self.backgroundNode, alpha: 1.0, completion: { _ in + }) + self.gridNode.alpha = 0.0 + transition.updateAlpha(node: self.gridNode, alpha: 1.0, completion: { _ in + }) + self.trendingPane.alpha = 0.0 + transition.updateAlpha(node: self.trendingPane, alpha: 1.0, completion: { _ in + }) + switch transition { + case let .animated(duration, curve): + self.searchBar.animateIn(from: placeholder, duration: duration, timingFunction: curve.timingFunction) + case .immediate: + break + } + } + + func animateOut(to placeholder: StickerPaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + self.searchBar.transitionOut(to: placeholder, transition: transition, completion: { + completion() + }) + transition.updateAlpha(node: self.backgroundNode, alpha: 0.0, completion: { _ in + }) + transition.updateAlpha(node: self.gridNode, alpha: 0.0, completion: { _ in + }) + transition.updateAlpha(node: self.trendingPane, alpha: 0.0, completion: { _ in + }) + self.deactivate() + } + + private func enqueueTransition(_ transition: StickerPaneSearchGridTransition) { + enqueuedTransitions.append(transition) + + if self.validLayout != nil { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let transition = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + let itemTransition: ContainedViewLayoutTransition = .immediate + self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in }) + } + } + + func updatePreviewing(animated: Bool) { + self.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? StickerPaneSearchStickerItemNode { + itemNode.updatePreviewing(animated: animated) + } + } + } + + func itemAt(point: CGPoint) -> (ASDisplayNode, FoundStickerItem)? { + if let itemNode = self.gridNode.itemNodeAtPoint(self.view.convert(point, to: self.gridNode.view)) as? StickerPaneSearchStickerItemNode, let stickerItem = itemNode.stickerItem { + return (itemNode, stickerItem) + } + return nil + } +} diff --git a/TelegramUI/StickerPaneSearchGlobaltem.swift b/TelegramUI/StickerPaneSearchGlobaltem.swift new file mode 100644 index 0000000000..6bfeab5a73 --- /dev/null +++ b/TelegramUI/StickerPaneSearchGlobaltem.swift @@ -0,0 +1,272 @@ +import Foundation +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import Postbox + +final class StickerPaneSearchGlobalItem: GridItem { + let account: Account + let theme: PresentationTheme + let strings: PresentationStrings + let info: StickerPackCollectionInfo + let topItems: [StickerPackItem] + let installed: Bool + let unread: Bool + let open: () -> Void + let install: () -> Void + + let section: GridSection? = nil + let fillsRowWithHeight: CGFloat? = 128.0 + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, info: StickerPackCollectionInfo, topItems: [StickerPackItem], installed: Bool, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void) { + self.account = account + self.theme = theme + self.strings = strings + self.info = info + self.topItems = topItems + self.installed = installed + self.unread = unread + self.open = open + self.install = install + } + + func node(layout: GridNodeLayout) -> GridItemNode { + let node = StickerPaneSearchGlobalItemNode() + node.setup(item: self) + return node + } + + func update(node: GridItemNode) { + guard let node = node as? StickerPaneSearchGlobalItemNode else { + assertionFailure() + return + } + node.setup(item: self) + } +} + +private let titleFont = Font.bold(16.0) +private let statusFont = Font.regular(15.0) +private let buttonFont = Font.medium(13.0) + +private final class TrendingTopItemNode: TransformImageNode { + var file: TelegramMediaFile? = nil + let loadDisposable = MetaDisposable() +} + +class StickerPaneSearchGlobalItemNode: GridItemNode { + private let titleNode: TextNode + private let descriptionNode: TextNode + private let unreadNode: ASImageNode + private let installTextNode: TextNode + private let installBackgroundNode: ASImageNode + private let installButtonNode: HighlightTrackingButtonNode + private var itemNodes: [TrendingTopItemNode] + + private var item: StickerPaneSearchGlobalItem? + private var appliedItem: StickerPaneSearchGlobalItem? + private let preloadDisposable = MetaDisposable() + + override init() { + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.descriptionNode = TextNode() + self.descriptionNode.isLayerBacked = true + self.descriptionNode.contentMode = .left + self.descriptionNode.contentsScale = UIScreen.main.scale + + self.unreadNode = ASImageNode() + self.unreadNode.isLayerBacked = true + self.unreadNode.displayWithoutProcessing = true + self.unreadNode.displaysAsynchronously = false + + self.installTextNode = TextNode() + self.installTextNode.isLayerBacked = true + self.installTextNode.contentMode = .left + self.installTextNode.contentsScale = UIScreen.main.scale + + self.installBackgroundNode = ASImageNode() + self.installBackgroundNode.isLayerBacked = true + self.installBackgroundNode.displayWithoutProcessing = true + self.installBackgroundNode.displaysAsynchronously = false + + self.installButtonNode = HighlightTrackingButtonNode() + + self.itemNodes = [] + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.descriptionNode) + self.addSubnode(self.unreadNode) + self.addSubnode(self.installBackgroundNode) + self.addSubnode(self.installTextNode) + self.addSubnode(self.installButtonNode) + + self.installButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.installBackgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.installBackgroundNode.alpha = 0.4 + strongSelf.installTextNode.layer.removeAnimation(forKey: "opacity") + strongSelf.installTextNode.alpha = 0.4 + } else { + strongSelf.installBackgroundNode.alpha = 1.0 + strongSelf.installBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.installTextNode.alpha = 1.0 + strongSelf.installTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.installButtonNode.addTarget(self, action: #selector(self.installPressed), forControlEvents: .touchUpInside) + } + + deinit { + self.preloadDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + func setup(item: StickerPaneSearchGlobalItem) { + self.item = item + self.setNeedsLayout() + } + + override func layout() { + super.layout() + guard let item = self.item else { + return + } + + let params = ListViewItemLayoutParams(width: self.bounds.size.width, leftInset: 0.0, rightInset: 0.0) + + let makeInstallLayout = TextNode.asyncLayout(self.installTextNode) + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode) + + let currentItem = self.appliedItem + self.appliedItem = item + + var updateButtonBackgroundImage: UIImage? + if currentItem?.theme !== item.theme { + updateButtonBackgroundImage = PresentationResourcesChat.chatInputMediaPanelAddPackButtonImage(item.theme) + } + let unreadImage = PresentationResourcesItemList.stickerUnreadDotImage(item.theme) + + let leftInset: CGFloat = 14.0 + let rightInset: CGFloat = 16.0 + let topOffset: CGFloat = 12.0 + + let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_Install, font: buttonFont, textColor: item.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.info.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0 - installLayout.size.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.StickerPack_StickerCount(item.info.count), font: statusFont, textColor: item.theme.chat.inputMediaPanel.stickersSectionTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + var topItems = item.topItems + if topItems.count > 5 { + topItems.removeSubrange(5 ..< topItems.count) + } + + let strongSelf = self + if item.topItems.count < Int(item.info.count) && item.topItems.count < 5 && strongSelf.item?.info.id != item.info.id { + strongSelf.preloadDisposable.set(preloadedFeaturedStickerSet(network: item.account.network, postbox: item.account.postbox, id: item.info.id).start()) + } + strongSelf.item = item + + let _ = installApply() + let _ = titleApply() + let _ = descriptionApply() + + if let updateButtonBackgroundImage = updateButtonBackgroundImage { + strongSelf.installBackgroundNode.image = updateButtonBackgroundImage + } + + let installWidth: CGFloat = installLayout.size.width + 20.0 + let buttonFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - installWidth, y: 4.0 + topOffset), size: CGSize(width: installWidth, height: 26.0)) + strongSelf.installBackgroundNode.frame = buttonFrame + strongSelf.installTextNode.frame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - installLayout.size.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - installLayout.size.height) / 2.0) + 1.0), size: installLayout.size) + strongSelf.installButtonNode.frame = buttonFrame + + if item.installed { + strongSelf.installButtonNode.isHidden = true + strongSelf.installBackgroundNode.isHidden = true + strongSelf.installTextNode.isHidden = true + } else { + strongSelf.installButtonNode.isHidden = false + strongSelf.installBackgroundNode.isHidden = false + strongSelf.installTextNode.isHidden = false + } + + let titleFrame = CGRect(origin: CGPoint(x: params.leftInset + leftInset, y: 2.0 + topOffset), size: titleLayout.size) + strongSelf.titleNode.frame = titleFrame + strongSelf.descriptionNode.frame = CGRect(origin: CGPoint(x: params.leftInset + leftInset, y: 23.0 + topOffset), size: descriptionLayout.size) + + if false && item.unread { + strongSelf.unreadNode.isHidden = false + } else { + strongSelf.unreadNode.isHidden = true + } + if let image = unreadImage { + strongSelf.unreadNode.image = image + strongSelf.unreadNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: titleFrame.minY + 7.0), size: image.size) + } + + var offset: CGFloat = params.leftInset + leftInset + let itemSize = CGSize(width: 68.0, height: 68.0) + + for i in 0 ..< topItems.count { + let file = topItems[i].file + let node: TrendingTopItemNode + if i < strongSelf.itemNodes.count { + node = strongSelf.itemNodes[i] + } else { + node = TrendingTopItemNode() + node.contentAnimations = [.subsequentUpdates] + strongSelf.itemNodes.append(node) + strongSelf.addSubnode(node) + } + if file.fileId != node.file?.fileId { + node.file = file + node.setSignal(chatMessageSticker(account: item.account, file: file, small: true)) + node.loadDisposable.set(freeMediaFileInteractiveFetched(account: item.account, file: file).start()) + } + if let dimensions = file.dimensions { + let imageSize = dimensions.aspectFitted(itemSize) + node.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() + node.frame = CGRect(origin: CGPoint(x: offset, y: 48.0 + topOffset), size: imageSize) + offset += imageSize.width + 4.0 + } + } + + if topItems.count < strongSelf.itemNodes.count { + for i in (topItems.count ..< strongSelf.itemNodes.count).reversed() { + strongSelf.itemNodes[i].removeFromSupernode() + strongSelf.itemNodes.remove(at: i) + } + } + } + + @objc func installPressed() { + if let item = self.item { + item.install() + } + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let item = self.item { + item.open() + } + } + } +} diff --git a/TelegramUI/StickerPaneSearchStickerItem.swift b/TelegramUI/StickerPaneSearchStickerItem.swift new file mode 100644 index 0000000000..54362abda1 --- /dev/null +++ b/TelegramUI/StickerPaneSearchStickerItem.swift @@ -0,0 +1,186 @@ +import Foundation +import Display +import TelegramCore +import SwiftSignalKit +import AsyncDisplayKit +import Postbox + +final class StickerPaneSearchStickerSection: GridSection { + let code: String + let theme: PresentationTheme + let height: CGFloat = 26.0 + + var hashValue: Int { + return self.code.hashValue + } + + init(code: String, theme: PresentationTheme) { + self.code = code + self.theme = theme + } + + func isEqual(to: GridSection) -> Bool { + if let to = to as? StickerPaneSearchStickerSection { + return self.code == to.code && self.theme === to.theme + } else { + return false + } + } + + func node() -> ASDisplayNode { + return StickerPaneSearchStickerSectionNode(code: self.code, theme: self.theme) + } +} + +private let sectionTitleFont = Font.medium(12.0) + +final class StickerPaneSearchStickerSectionNode: ASDisplayNode { + let titleNode: ASTextNode + + init(code: String, theme: PresentationTheme) { + self.titleNode = ASTextNode() + self.titleNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.titleNode) + self.titleNode.attributedText = NSAttributedString(string: code, font: sectionTitleFont, textColor: theme.chat.inputMediaPanel.stickersSectionTextColor) + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.truncationMode = .byTruncatingTail + } + + override func layout() { + super.layout() + + let bounds = self.bounds + + let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude)) + self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 9.0), size: titleSize) + } +} + +final class StickerPaneSearchStickerItem: GridItem { + let account: Account + let code: String + let stickerItem: FoundStickerItem + let selected: () -> Void + let inputNodeInteraction: ChatMediaInputNodeInteraction + + let section: GridSection? + + init(account: Account, code: String, stickerItem: FoundStickerItem, inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping () -> Void) { + self.account = account + self.code = code + self.stickerItem = stickerItem + self.inputNodeInteraction = inputNodeInteraction + self.selected = selected + self.section = StickerPaneSearchStickerSection(code: self.code, theme: theme) + } + + func node(layout: GridNodeLayout) -> GridItemNode { + let node = StickerPaneSearchStickerItemNode() + node.inputNodeInteraction = self.inputNodeInteraction + node.setup(account: self.account, stickerItem: self.stickerItem) + node.selected = self.selected + return node + } + + func update(node: GridItemNode) { + guard let node = node as? StickerPaneSearchStickerItemNode else { + assertionFailure() + return + } + node.inputNodeInteraction = self.inputNodeInteraction + node.setup(account: self.account, stickerItem: self.stickerItem) + node.selected = self.selected + } +} + +final class StickerPaneSearchStickerItemNode: GridItemNode { + private var currentState: (Account, FoundStickerItem, CGSize)? + private let imageNode: TransformImageNode + + private let stickerFetchedDisposable = MetaDisposable() + + var currentIsPreviewing = false + + var inputNodeInteraction: ChatMediaInputNodeInteraction? + var selected: (() -> Void)? + + var stickerItem: FoundStickerItem? { + return self.currentState?.1 + } + + override init() { + self.imageNode = TransformImageNode() + + super.init() + + self.addSubnode(self.imageNode) + } + + deinit { + stickerFetchedDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) + } + + func setup(account: Account, stickerItem: FoundStickerItem) { + if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem { + if let dimensions = stickerItem.file.dimensions { + self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) + self.stickerFetchedDisposable.set(freeMediaFileInteractiveFetched(account: account, file: stickerItem.file).start()) + + self.currentState = (account, stickerItem, dimensions) + self.setNeedsLayout() + } + } + } + + override func layout() { + super.layout() + + let bounds = self.bounds + let boundingSize = bounds.insetBy(dx: 6.0, dy: 6.0).size + + if let (_, _, mediaDimensions) = self.currentState { + let imageSize = mediaDimensions.aspectFitted(boundingSize) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) + } + } + + @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { + self.selected?() + } + + func transitionNode() -> ASDisplayNode? { + return self.imageNode + } + + func updatePreviewing(animated: Bool) { + var isPreviewing = false + if let (_, item, _) = self.currentState, let interaction = self.inputNodeInteraction { + isPreviewing = interaction.previewedStickerPackItem == .found(item) + } + if self.currentIsPreviewing != isPreviewing { + self.currentIsPreviewing = isPreviewing + + if isPreviewing { + self.layer.sublayerTransform = CATransform3DMakeScale(0.8, 0.8, 1.0) + if animated { + self.layer.animateSpring(from: 1.0 as NSNumber, to: 0.8 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.4) + } + } else { + self.layer.sublayerTransform = CATransform3DIdentity + if animated { + self.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "sublayerTransform.scale", duration: 0.5) + } + } + } + } +} diff --git a/TelegramUI/StickerPreviewPeekContent.swift b/TelegramUI/StickerPreviewPeekContent.swift index 7846414e94..aa804e4927 100644 --- a/TelegramUI/StickerPreviewPeekContent.swift +++ b/TelegramUI/StickerPreviewPeekContent.swift @@ -5,12 +5,26 @@ import Postbox import TelegramCore import SwiftSignalKit +enum StickerPreviewPeekItem: Equatable { + case pack(StickerPackItem) + case found(FoundStickerItem) + + var file: TelegramMediaFile { + switch self { + case let .pack(item): + return item.file + case let .found(item): + return item.file + } + } +} + final class StickerPreviewPeekContent: PeekControllerContent { let account: Account - let item: StickerPackItem + let item: StickerPreviewPeekItem let menu: [PeekControllerMenuItem] - init(account: Account, item: StickerPackItem, menu: [PeekControllerMenuItem]) { + init(account: Account, item: StickerPreviewPeekItem, menu: [PeekControllerMenuItem]) { self.account = account self.item = item self.menu = menu @@ -43,13 +57,13 @@ final class StickerPreviewPeekContent: PeekControllerContent { private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerContentNode { private let account: Account - private let item: StickerPackItem + private let item: StickerPreviewPeekItem private var textNode: ASTextNode private var imageNode: TransformImageNode private var containerLayout: (ContainerViewLayout, CGFloat)? - init(account: Account, item: StickerPackItem) { + init(account: Account, item: StickerPreviewPeekItem) { self.account = account self.item = item diff --git a/TelegramUI/ThemeSettingsChatPreviewItem.swift b/TelegramUI/ThemeSettingsChatPreviewItem.swift index 12e413b572..4a2eb3c2e2 100644 --- a/TelegramUI/ThemeSettingsChatPreviewItem.swift +++ b/TelegramUI/ThemeSettingsChatPreviewItem.swift @@ -87,7 +87,7 @@ class ThemeSettingsChatPreviewItemNode: ListViewItemNode { self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) self.controllerInteraction = ChatControllerInteraction(openMessage: { _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false diff --git a/TelegramUI/UrlEscaping.swift b/TelegramUI/UrlEscaping.swift new file mode 100644 index 0000000000..50d39d3408 --- /dev/null +++ b/TelegramUI/UrlEscaping.swift @@ -0,0 +1,13 @@ +import Foundation + +extension CharacterSet { + static let urlQueryValueAllowed: CharacterSet = { + let generalDelimitersToEncode = ":#[]@" + let subDelimitersToEncode = "!$&'()*+,;=" + + var allowed = CharacterSet.urlQueryAllowed + allowed.remove(charactersIn: generalDelimitersToEncode + subDelimitersToEncode) + + return allowed + }() +} diff --git a/TelegramUI/UrlHandling.swift b/TelegramUI/UrlHandling.swift index 58f64bb7e0..cda79d8052 100644 --- a/TelegramUI/UrlHandling.swift +++ b/TelegramUI/UrlHandling.swift @@ -43,7 +43,7 @@ private func parseInternalUrl(query: String) -> ParsedInternalUrl? { let peerName: String = pathComponents[0] if pathComponents.count == 1 { if let queryItems = components.queryItems { - if peerName == "socks" { + if peerName == "socks" || peerName == "proxy" { var server: String? var port: String? var user: String? diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index c61e03bdc9..9af34d8d56 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -640,14 +640,18 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } - let notificationAction: (Int32) -> Void = { muteUntil in + let notificationAction: (Int32?) -> Void = { muteUntil in let muteInterval: Int32? - if muteUntil <= 0 { - muteInterval = nil - } else if muteUntil == Int32.max { - muteInterval = Int32.max + if let muteUntil = muteUntil { + if muteUntil <= 0 { + muteInterval = 0 + } else if muteUntil == Int32.max { + muteInterval = Int32.max + } else { + muteInterval = muteUntil + } } else { - muteInterval = muteUntil + muteInterval = nil } changeMuteSettingsDisposable.set(updatePeerMuteSetting(account: account, peerId: peerId, muteInterval: muteInterval).start()) @@ -657,13 +661,20 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll dismissAction() notificationAction(0) })) - let intervals: [Int32] = [ + let intervals: [Int32?] = [ + nil, 1 * 60 * 60, 8 * 60 * 60, 2 * 24 * 60 * 60 ] for value in intervals { - items.append(ActionSheetButtonItem(title: muteForIntervalString(strings: presentationData.strings, value: value), action: { + let title: String + if let value = value { + title = muteForIntervalString(strings: presentationData.strings, value: value) + } else { + title = "Default" + } + items.append(ActionSheetButtonItem(title: title, action: { dismissAction() notificationAction(value) }))