From 4dffa90851172e48848639eef41e48965bb95b11 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 20 May 2019 15:05:36 +0200 Subject: [PATCH] Added URL Auth support --- .../Text/FormatBold.imageset/Contents.json | 22 ++ .../Text/FormatBold.imageset/bold@2x.png | Bin 0 -> 603 bytes .../Text/FormatBold.imageset/bold@3x.png | Bin 0 -> 906 bytes .../Text/FormatItalic.imageset/Contents.json | 22 ++ .../Text/FormatItalic.imageset/italic@2x.png | Bin 0 -> 443 bytes .../Text/FormatItalic.imageset/italic@3x.png | Bin 0 -> 696 bytes .../Text/FormatLink.imageset/Contents.json | 22 ++ .../Text/FormatLink.imageset/link@2x.png | Bin 0 -> 858 bytes .../Text/FormatLink.imageset/link@3x.png | Bin 0 -> 1343 bytes .../FormatMonospace.imageset/Contents.json | 22 ++ .../FormatMonospace.imageset/markdown@2x.png | Bin 0 -> 573 bytes .../FormatMonospace.imageset/markdown@3x.png | Bin 0 -> 831 bytes .../Contents.json | 22 ++ .../strike@2x.png | Bin 0 -> 688 bytes .../strike@3x.png | Bin 0 -> 974 bytes TelegramUI.xcodeproj/project.pbxproj | 44 +++ TelegramUI/AnimatedStickerNode.swift | 12 + TelegramUI/AnimatedStickerPlayer.swift | 5 + TelegramUI/AnimatedStickerPlayerManager.swift | 5 + TelegramUI/AnimatedStickerUtils.swift | 154 ++++++++ .../AnimatedStickerVideoCompositor.swift | 5 + .../CachedResourceRepresentations.swift | 14 + TelegramUI/ChatAnimationGalleryItem.swift | 2 +- TelegramUI/ChatButtonKeyboardInputNode.swift | 20 +- TelegramUI/ChatController.swift | 117 ++++++ TelegramUI/ChatControllerInteraction.swift | 6 +- TelegramUI/ChatMessageActionButtonsNode.swift | 2 +- .../ChatMessageActionUrlAuthController.swift | 372 ++++++++++++++++++ .../ChatMessageAnimatedStickerItemNode.swift | 195 ++++++--- TelegramUI/ChatMessageItem.swift | 2 +- TelegramUI/ChatMessageItemView.swift | 4 +- .../ChatRecentActionsControllerNode.swift | 4 +- TelegramUI/ChatTextInputAttributes.swift | 1 + TelegramUI/ContactListActionItem.swift | 7 +- TelegramUI/ContactsController.swift | 7 + TelegramUI/ContactsControllerNode.swift | 11 + .../DataAndStorageSettingsController.swift | 3 +- TelegramUI/FFMpegMediaVideoFrameDecoder.swift | 2 +- TelegramUI/FetchCachedRepresentations.swift | 24 +- TelegramUI/GZip.h | 17 + TelegramUI/GZip.m | 79 ++++ TelegramUI/ItemListPlaceholderItem.swift | 205 ++++++++++ TelegramUI/MultiplexedVideoNode.swift | 2 +- TelegramUI/OverlayPlayerControllerNode.swift | 29 +- TelegramUI/PasscodeEntryController.swift | 9 +- .../PeerMediaCollectionController.swift | 1 + TelegramUI/RecentSessionsController.swift | 1 + TelegramUI/StickerResources.swift | 21 +- TelegramUI/TelegramUIPrivate/module.modulemap | 1 + 49 files changed, 1395 insertions(+), 98 deletions(-) create mode 100644 Images.xcassets/Chat/Input/Text/FormatBold.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Input/Text/FormatBold.imageset/bold@2x.png create mode 100644 Images.xcassets/Chat/Input/Text/FormatBold.imageset/bold@3x.png create mode 100644 Images.xcassets/Chat/Input/Text/FormatItalic.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Input/Text/FormatItalic.imageset/italic@2x.png create mode 100644 Images.xcassets/Chat/Input/Text/FormatItalic.imageset/italic@3x.png create mode 100644 Images.xcassets/Chat/Input/Text/FormatLink.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Input/Text/FormatLink.imageset/link@2x.png create mode 100644 Images.xcassets/Chat/Input/Text/FormatLink.imageset/link@3x.png create mode 100644 Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/markdown@2x.png create mode 100644 Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/markdown@3x.png create mode 100644 Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/strike@2x.png create mode 100644 Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/strike@3x.png create mode 100644 TelegramUI/AnimatedStickerNode.swift create mode 100644 TelegramUI/AnimatedStickerPlayer.swift create mode 100644 TelegramUI/AnimatedStickerPlayerManager.swift create mode 100644 TelegramUI/AnimatedStickerUtils.swift create mode 100644 TelegramUI/AnimatedStickerVideoCompositor.swift create mode 100644 TelegramUI/ChatMessageActionUrlAuthController.swift create mode 100644 TelegramUI/GZip.h create mode 100644 TelegramUI/GZip.m create mode 100644 TelegramUI/ItemListPlaceholderItem.swift diff --git a/Images.xcassets/Chat/Input/Text/FormatBold.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/FormatBold.imageset/Contents.json new file mode 100644 index 0000000000..c1829fc87f --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/FormatBold.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bold@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bold@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/FormatBold.imageset/bold@2x.png b/Images.xcassets/Chat/Input/Text/FormatBold.imageset/bold@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1437fe94866b03edcee5bd7d6e96c41bc27d7672 GIT binary patch literal 603 zcmV-h0;K(kP)B_?iyNM^?30wSUm z;v29LhnPrim_iWoVF{^_zgb*AdCube)Qm5%%`SW*^=rV2i2I(g1E2`Xz>f9Q#n*oj zHPY4+3-RYMXUndF{Mdiq>b1Sqi4o=<3UfOJE)Kt}DF5!u+h=aIO#%zO2 zhq1{IiMS$WnZCht97>Z|krKj|EODk&jetoo1N{Fedb!`Bm{HEj8u!@*0;Dt2>47gCdRCCJJPoHLbGA?62}9ep+tCQrQ( z&Vt!S3xdcSfpex(ErcQ_P<8<31clcE=S(Hhesc(ZuoQHWoAztktrYK&`_cnfE{Q@q z`1rW10LJ}+dAeD{w%zP~sctPnjOkue6mcdz3q>WdB0&PoZ*#MS9&0$EfTN(n%ButF pfI6TKr~~SNI-m}y1L{DK1OI`}NSs56Z9X(wf zLn;{G&f1$T;waHx+$j~ny|F7^K}=9uRD|gf`;q?+O=V_Ht!_Dg zmUb{oZxv+mP5jdqtRupGaK*=(xE+%pzf1eJ$+~*?uzxl@IuZ&-~(rf9b$yJ4-39-%#y{9%j&e$-0 z%dhInO%2bZ+}Chth$Sp$d|c&xlxy|GD{f|aOgY7eC#1_->m+X$yZh?sgqUg1^)I@< zxxZy*;g;y$TH&a7AIxW$Z!DIYn{C#1Pfsdr>hfEB;Xz-fG1eS@!DH1`bA2~^{ko-B z4hu~9oWUmBd}tzz&~D*BGw&{KyE;eqz`G6gTW?<5w6}4d#g!!PUDCX3s;==1ZHk}x z;QW(5-fPwV+wQ-pmACnKX5Ed1-0vxW&OGLR)Ws(DUAbE5*RvVv%>o};I+?^4yooCj zd|Ud&=%m1&_a9h|f8UF4ntMceg}i{a^^do^a{i_3FI9e#QagXuTAM>fd(O^kUYHTV z_SpT@o@bWtxU5{)|C(xatcg!s>ROZDwtG9gY#Z|!UV6`B$n5VFoOV_+H_Q0AKpn@1 zn_Es!e`g_dD*0pohX1Eaw_W?9^igA88>d3fxgRnKzlCn4mWkW)*iSkb<}S9)WV75& zrul1q<~T$LEKlO^(rn00j;MQgMS)A2^GDmm-FcH0&M|B%Esx)MH+N6aHs#F<8>CJO z<{xtBzMru;_l4E5>T5q)IDQNNS$X&3!AJAHOwD;~y=e8D63b1u7B`3b-}v5mb-U5T z&hU~AJJfH+T>8plDzVeIc2P5Hk70_uQOwiHrzc*O6V<7j%Dd5<)6?GX!}7A}a$*fy z84{99)H6bzwoT^zcjeup`w!Y14BqajlFr(;EH(c_bD!DTXPNV+`A7FI{MOx;D!|9$ zmDXqRxG|}eC-_(5jgPBt_kJ^PFs!=TT^4R}I;kP`@H>SC@7fMuvJ5)2A#TzoMTK64 z==Sq_MQYE#QafmGvZ;UdHl?hp-Z%5jzc+lD9_7m7;y?gR<7=!J3qHk?R-e-j%v=nf Lu6{1-oD!Mz`&1@%ghZ0HP=88 z1Hr&Z@eedo5Z2eQ3(Z`z!L-asH54;DyL4C8bO$>aDCB?~kOOi+4#)vHAP1^*plO?t|OR-w-y3cA#Uj6uG{2X!-YV0oDlvOLVgf+p9rhtk=A;2^YIYHI<&PA zumJH*M?R#jEPtpF@^67TE7Znqx5>Rm3;B~l&uz+7K3eO>&ByE12N@5-ZUh0Ad)sjh zn-8lICWPD@U}#0!s8&ciA7O)TMn&F;7xF=^=k{jmT9}bHH|ST<>_ct@0V@z+bL2zk z!|H?xA+K*Mt5dCzLu2p4ex}L~T4@Q4z#g~&X}oZ70(QZCv5Fvf4*JG=u|h;l4#)vH lAP3}t9FPNYKn_&lz%S@xA?#G%NSs56Z@;zM~ zLn;{G-rDQM94Ns0;K+WKjRK;zOo1MvuH3=e5x0NZm5JH4&o%qzCaCj=QIz|ln@f(! zL4^#V^Cb_9c=SBqotsshcwz3Hv*-Snl_v=mTk>*xI4}uKXkbxMU_@j7)tm60=|Ya# z+G*}bD%efeKL7mlFyFWQ-LC^~yFa$GXg<7G`OWi$Cgbf!-e&30oNyqit{A0Ds0=?p)q041`e*>toIGx&pOjPtEgN$=_}7`ma}^WL)&D| zD2x5xtoSwh!kd`Hg*(qLT9#Q9)an{=L-$8(<;uO4d226To!WXxV%pxyZT{}T@{tF; z-hN*F;mwYNPWxv$6n;)@yis;>{Te;cW9HbD9^R^?=G&N*Ic0ud&RR3^h!x3JTY z_j1fxywlusUw!b4a8(y(!M@x7LOH|uW-28Ke-zT%n)CkAcO#{f|Fc=$PrXnR+uOWC z>7797k$87rVST~XlCf2rHf_AX^ho&PNey-DRcYS1;vJG28tc_QFyGIMTqo&h@pm z1q%@f5eN|o5eN|o5hyPL)oQgHc9y4L11lr^1kA!OF#AYm0(v(vbumK`*#S2p&6a7r zP>iAmRzX}RJa8T?kRO9VxCGPi7tDqmSf;r{P!@OME_Hk3t5SCe-hxa!(>jkCV&YER zrmhlq!pgJ_zzWDFt-jP0A-S9|v5b?mvQJi5YKo9st8)JTN!}nsJ_=u3v}Cmy;!`yWwcw)fsaw+L0yFb>F9?dABB2Hd4pJI*rc2 zVb}wU)b)|hvm;BSsYc&!I-HL>JA21J zX=W* z{ZU!(m4BSH6NNgVTdcnT)_m+}9|E)2WRCghQa%XJtpjPXPDGLPSMeGrOfcfcols>L z;0e43Z}Sw2+lj~t{ZnLikIdTRWDfY0c3n4g^d{MFDzCt%-7HeY#DvXKJ_}!=pAXh| z-=365s9yNfpvS8Re!wf3WBej%@rEe-_jD}p>Wn=CyYi0P^!|v@4+Um}Sr=e{KVqL| z9oI;|tk38lH+iP(wVYtg4Y2FUJ2yvpS%m4=4q4qR1bzHYXv`y6{ic-XL9{YLCZq_{ z4+R7338Jlaoseh*_QEpAbbqRGhC01V{ubLhJK-abNZp~#R>002t}1^@s6I8J)%00004XF*Lt006O% z3;baP000E`NklVf>6vsXONb{QA#8MyXK12^nkfuV=i&2U{dg%`+-;{c* ztAYhS=!1k{BnUnTnJ;D1i@y~G!Aug1)E=UOUi^6zF*UlFcKm#GHY_)D&dfPyj_1s| zYr&U2d(YbI?De@bduH~`9W~0$2{-{K-~^n26L11fzzH}3C*TB}fD>>6?I2KTN6>c0 ztX8WU^Hekm4OJ?Yf7lF}tsk68_hrq&|DPfvYh#BC+)p! z0)>7;*6vbSkDf})oc6isB(kO>tandIl1l@1i}ttDAlWv2#-W|a+9~VPGuvcX3)Jg* zlO%?KeP?O0y)$Q!pxAQUVTgY$dvL8&h1BXdeT{^2A}_PvZ9udK=A0dR%z|T}3)2_oLm+Sr;jrwLv;QZ94BY+ugqjqzYPr zE}$&aW6Ec!ztPO-^9Iss7feT(o-@l~47Lw_8%Ezi`8&R|@@9$eYv}JN{@T3#JMPE8 zb;J2%lz^)Dkk+2QK-!=y*}}8ceKtx(?IFS$z1xyX8`?ztbLM}5Dv9KO-=Lfx;7H11?rJU z82q$Cun~QQUa^T#A3*j9TV?laoBbmLRFdq6Bpgg&1}qTjv(Yzb6B-}w8#$m7SRVty znohkJER?iud={bjs;4=sG^d_kF2qa0bypAY??;-vtqoCGiFBj-h|%^e+KjZsY^_wF z^tx)V9X;E1K?Mr<%|!80qEI)HVs1s-(FqhoqN(~bD#sC}q#3Lpg01BOWq{41^B*=q z&h0(;6|znh#?sc3!Rl1HZj#eqKl9~6JSh4+gFZ*YD9bM6@BxZneZ^tBtImK2qs%t6 zGD2cKpTs;yc?k*rzvxf23%!dbw(=Vt>&w9P?LjcjJ%yeY#4Fd)Q|m9GM3DVRV|Ojz zM%#(|BKio~CFe%NbdNuY=u&QNT@j-rpuOSLAEU4p)K(`^Gq(ocVvJHG)P#>!`AfKPbM&Y79ic5 zS?jG+FF*<Hw-D0#3jQH~}Z%1e|~qZ~{)i2|RWJ_W^RfHQBiX6$AhP002ovPDHLkV1np+ Beii@# literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/Contents.json new file mode 100644 index 0000000000..9af7e0f338 --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "markdown@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "markdown@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/markdown@2x.png b/Images.xcassets/Chat/Input/Text/FormatMonospace.imageset/markdown@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d74c0d35bc16cafa910abf31436c266b360cf772 GIT binary patch literal 573 zcmV-D0>b@?P)NklETme_W6>tSyfh5(^ zAj`5f)FNlQPSf;@8}f1H;7;MvPKp2_VIKY?5`T;G>h?|5eW6DF zK}qf|`sYfR9`7T#spsq#-h8dh@1yR3_5Ru7@Z3`A0P1Byi@6q6_Ja8 z?{Em`9@?|;OZZ@DtWnd7NDrtU;Hy{#`6Xjwykd>O%a^T)&>Rdl-(DcJh`w$38~h%w zYL-Okw;_Y`IoU@{^L6mMLH(*`Mg(Brf%Fc(jMdn{(`;LjB4$LWIE=Sf8}Syr2ZE0@ zpl#Mf`0I28;B+10ubDOv+ZBQOpFn&gQ1c3jE}d{jr_9JIXBef`_y)13k(mt!&4~c~ z74VUpn)Mkm1uu&`5vj%`#J(bvunH2xzgZDUs&b6G(-L{*7iuP2*U#aEN&0w_tCNYX zAG5VK!Du=-#o88CKZb}laFu#ZuP7~S1r;EoNK3?(!=^-zE6-${7_+syz@+*gkP?nG z2hCN<=z3k-!q>{;pi3-W+?-!!hsUmfE8q&a0NSs56ZzInPh zhEy=VowdJ9#8Kc_x{IrbtAWu3W_}(PW=8|_3(jAj2<<&`d`?EFsTBmWPkMUTU)uLf9eIc#EVd$m2@IsbY{==YGn+p5Z|%U5^xvUoILK}7;9 zigkXiH=VuAlJ9nJNA22mugeAh&f)sp?^KxWx3l)6$me*sFV^9$Dd!g+zqQvcGV#&d z8!0c|eVJ*0CH?vZR<~LEn_9kqi^$&E?r%0dC_&b(O?=lTmNTw5&X@drvaRy)uLPkz zX-{Tkye*46W8QsE`kK|IUn^(oF0T4knv%KwK~#IZ*<|xI@h9GNrj-jkJ{qyCp+e|a za8`Bm$K*5m+s{c)ds2R1C3E|%!0>IW1NktUPawrQ-2^789E3Nq3!il+sI?rpdqJ~g&Y zDIxRO#|Iq_MER54);>;t~+*`3+w7aSGRb}@V^C*rt6IZ0p`nE|UVpi4eu4{~j zGTW9fy}awTU&Gb5>t2bwPT#$Jch&v50j}>q`c<5%s5joUIzmQgA!9<`^Zo167az&J zA-aBb?*^d{!eWW40v-jase)lljV$3|Fs|x>MXSXb=e&G!yv_HmcU)o1?`dNG9oc4D zDmlm|`WVLjilz{iUmCec0(${=b_O$Bl*PDJt lRjsoZN1B5t#AQk!#Gij+aXc2C)(gxL44$rjF6*2Ung9b5d5!=8 literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/Contents.json b/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/Contents.json new file mode 100644 index 0000000000..6ebf2a685d --- /dev/null +++ b/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "strike@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "strike@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/strike@2x.png b/Images.xcassets/Chat/Input/Text/FormatStrikethrough.imageset/strike@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1b30548a1ff147780d7ee8428c93814a5c868839 GIT binary patch literal 688 zcmV;h0#E&kP)YNOj@W*1Z{^Zv?RvQFMSV=q{+LNybCYz`@yGo_wILh z-`_8n%e|&jYMKGffM!55pc&8%Xa+O`Bb|X}vpE4{BNqgs&%sl84?EC+L)e11a38$H z1}5U*G3-N_ea7K>T*1Ve6@)!O*s>vRoBAh^@+{=xHcWunKHTs;?^~;tgg*pv2mXPW zY@-Lz@x9>LMR*N?m8qObLJTbphuqn*AiR3We>=OS$v zK+!#rck{sH{rnMJgA81PTkrzRpg+)deM_(nD3s47J+r&}sXvs5#NSs56ZTRmMI zLn;{G&a%xAaTGaTob^XfL{RjiM32M-0bk{=usFd9H&*m^^e$*^O-Ni_nv}RQLA5Jk zsbcTcj6{zuL4nCvCJ1yaaAG;?vFQ2xi2`jqcix`+H|cIQ|G$*WAak98Ns%b=j1zXpCBv$ z;zoe}_x(2QD`#&Dz9n9J#$ey|z{j)vd++ogO)&nICAn8T_0biH4X-@zJ`K!zYQ%2* zcA8`6ybhztPkmNdrEV*qPEL+Ds4c2Jo>!{EWA*D$iEYvahNx7YxOAE2MKe^_M7A?u zUh{a%?(VDkea5!$HoyF;=efQlH1+zYx*b_l%FYNsu=;#<*R`63cNf*jurL%KZ&E#5 zSaQj5&&J&iO6E&1pAh|IP|J8Sc6XZq!!b*7&Bgv@;`0`+ej$*hr()Sts{Avm^Pxdf zL_*6-VH3A!S7gPj7V%&5$(X~L+i$&c$|v<5Pdm2GNU3~1L8$r7DK*~LHo@~9rnZQ< zD7|@pVopllQJ#I5RIhZdTX20v*q>)d<|PGnyzpvRz;4=MySzU0$fcvc%%=>$TxNV5 zQW$%@fNjNPzKFesUSigw5zb-;H#d2jPKx6Vp1UTr=0?`((>FtuC;!#?$NcT#qje8m zru{K8?+kXE|9;`$vS01_jWK=f*Q#D?E1x}GsOG(IO+(t3<144Hj)~c8GPm%L+}chf z&rfBGEmMvy)!14zvswFQ-Nwh7ve~t2XDV(!lXOly(OeQY-?R43+5V$vvc4QD*->C~ z=DxC{sHAfF7uy%Qn;yFb)CoNozFW#MOMovmIiDl9xTn_jey&MZ%J(FpT$fyruwBL{ zzV7lq_AYhfgz%q7xf)6ry#Kb}?Z~z_!CS?;Cp=|qW_pw8;xI|A-};l({)+vD8zOYg z3~#qS*&ddd+}3zK@UHwsojQj@>@(vwXPuS(ylK)_<)1T`#4L9C)K Bool { + if let items = items { + for case let item as [AnyHashable: Any] in items { + if let type = item["ty"] as? String { + if type == "rp" || type == "sr" || type == "mm" || type == "gs" { + return false + } + } + + if shapes, let subitems = item["it"] as? [Any] { + if !verifyLottieItems(subitems, shapes: false) { + return false + } + } + } + } + return true; +} + +private func verifyLottieLayers(_ layers: [AnyHashable: Any]?) -> Bool { + return true +} + +func validateStickerComposition(json: [AnyHashable: Any]) -> Bool { + guard let tgs = json["tgs"] as? Int, tgs == 1 else { + return false + } + + return true +} + +func convertCompressedLottieToCombinedMp4(data: Data, size: CGSize) -> Signal { + return Signal({ subscriber in + let startTime = CACurrentMediaTime() + let decompressedData = TGGUnzipData(data) + if let decompressedData = decompressedData, let json = (try? JSONSerialization.jsonObject(with: decompressedData, options: [])) as? [AnyHashable: Any] { + if let _ = json["tgs"] { + let model = LOTComposition(json: json) + if let startFrame = model.startFrame?.int32Value, let endFrame = model.endFrame?.int32Value { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let path = NSTemporaryDirectory() + "\(randomId).mp4" + let url = URL(fileURLWithPath: path) + + let videoSize = CGSize(width: size.width, height: size.height * 2.0) + let scale = size.width / 512.0 + + if let assetWriter = try? AVAssetWriter(outputURL: url, fileType: AVFileType.mp4) { + let videoSettings: [String: AnyObject] = [AVVideoCodecKey : AVVideoCodecH264 as AnyObject, AVVideoWidthKey : videoSize.width as AnyObject, AVVideoHeightKey : videoSize.height as AnyObject] + + let assetWriterInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoSettings) + let sourceBufferAttributes = [(kCVPixelBufferPixelFormatTypeKey as String): Int(kCVPixelFormatType_32ARGB), + (kCVPixelBufferWidthKey as String): Float(videoSize.width), + (kCVPixelBufferHeightKey as String): Float(videoSize.height)] as [String : Any] + let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: assetWriterInput, sourcePixelBufferAttributes: sourceBufferAttributes) + + assetWriter.add(assetWriterInput) + + if assetWriter.startWriting() { + print("startedWriting at \(CACurrentMediaTime() - startTime)") + assetWriter.startSession(atSourceTime: kCMTimeZero) + + var currentFrame: Int32 = 0 + let writeQueue = DispatchQueue(label: "assetWriterQueue") + writeQueue.async { + let container = LOTAnimationLayerContainer(model: model, size: size) + + let singleContext = DrawingContext(size: size, scale: 1.0, clear: true) + let context = DrawingContext(size: size, scale: 1.0, clear: false) + + let fps: Int32 = model.framerate?.int32Value ?? 30 + let frameDuration = CMTimeMake(1, fps) + + assetWriterInput.requestMediaDataWhenReady(on: writeQueue) { + while assetWriterInput.isReadyForMoreMediaData && startFrame + currentFrame < endFrame { + let lastFrameTime = CMTimeMake(Int64(currentFrame - startFrame), fps) + let presentationTime = currentFrame == 0 ? lastFrameTime : CMTimeAdd(lastFrameTime, frameDuration) + + singleContext.withContext { context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.saveGState() + context.scaleBy(x: scale, y: scale) + container?.renderFrame(startFrame + currentFrame, in: context) + context.restoreGState() + } + + let image = singleContext.generateImage() + let alphaImage = generateTintedImage(image: image, color: .white, backgroundColor: .black) + context.withFlippedContext { context in + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height), size: videoSize)) + if let image = image?.cgImage { + context.draw(image, in: CGRect(origin: CGPoint(x: 0.0, y: size.height), size: size)) + } + if let alphaImage = alphaImage?.cgImage { + context.draw(alphaImage, in: CGRect(origin: CGPoint(), size: size)) + } + } + + if let image = context.generateImage() { + if let pixelBufferPool = pixelBufferAdaptor.pixelBufferPool { + let pixelBufferPointer = UnsafeMutablePointer.allocate(capacity: 1) + let status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool, pixelBufferPointer) + if let pixelBuffer = pixelBufferPointer.pointee, status == 0 { + fillPixelBufferFromImage(image, pixelBuffer: pixelBuffer) + + pixelBufferAdaptor.append(pixelBuffer, withPresentationTime: presentationTime) + pixelBufferPointer.deinitialize(count: 1) + } else { + break + } + + pixelBufferPointer.deallocate() + } else { + break + } + } + currentFrame += 1 + } + + if startFrame + currentFrame == endFrame { + assetWriterInput.markAsFinished() + assetWriter.finishWriting { + subscriber.putNext(path) + subscriber.putCompletion() + print("animation render time \(CACurrentMediaTime() - startTime)") + } + } + } + } + } + } + } + } + } + return EmptyDisposable + }) +} + +private func fillPixelBufferFromImage(_ image: UIImage, pixelBuffer: CVPixelBuffer) { + CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer) + let rgbColorSpace = CGColorSpaceCreateDeviceRGB() + let context = CGContext(data: pixelData, width: Int(image.size.width), height: Int(image.size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) + context?.draw(image.cgImage!, in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) + CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) +} diff --git a/TelegramUI/AnimatedStickerVideoCompositor.swift b/TelegramUI/AnimatedStickerVideoCompositor.swift new file mode 100644 index 0000000000..cbadf2f9b2 --- /dev/null +++ b/TelegramUI/AnimatedStickerVideoCompositor.swift @@ -0,0 +1,5 @@ +import AVFoundation + +final class AnimatedStickerVideoCompositor: NSObject { + +} diff --git a/TelegramUI/CachedResourceRepresentations.swift b/TelegramUI/CachedResourceRepresentations.swift index 1d077504d1..ebbc35d3fb 100644 --- a/TelegramUI/CachedResourceRepresentations.swift +++ b/TelegramUI/CachedResourceRepresentations.swift @@ -213,3 +213,17 @@ final class CachedEmojiRepresentation: CachedMediaResourceRepresentation { } } } + +final class CachedAnimatedStickerRepresentation: CachedMediaResourceRepresentation { + var uniqueId: String { + return "animated-sticker" + } + + func isEqual(to: CachedMediaResourceRepresentation) -> Bool { + if let _ = to as? CachedAnimatedStickerRepresentation { + return true + } else { + return false + } + } +} diff --git a/TelegramUI/ChatAnimationGalleryItem.swift b/TelegramUI/ChatAnimationGalleryItem.swift index b4412e6df7..3366b7a3ec 100644 --- a/TelegramUI/ChatAnimationGalleryItem.swift +++ b/TelegramUI/ChatAnimationGalleryItem.swift @@ -131,7 +131,7 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode { func setFile(context: AccountContext, fileReference: FileMediaReference) { if self.contextAndMedia == nil || !self.contextAndMedia!.1.media.isEqual(to: fileReference.media) { - let signal = chatMessageAnimationData(postbox: context.account.postbox, fileReference: fileReference, synchronousLoad: false) + let signal = chatMessageAnimatedStrickerBackingData(postbox: context.account.postbox, fileReference: fileReference, synchronousLoad: false) |> mapToSignal { data, completed -> Signal in if completed, let data = data { return .single(data) diff --git a/TelegramUI/ChatButtonKeyboardInputNode.swift b/TelegramUI/ChatButtonKeyboardInputNode.swift index 1bb44e3a02..9c5d42db0f 100644 --- a/TelegramUI/ChatButtonKeyboardInputNode.swift +++ b/TelegramUI/ChatButtonKeyboardInputNode.swift @@ -160,20 +160,20 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { if let button = button as? ChatButtonKeyboardInputButtonNode, let markupButton = button.button { switch markupButton.action { case .text: - controllerInteraction.sendMessage(markupButton.title) + self.controllerInteraction.sendMessage(markupButton.title) case let .url(url): - controllerInteraction.openUrl(url, true, nil) + self.controllerInteraction.openUrl(url, true, nil) case .requestMap: - controllerInteraction.shareCurrentLocation() + self.controllerInteraction.shareCurrentLocation() case .requestPhone: - controllerInteraction.shareAccountContact() + self.controllerInteraction.shareAccountContact() case .openWebApp: if let message = self.message { - controllerInteraction.requestMessageActionCallback(message.id, nil, true) + self.controllerInteraction.requestMessageActionCallback(message.id, nil, true) } case let .callback(data): if let message = self.message { - controllerInteraction.requestMessageActionCallback(message.id, data, false) + self.controllerInteraction.requestMessageActionCallback(message.id, data, false) } case let .switchInline(samePeer, query): if let message = message { @@ -195,13 +195,15 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { peerId = message.id.peerId } if let botPeer = botPeer, let addressName = botPeer.addressName { - controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: "@\(addressName) \(query)")), messageId: nil), nil) + self.controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: "@\(addressName) \(query)")), messageId: nil), nil) } } case .payment: break - case .urlAuth: - break + case let .urlAuth(url, buttonId): + if let message = self.message { + self.controllerInteraction.requestMessageActionUrlAuth(url, message.id, buttonId) + } } } } diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index e2195fab48..69ed212120 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -128,6 +128,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal private let sentMessageEventsDisposable = MetaDisposable() private let failedMessageEventsDisposable = MetaDisposable() private let messageActionCallbackDisposable = MetaDisposable() + private let messageActionUrlAuthDisposable = MetaDisposable() private let editMessageDisposable = MetaDisposable() private let enqueueMediaMessageDisposable = MetaDisposable() private var resolvePeerByNameDisposable: MetaDisposable? @@ -643,6 +644,121 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal })) } } + }, requestMessageActionUrlAuth: { [weak self] defaultUrl, messageId, buttonId in + if let strongSelf = self { + if let _ = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if !$0.contains(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.append(.requestInProgress) + return updatedContexts.sorted() + } + return $0 + } + }) + + strongSelf.messageActionUrlAuthDisposable.set(((combineLatest(strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId), requestMessageActionUrlAuth(account: strongSelf.context.account, messageId: messageId, buttonId: buttonId) |> afterDisposed { + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if let index = $0.index(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts + } + return $0 + } + }) + } + } + })) |> deliverOnMainQueue).start(next: { peer, result in + if let strongSelf = self { + switch result { + case .default: + strongSelf.openUrl(defaultUrl, concealed: false) + case let .request(domain, bot, requestWriteAccess): + let controller = chatMessageActionUrlAuthController(context: strongSelf.context, defaultUrl: defaultUrl, domain: domain, bot: bot, requestWriteAccess: requestWriteAccess, displayName: peer.displayTitle, open: { [weak self] authorize, allowWriteAccess in + if let strongSelf = self { + if authorize { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if !$0.contains(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.append(.requestInProgress) + return updatedContexts.sorted() + } + return $0 + } + }) + + strongSelf.messageActionUrlAuthDisposable.set(((acceptMessageActionUrlAuth(account: strongSelf.context.account, messageId: messageId, buttonId: buttonId, allowWriteAccess: allowWriteAccess) |> afterDisposed { + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if let index = $0.index(where: { + switch $0 { + case .requestInProgress: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts + } + return $0 + } + }) + } + } + }) |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + switch result { + case let .accepted(url): + strongSelf.openUrl(url, concealed: false) + default: + strongSelf.openUrl(defaultUrl, concealed: false) + } + } + })) + } else { + strongSelf.openUrl(defaultUrl, concealed: false) + } + } + }) + strongSelf.present(controller, in: .window(.root)) + case let .accepted(url): + strongSelf.openUrl(url, concealed: false) + } + } + })) + } + } }, activateSwitchInline: { [weak self] peerId, inputString in guard let strongSelf = self else { return @@ -1706,6 +1822,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal self.sentMessageEventsDisposable.dispose() self.failedMessageEventsDisposable.dispose() self.messageActionCallbackDisposable.dispose() + self.messageActionUrlAuthDisposable.dispose() self.editMessageDisposable.dispose() self.enqueueMediaMessageDisposable.dispose() self.resolvePeerByNameDisposable?.dispose() diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index e16d330aa3..9b3f684635 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -62,6 +62,7 @@ public final class ChatControllerInteraction { let sendSticker: (FileMediaReference, Bool) -> Void let sendGif: (FileMediaReference) -> Void let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool) -> Void + let requestMessageActionUrlAuth: (String, MessageId, Int32) -> Void let activateSwitchInline: (PeerId?, String) -> Void let openUrl: (String, Bool, Bool?) -> Void let shareCurrentLocation: () -> Void @@ -102,7 +103,7 @@ public final class ChatControllerInteraction { var pollActionState: ChatInterfacePollActionState var searchTextHighightState: String? - init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool) -> Void, sendGif: @escaping (FileMediaReference) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @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, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState) { + init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool) -> Void, sendGif: @escaping (FileMediaReference) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @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, navigationController: @escaping () -> NavigationController?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention @@ -114,6 +115,7 @@ public final class ChatControllerInteraction { self.sendSticker = sendSticker self.sendGif = sendGif self.requestMessageActionCallback = requestMessageActionCallback + self.requestMessageActionUrlAuth = requestMessageActionUrlAuth self.activateSwitchInline = activateSwitchInline self.openUrl = openUrl self.shareCurrentLocation = shareCurrentLocation @@ -153,7 +155,7 @@ public final class ChatControllerInteraction { static var `default`: ChatControllerInteraction { return ChatControllerInteraction(openMessage: { _, _ in - return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + return false }, openPeer: { _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _ in }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, navigationController: { return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in diff --git a/TelegramUI/ChatMessageActionButtonsNode.swift b/TelegramUI/ChatMessageActionButtonsNode.swift index fde439917e..6c1825609c 100644 --- a/TelegramUI/ChatMessageActionButtonsNode.swift +++ b/TelegramUI/ChatMessageActionButtonsNode.swift @@ -79,7 +79,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { switch button.action { case .text: iconImage = incoming ? graphics.chatBubbleActionButtonIncomingMessageIconImage : graphics.chatBubbleActionButtonOutgoingMessageIconImage - case .url: + case .url, .urlAuth: iconImage = incoming ? graphics.chatBubbleActionButtonIncomingLinkIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage case .requestPhone: iconImage = incoming ? graphics.chatBubbleActionButtonIncomingPhoneIconImage : graphics.chatBubbleActionButtonOutgoingLinkIconImage diff --git a/TelegramUI/ChatMessageActionUrlAuthController.swift b/TelegramUI/ChatMessageActionUrlAuthController.swift new file mode 100644 index 0000000000..de310b6117 --- /dev/null +++ b/TelegramUI/ChatMessageActionUrlAuthController.swift @@ -0,0 +1,372 @@ +import Foundation +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore + +private final class ChatMessageActionUrlAuthContentActionNode: HighlightableButtonNode { + private let backgroundNode: ASDisplayNode + + let action: TextAlertAction + + init(theme: AlertControllerTheme, action: TextAlertAction) { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.alpha = 0.0 + + self.action = action + + super.init() + + self.titleNode.maximumNumberOfLines = 2 + + self.highligthedChanged = { [weak self] value in + if let strongSelf = self { + if value { + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backgroundNode.alpha = 1.0 + } else if !strongSelf.backgroundNode.alpha.isZero { + strongSelf.backgroundNode.alpha = 0.0 + strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + } + } + } + + self.updateTheme(theme) + } + + func updateTheme(_ theme: AlertControllerTheme) { + self.backgroundNode.backgroundColor = theme.highlightedItemColor + + var font = Font.regular(17.0) + var color = theme.accentColor + switch self.action.type { + case .defaultAction, .genericAction: + break + case .destructiveAction: + color = theme.destructiveColor + } + switch self.action.type { + case .defaultAction: + font = Font.semibold(17.0) + case .destructiveAction, .genericAction: + break + } + self.setAttributedTitle(NSAttributedString(string: self.action.title, font: font, textColor: color, paragraphAlignment: .center), for: []) + } + + override func didLoad() { + super.didLoad() + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + @objc func pressed() { + self.action.action() + } + + override func layout() { + super.layout() + + self.backgroundNode.frame = self.bounds + } +} + +private let textFont = Font.regular(13.0) +private let boldTextFont = Font.semibold(13.0) + +private func formattedText(_ text: String, color: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { + return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: color), linkAttribute: { _ in return nil}), textAlignment: textAlignment) +} + +private final class ChatMessageActionUrlAuthAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + private let defaultUrl: String + private let domain: String + private let bot: Peer + private let displayName: String + + private let titleNode: ASTextNode + private let textNode: ASTextNode + private let authorizeCheckNode: CheckNode + private let authorizeLabelNode: ASTextNode + private let allowWriteCheckNode: CheckNode + private let allowWriteLabelNode: ASTextNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [ChatMessageActionUrlAuthContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + var authorize: Bool = true { + didSet { + self.authorizeCheckNode.setIsChecked(self.authorize, animated: true) + if !self.authorize && self.allowWriteAccess { + self.allowWriteAccess = false + } + } + } + + var allowWriteAccess: Bool = true { + didSet { + self.allowWriteCheckNode.setIsChecked(self.allowWriteAccess, animated: true) + if !self.authorize && self.allowWriteAccess { + self.authorize = true + } + } + } + + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, defaultUrl: String, domain: String, bot: Peer, requestWriteAccess: Bool, displayName: String, actions: [TextAlertAction]) { + self.strings = strings + self.defaultUrl = defaultUrl + self.domain = domain + self.bot = bot + self.displayName = displayName + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 2 + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.authorizeCheckNode = CheckNode(strokeColor: theme.separatorColor, fillColor: theme.accentColor, foregroundColor: .white, style: .plain) + self.authorizeCheckNode.setIsChecked(true, animated: false) + self.authorizeLabelNode = ASTextNode() + self.authorizeLabelNode.maximumNumberOfLines = 2 + + self.allowWriteCheckNode = CheckNode(strokeColor: theme.separatorColor, fillColor: theme.accentColor, foregroundColor: .white, style: .plain) + self.allowWriteCheckNode.setIsChecked(true, animated: false) + self.allowWriteLabelNode = ASTextNode() + self.allowWriteLabelNode.maximumNumberOfLines = 2 + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> ChatMessageActionUrlAuthContentActionNode in + return ChatMessageActionUrlAuthContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + self.addSubnode(self.authorizeCheckNode) + self.addSubnode(self.authorizeLabelNode) + + if requestWriteAccess { + self.addSubnode(self.allowWriteCheckNode) + self.addSubnode(self.allowWriteLabelNode) + } + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.authorizeCheckNode.addTarget(target: self, action: #selector(self.authorizePressed)) + self.allowWriteCheckNode.addTarget(target: self, action: #selector(self.allowWritePressed)) + + self.updateTheme(theme) + } + + @objc private func authorizePressed() { + self.authorize = !self.authorize + } + + @objc private func allowWritePressed() { + self.allowWriteAccess = !self.allowWriteAccess + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: strings.Conversation_OpenBotLinkTitle, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + self.textNode.attributedText = formattedText(strings.Conversation_OpenBotLinkText(self.defaultUrl).0, color: theme.primaryColor, textAlignment: .center) + self.authorizeLabelNode.attributedText = formattedText(strings.Conversation_OpenBotLinkLogin(self.domain, self.displayName).0, color: theme.primaryColor) + self.allowWriteLabelNode.attributedText = formattedText(strings.Conversation_OpenBotLinkAllowMessages(self.bot.displayTitle).0, color: theme.primaryColor) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let titleSize = self.titleNode.measure(size) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 9.0 + + let textSize = self.textNode.measure(size) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 16.0 + + let checkSize = CGSize(width: 32.0, height: 32.0) + let condensedSize = CGSize(width: size.width - 76.0, height: size.height) + + var entriesHeight: CGFloat = 0.0 + + let authorizeSize = self.authorizeLabelNode.measure(condensedSize) + transition.updateFrame(node: self.authorizeLabelNode, frame: CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: authorizeSize)) + transition.updateFrame(node: self.authorizeCheckNode, frame: CGRect(origin: CGPoint(x: 7.0, y: origin.y - 7.0), size: checkSize)) + origin.y += authorizeSize.height + entriesHeight += authorizeSize.height + + if self.allowWriteLabelNode.supernode != nil { + origin.y += 16.0 + entriesHeight += 16.0 + + let allowWriteSize = self.allowWriteLabelNode.measure(condensedSize) + transition.updateFrame(node: self.allowWriteLabelNode, frame: CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: allowWriteSize)) + transition.updateFrame(node: self.allowWriteCheckNode, frame: CGRect(origin: CGPoint(x: 7.0, y: origin.y - 7.0), size: checkSize)) + origin.y += allowWriteSize.height + entriesHeight += allowWriteSize.height + } + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.measure(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + var contentWidth = max(titleSize.width, minActionsWidth) + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + entriesHeight + actionsHeight + 30.0 + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + return resultSize + } +} + +func chatMessageActionUrlAuthController(context: AccountContext, defaultUrl: String, domain: String, bot: Peer, requestWriteAccess: Bool, displayName: String, open: @escaping (Bool, Bool) -> Void) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let theme = presentationData.theme + let strings = presentationData.strings + + var contentNode: ChatMessageActionUrlAuthAlertContentNode? + + var dismissImpl: ((Bool) -> Void)? + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Conversation_OpenBotLinkOpen, action: { + dismissImpl?(true) + if let contentNode = contentNode { + open(contentNode.authorize, contentNode.allowWriteAccess) + } + })] + contentNode = ChatMessageActionUrlAuthAlertContentNode(theme: AlertControllerTheme(presentationTheme: theme), ptheme: theme, strings: strings, defaultUrl: defaultUrl, domain: domain, bot: bot, requestWriteAccess: requestWriteAccess, displayName: displayName, actions: actions) + let controller = AlertController(theme: AlertControllerTheme(presentationTheme: theme), contentNode: contentNode!) + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/TelegramUI/ChatMessageAnimatedStickerItemNode.swift b/TelegramUI/ChatMessageAnimatedStickerItemNode.swift index 3241be1f09..73250443f7 100644 --- a/TelegramUI/ChatMessageAnimatedStickerItemNode.swift +++ b/TelegramUI/ChatMessageAnimatedStickerItemNode.swift @@ -4,64 +4,165 @@ import Display import SwiftSignalKit import Postbox import TelegramCore -import Lottie +import AVFoundation +import CoreImage -private final class StickerAnimationNode : ASDisplayNode { - private var disposable = MetaDisposable() - var loopCount: Int = 0 +private class AlphaFrameFilter: CIFilter { + static var kernel: CIColorKernel? = { + return CIColorKernel(source: """ +kernel vec4 alphaFrame(__sample s, __sample m) { + return vec4( s.rgb, m.r ); +} +""") + }() + + var inputImage: CIImage? + var maskImage: CIImage? + + override var outputImage: CIImage? { + let kernel = AlphaFrameFilter.kernel! + guard let inputImage = inputImage, let maskImage = maskImage else { + return nil + } + let args = [inputImage as AnyObject, maskImage as AnyObject] + return kernel.apply(extent: inputImage.extent, arguments: args) + } +} + +private func createVideoComposition(for playerItem: AVPlayerItem) -> AVVideoComposition? { + let videoSize = CGSize(width: playerItem.presentationSize.width, height: playerItem.presentationSize.height / 2.0) + if #available(iOSApplicationExtension 9.0, *) { + let composition = AVMutableVideoComposition(asset: playerItem.asset, applyingCIFiltersWithHandler: { request in + let sourceRect = CGRect(origin: .zero, size: videoSize) + let alphaRect = sourceRect.offsetBy(dx: 0, dy: sourceRect.height) + let filter = AlphaFrameFilter() + filter.inputImage = request.sourceImage.cropped(to: alphaRect) + .transformed(by: CGAffineTransform(translationX: 0, y: -sourceRect.height)) + filter.maskImage = request.sourceImage.cropped(to: sourceRect) + return request.finish(with: filter.outputImage!, context: nil) + }) + composition.renderSize = videoSize + return composition + } else { + return nil + } +} + +private final class StickerAnimationNode: ASDisplayNode { + private var account: Account? + private var fileReference: FileMediaReference? + private let disposable = MetaDisposable() + private let fetchDisposable = MetaDisposable() + + var playerLayer: AVPlayerLayer { + return self.layer as! AVPlayerLayer + } + + var player: AVPlayer? { + get { + if self.isNodeLoaded { + return self.playerLayer.player + } else { + return nil + } + } + set { + if let player = self.playerLayer.player { + player.removeObserver(self, forKeyPath: #keyPath(AVPlayer.rate)) + } + self.playerLayer.player = newValue + if let newValue = newValue { + newValue.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: [], context: nil) + } + } + } + + private var playerItem: AVPlayerItem? = nil { + willSet { + self.playerItem?.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.status)) + } + didSet { + self.playerItem?.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: .new, context: nil) + self.setupLooping() + } + } override init() { super.init() - self.setViewBlock({ - let view = LOTAnimationView() - return view + self.setLayerBlock({ + return AVPlayerLayer() }) + + self.playerLayer.isHidden = true + if #available(iOSApplicationExtension 9.0, *) { + self.playerLayer.pixelBufferAttributes = [(kCVPixelBufferPixelFormatTypeKey as String): kCVPixelFormatType_32BGRA] + } } deinit { + NotificationCenter.default.removeObserver(self.didPlayToEndTimeObsever as Any) + self.player = nil + self.playerItem = nil self.disposable.dispose() + self.fetchDisposable.dispose() } - func setSignal(_ signal: Signal) { - self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] next in - if let object = try? JSONSerialization.jsonObject(with: next, options: []) as? [AnyHashable: Any], let json = object { - self?.animationView()?.setAnimation(json: json) + func setup(account: Account, fileReference: FileMediaReference) { + self.disposable.set(chatMessageAnimationData(postbox: account.postbox, fileReference: fileReference, synchronousLoad: false).start(next: { [weak self] data in + if let strongSelf = self, data.complete { + let playerItem = AVPlayerItem(url: URL(fileURLWithPath: data.path)) + strongSelf.player = AVPlayer(playerItem: playerItem) + strongSelf.playerItem = playerItem } })) + self.fetchDisposable.set(fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(fileReference.media.resource)).start()) } - func animationView() -> LOTAnimationView? { - return self.view as? LOTAnimationView + private func setupLooping() { + guard let playerItem = self.playerItem, let player = self.player else { + return + } + + self.didPlayToEndTimeObsever = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: nil, using: { _ in + player.seek(to: kCMTimeZero) { _ in + player.play() + } + }) + } + + private var didPlayToEndTimeObsever: NSObjectProtocol? = nil { + willSet(newObserver) { + if let observer = self.didPlayToEndTimeObsever, self.didPlayToEndTimeObsever !== newObserver { + NotificationCenter.default.removeObserver(observer) + } + } + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if let playerItem = object as? AVPlayerItem, playerItem === self.playerItem { + if case .readyToPlay = playerItem.status, playerItem.videoComposition == nil { + playerItem.videoComposition = createVideoComposition(for: playerItem) + playerItem.seekingWaitsForVideoCompositionRendering = true + } + self.player?.play() + } else if let player = object as? AVPlayer, player === self.player { + if self.playerLayer.isHidden && player.rate > 0.0 { + Queue.mainQueue().after(0.3) { + self.playerLayer.isHidden = false + } + } + } else { + return super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + } } func play() { - DispatchQueue.main.async { - if let animationView = self.animationView(), !animationView.isAnimationPlaying { - self.loopCount = 2 - - var completion: ((Bool) -> Void)! - let placeholder: (Bool) -> Void = { [weak animationView] _ in - self.loopCount -= 1 - if self.loopCount > 0 { - animationView?.play(completion: completion) - } - } - completion = placeholder - - if !animationView.isAnimationPlaying { - animationView.play(completion: completion) - } - } - } + } func reset() { - DispatchQueue.main.async { - if let animationView = self.animationView() { - animationView.stop() - } - } + } } @@ -77,8 +178,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var telegramFile: TelegramMediaFile? - private let fetchDisposable = MetaDisposable() - private let dateAndStatusNode: ChatMessageDateAndStatusNode private var replyInfoNode: ChatMessageReplyInfoNode? private var replyBackgroundNode: ASImageNode? @@ -104,10 +203,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { fatalError("init(coder:) has not been implemented") } - deinit { - self.fetchDisposable.dispose() - } - override func didLoad() { super.didLoad() @@ -141,19 +236,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { for media in item.message.media { if let telegramFile = media as? TelegramMediaFile { if self.telegramFile != telegramFile { - let signal = chatMessageAnimationData(postbox: item.context.account.postbox, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: telegramFile), synchronousLoad: false) - |> mapToSignal { data, completed -> Signal in - if completed, let data = data { - return .single(data) - } else { - return .complete() - } - } + self.telegramFile = telegramFile - self.animationNode.setSignal(signal) - self.fetchDisposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start()) - - self.animationNode.play() + self.animationNode.setup(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)) } break @@ -454,7 +539,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } if let item = self.item, self.imageNode.frame.contains(location) { - self.animationNode.play() + //self.animationNode.play() //let _ = item.controllerInteraction.openMessage(item.message, .default) return } diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index f0ed6cf146..35b4bb6ae0 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -346,7 +346,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { loop: for media in self.message.media { if let telegramFile = media as? TelegramMediaFile { - if GlobalExperimentalSettings.animatedStickers && telegramFile.fileName == "animation.json" { + if let fileName = telegramFile.fileName, fileName.hasSuffix(".tgs"), let size = telegramFile.size, size > 0 && size < 64 * 1024 { viewClassName = ChatMessageAnimatedStickerItemNode.self break loop } diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index fecfb4326b..3b923e628f 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -749,8 +749,8 @@ public class ChatMessageItemView: ListViewItemNode { } case .payment: item.controllerInteraction.openCheckoutOrReceipt(item.message.id) - case .urlAuth: - break + case let .urlAuth(url, buttonId): + item.controllerInteraction.requestMessageActionUrlAuth(url, item.message.id, buttonId) } } } diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift index 4462517b81..e52ab20628 100644 --- a/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -178,7 +178,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { self?.openPeerMention(name) }, openMessageContextMenu: { [weak self] message, selectAll, node, frame in self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame) - }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _ in + }, navigateToMessage: { _, _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendMessage: { _ in }, sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _ in self?.openUrl(url) }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { @@ -607,9 +607,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if let query = strongSelf.filter.query, hasFilter { text = strongSelf.presentationData.strings.Channel_AdminLog_EmptyFilterQueryText(query).0 } else { - text = isSupergroup ? strongSelf.presentationData.strings.Group_AdminLog_EmptyText : strongSelf.presentationData.strings.Broadcast_AdminLog_EmptyText - } strongSelf.emptyNode.setup(title: hasFilter ? strongSelf.presentationData.strings.Channel_AdminLog_EmptyFilterTitle : strongSelf.presentationData.strings.Channel_AdminLog_EmptyTitle, text: text) } diff --git a/TelegramUI/ChatTextInputAttributes.swift b/TelegramUI/ChatTextInputAttributes.swift index a454389769..3e1ac3ca05 100644 --- a/TelegramUI/ChatTextInputAttributes.swift +++ b/TelegramUI/ChatTextInputAttributes.swift @@ -35,6 +35,7 @@ private struct FontAttributes: OptionSet { static let bold = FontAttributes(rawValue: 1 << 0) static let italic = FontAttributes(rawValue: 1 << 1) static let monospace = FontAttributes(rawValue: 1 << 2) + static let strikethrough = FontAttributes(rawValue: 1 << 3) } func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor) -> NSAttributedString { diff --git a/TelegramUI/ContactListActionItem.swift b/TelegramUI/ContactListActionItem.swift index 44b624a02f..048a25d21f 100644 --- a/TelegramUI/ContactListActionItem.swift +++ b/TelegramUI/ContactListActionItem.swift @@ -276,17 +276,18 @@ class ContactListActionItemNode: ListViewItemNode { strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) } + 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.isHidden = true strongSelf.bottomStripeNode.isHidden = hideBottomStripe - if !hideBottomStripe { - print("") - } strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: titleOffset, y: floor((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 50.0 + UIScreenPixel + UIScreenPixel)) + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: 50.0 + UIScreenPixel + UIScreenPixel)) } }) } diff --git a/TelegramUI/ContactsController.swift b/TelegramUI/ContactsController.swift index f0b10d8bff..ee22c9f949 100644 --- a/TelegramUI/ContactsController.swift +++ b/TelegramUI/ContactsController.swift @@ -245,6 +245,13 @@ public class ContactsController: ViewController { openPeer(peer, false) } + self.contactsNode.openPeopleNearby = { [weak self] in + if let strongSelf = self { + //let controller = peopleNearbyController(context: strongSelf.context) + //(strongSelf.navigationController as? NavigationController)?.pushViewController(controller) + } + } + self.contactsNode.openInvite = { [weak self] in if let strongSelf = self { (strongSelf.navigationController as? NavigationController)?.pushViewController(InviteContactsController(context: strongSelf.context)) diff --git a/TelegramUI/ContactsControllerNode.swift b/TelegramUI/ContactsControllerNode.swift index 4f292bd0b7..6e23be2b0f 100644 --- a/TelegramUI/ContactsControllerNode.swift +++ b/TelegramUI/ContactsControllerNode.swift @@ -17,6 +17,7 @@ final class ContactsControllerNode: ASDisplayNode { var requestDeactivateSearch: (() -> Void)? var requestOpenPeerFromSearch: ((ContactListPeer) -> Void)? + var openPeopleNearby: (() -> Void)? var openInvite: (() -> Void)? private var presentationData: PresentationData @@ -27,7 +28,11 @@ final class ContactsControllerNode: ASDisplayNode { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + var addNearbyImpl: (() -> Void)? var inviteImpl: (() -> Void)? + //ContactListAdditionalOption(title: presentationData.strings.Contacts_AddPeopleNearby, icon: .generic(UIImage(bundleImageName: "Contact List/PeopleNearbyIcon")!), action: { + // addNearbyImpl?() + //}), let options = [ContactListAdditionalOption(title: presentationData.strings.Contacts_InviteFriends, icon: .generic(UIImage(bundleImageName: "Contact List/AddMemberIcon")!), action: { inviteImpl?() })] @@ -68,6 +73,12 @@ final class ContactsControllerNode: ASDisplayNode { } }) + addNearbyImpl = { [weak self] in + if let strongSelf = self { + strongSelf.openPeopleNearby?() + } + } + inviteImpl = { [weak self] in let _ = (DeviceAccess.authorizationStatus(context: context, subject: .contacts) |> take(1) diff --git a/TelegramUI/DataAndStorageSettingsController.swift b/TelegramUI/DataAndStorageSettingsController.swift index 67c5a5b3f4..6869b97dc1 100644 --- a/TelegramUI/DataAndStorageSettingsController.swift +++ b/TelegramUI/DataAndStorageSettingsController.swift @@ -570,7 +570,6 @@ func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndSt } let controller = ItemListController(context: context, state: signal) - pushControllerImpl = { [weak controller] c in if let controller = controller { (controller.navigationController as? NavigationController)?.pushViewController(c) @@ -579,6 +578,6 @@ func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndSt presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } - + return controller } diff --git a/TelegramUI/FFMpegMediaVideoFrameDecoder.swift b/TelegramUI/FFMpegMediaVideoFrameDecoder.swift index cfb1e5b1ad..ea0beed23b 100644 --- a/TelegramUI/FFMpegMediaVideoFrameDecoder.swift +++ b/TelegramUI/FFMpegMediaVideoFrameDecoder.swift @@ -49,7 +49,7 @@ final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { } func decode(frame: MediaTrackDecodableFrame, ptsOffset: CMTime?) -> MediaTrackFrame? { - var status = frame.packet.send(toDecoder: self.codecContext) + let status = frame.packet.send(toDecoder: self.codecContext) if status == 0 { if self.codecContext.receive(into: self.videoFrame) { var pts = CMTimeMake(self.videoFrame.pts, frame.pts.timescale) diff --git a/TelegramUI/FetchCachedRepresentations.swift b/TelegramUI/FetchCachedRepresentations.swift index ba13566825..3d36c8c149 100644 --- a/TelegramUI/FetchCachedRepresentations.swift +++ b/TelegramUI/FetchCachedRepresentations.swift @@ -8,6 +8,8 @@ import Display import UIKit import AVFoundation import WebP +import Lottie +import TelegramUIPrivateModule public func fetchCachedResourceRepresentation(account: Account, resource: MediaResource, representation: CachedMediaResourceRepresentation) -> Signal { if let representation = representation as? CachedStickerAJpegRepresentation { @@ -16,7 +18,7 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR if !data.complete { return .complete() } - return fetchCachedStickerAJpegRepresentation(account: account, resource: resource, resourceData: data, representation: representation) + return fetchCachedStickerAJpegRepresentation(account: account, resource: resource, resourceData: data, representation: representation) } } else if let representation = representation as? CachedScaledImageRepresentation { return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) @@ -105,6 +107,14 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR return fetchEmojiThumbnailRepresentation(account: account, resource: resource, representation: representation) } else if let representation = representation as? CachedEmojiRepresentation { return fetchEmojiRepresentation(account: account, resource: resource, representation: representation) + } else if let representation = representation as? CachedAnimatedStickerRepresentation { + return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) + |> mapToSignal { data -> Signal in + if !data.complete { + return .complete() + } + return fetchAnimatedStickerRepresentation(account: account, resource: resource, resourceData: data, representation: representation) + } } return .never() } @@ -871,3 +881,15 @@ private func fetchEmojiRepresentation(account: Account, resource: MediaResource, } } +private func fetchAnimatedStickerRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedAnimatedStickerRepresentation) -> Signal { + return Signal({ subscriber in + if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { + return convertCompressedLottieToCombinedMp4(data: data, size: CGSize(width: 400.0, height: 400.0)).start(next: { path in + subscriber.putNext(CachedMediaResourceRepresentationResult(temporaryPath: path)) + subscriber.putCompletion() + }) + } else { + return EmptyDisposable + } + }) |> runOn(Queue.concurrentDefaultQueue()) +} diff --git a/TelegramUI/GZip.h b/TelegramUI/GZip.h new file mode 100644 index 0000000000..144366f3e2 --- /dev/null +++ b/TelegramUI/GZip.h @@ -0,0 +1,17 @@ +#ifndef Telegram_GZip_h +#define Telegram_GZip_h + +#import + +#ifdef __cplusplus +extern "C" { +#endif + +NSData *TGGZipData(NSData *data, float level); +NSData *TGGUnzipData(NSData *data); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/TelegramUI/GZip.m b/TelegramUI/GZip.m new file mode 100644 index 0000000000..811c408e5c --- /dev/null +++ b/TelegramUI/GZip.m @@ -0,0 +1,79 @@ +#import "GZip.h" + +#import + +bool TGIsGzippedData(NSData *data) { + const UInt8 *bytes = (const UInt8 *)data.bytes; + return (data.length >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b); +} + +NSData *TGGZipData(NSData *data, float level) { + if (data.length == 0 || TGIsGzippedData(data)) { + return data; + } + + z_stream stream; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.avail_in = (uint)data.length; + stream.next_in = (Bytef *)(void *)data.bytes; + stream.total_out = 0; + stream.avail_out = 0; + + static const NSUInteger ChunkSize = 16384; + + NSMutableData *output = nil; + int compression = (level < 0.0f) ? Z_DEFAULT_COMPRESSION : (int)(roundf(level * 9)); + if (deflateInit2(&stream, compression, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) == Z_OK) { + output = [NSMutableData dataWithLength:ChunkSize]; + while (stream.avail_out == 0) { + if (stream.total_out >= output.length) { + output.length += ChunkSize; + } + stream.next_out = (uint8_t *)output.mutableBytes + stream.total_out; + stream.avail_out = (uInt)(output.length - stream.total_out); + deflate(&stream, Z_FINISH); + } + deflateEnd(&stream); + output.length = stream.total_out; + } + + return output; +} + +NSData *TGGUnzipData(NSData *data) +{ + if (data.length == 0 || !TGIsGzippedData(data)) { + return data; + } + + z_stream stream; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.avail_in = (uint)data.length; + stream.next_in = (Bytef *)data.bytes; + stream.total_out = 0; + stream.avail_out = 0; + + NSMutableData *output = nil; + if (inflateInit2(&stream, 47) == Z_OK) { + int status = Z_OK; + output = [NSMutableData dataWithCapacity:data.length * 2]; + while (status == Z_OK) { + if (stream.total_out >= output.length) { + output.length += data.length / 2; + } + stream.next_out = (uint8_t *)output.mutableBytes + stream.total_out; + stream.avail_out = (uInt)(output.length - stream.total_out); + status = inflate (&stream, Z_SYNC_FLUSH); + } + if (inflateEnd(&stream) == Z_OK) { + if (status == Z_STREAM_END) { + output.length = stream.total_out; + } + } + } + + return output; +} diff --git a/TelegramUI/ItemListPlaceholderItem.swift b/TelegramUI/ItemListPlaceholderItem.swift new file mode 100644 index 0000000000..8b1d6c9a79 --- /dev/null +++ b/TelegramUI/ItemListPlaceholderItem.swift @@ -0,0 +1,205 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit + +class ItemListPlaceholderItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let text: String + let sectionId: ItemListSectionId + let style: ItemListStyle + let tag: ItemListItemTag? + + init(theme: PresentationTheme, text: String, sectionId: ItemListSectionId, style: ItemListStyle, tag: ItemListItemTag? = nil) { + self.theme = theme + self.text = text + self.sectionId = sectionId + self.style = style + self.tag = tag + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = ItemListPlaceholderItemNode() + let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? ItemListPlaceholderItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + let selectable = false +} + +private let textFont = Font.regular(13.0) + +class ItemListPlaceholderItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + + let textNode: TextNode + + private var item: ItemListPlaceholderItem? + + override var canBeSelected: Bool { + return false + } + + var tag: ItemListItemTag? { + return self.item?.tag + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.textNode = TextNode() + self.textNode.isUserInteractionEnabled = false + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.textNode) + } + + func asyncLayout() -> (_ item: ItemListPlaceholderItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTextLayout = TextNode.asyncLayout(self.textNode) + + let currentItem = self.item + + return { item, params, neighbors in + var updatedTheme: PresentationTheme? + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + let leftInset = 16.0 + params.leftInset + let rightInset = 16.0 + params.rightInset + + let textColor = item.theme.list.itemSecondaryTextColor + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.text, font: textFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let height: CGFloat = 34.0 + textLayout.size.height + + switch item.style { + case .plain: + itemBackgroundColor = item.theme.list.plainBackgroundColor + itemSeparatorColor = item.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: height) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: height) + insets = itemListNeighborsGroupedInsets(neighbors) + } + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + } + + let _ = textApply() + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + 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 + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + default: + bottomStripeInset = 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: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 17.0), size: textLayout.size) + } + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/TelegramUI/MultiplexedVideoNode.swift b/TelegramUI/MultiplexedVideoNode.swift index 7df6ecea6d..e6c36fbdde 100644 --- a/TelegramUI/MultiplexedVideoNode.swift +++ b/TelegramUI/MultiplexedVideoNode.swift @@ -142,7 +142,7 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { deinit { self.displayLink.invalidate() self.displayLink.isPaused = true - for(_, disposable) in statusDisposable { + for(_, disposable) in self.statusDisposable { disposable.dispose() } for (_, value) in self.visibleLayers { diff --git a/TelegramUI/OverlayPlayerControllerNode.swift b/TelegramUI/OverlayPlayerControllerNode.swift index 97d4025a53..424c431fcd 100644 --- a/TelegramUI/OverlayPlayerControllerNode.swift +++ b/TelegramUI/OverlayPlayerControllerNode.swift @@ -55,8 +55,30 @@ 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 }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _, _ in }, navigationController: { + }, openPeer: { _, _, _ in + }, openPeerMention: { _ in + }, openMessageContextMenu: { _, _, _, _ in + }, navigateToMessage: { _, _ in + }, clickThroughMessage: { + }, toggleMessagesSelection: { _, _ in + }, sendMessage: { _ in + }, sendSticker: { _, _ in + }, sendGif: { _ in + }, requestMessageActionCallback: { _, _, _ in + }, requestMessageActionUrlAuth: { _, _, _ in + }, activateSwitchInline: { _, _ in + }, openUrl: { _, _, _ in + }, shareCurrentLocation: { + }, shareAccountContact: { + }, sendBotCommand: { _, _ in + }, openInstantPage: { _, _ in + }, openWallpaper: { _ in + }, openHashtag: { _, _ in + }, updateInputState: { _ in + }, updateInputMode: { _ in + }, openMessageShareMenu: { _ in + }, presentController: { _, _ in + }, navigationController: { return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in @@ -76,8 +98,7 @@ final class OverlayPlayerControllerNode: ViewControllerTracingNode, UIGestureRec }, seekToTimecode: { _, _, _ in }, requestMessageUpdate: { _ in }, cancelInteractiveKeyboardGestures: { - }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, - pollActionState: ChatInterfacePollActionState()) + }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState()) self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) diff --git a/TelegramUI/PasscodeEntryController.swift b/TelegramUI/PasscodeEntryController.swift index 148c87be91..837ba5cb2e 100644 --- a/TelegramUI/PasscodeEntryController.swift +++ b/TelegramUI/PasscodeEntryController.swift @@ -197,14 +197,7 @@ final public class PasscodeEntryController: ViewController { self.controllerNode.activateInput() if self.arguments.animated { - let iconFrame = self.arguments.lockIconInitialFrame() - if !iconFrame.isEmpty { - Queue.mainQueue().after(0.5) { - serviceSoundManager.playLockSound() - } - } - - self.controllerNode.animateIn(iconFrame: iconFrame, completion: { [weak self] in + self.controllerNode.animateIn(iconFrame: self.arguments.lockIconInitialFrame(), completion: { [weak self] in self?.presentationCompleted?() }) } else { diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 502f078f98..88bdda722f 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -182,6 +182,7 @@ public class PeerMediaCollectionController: TelegramController { },sendSticker: { _, _ in }, sendGif: { _ in }, requestMessageActionCallback: { _, _, _ in + }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, external in self?.openUrl(url, external: external ?? false) diff --git a/TelegramUI/RecentSessionsController.swift b/TelegramUI/RecentSessionsController.swift index 784e5a1c7a..f8e3da7319 100644 --- a/TelegramUI/RecentSessionsController.swift +++ b/TelegramUI/RecentSessionsController.swift @@ -647,5 +647,6 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont controller.present(c, in: .window(.root), with: p) } } + return controller } diff --git a/TelegramUI/StickerResources.swift b/TelegramUI/StickerResources.swift index ae285b70d2..b0fa1bde4d 100644 --- a/TelegramUI/StickerResources.swift +++ b/TelegramUI/StickerResources.swift @@ -97,7 +97,6 @@ private func chatMessageStickerDatas(postbox: Postbox, file: TelegramMediaFile, private func chatMessageStickerPackThumbnailData(postbox: Postbox, representation: TelegramMediaImageRepresentation, synchronousLoad: Bool) -> Signal { let resource = representation.resource - let maybeFetched = postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedStickerAJpegRepresentation(size: CGSize(width: 160.0, height: 160.0)), complete: false, fetch: false, attemptSynchronously: synchronousLoad) return maybeFetched @@ -131,7 +130,21 @@ private func chatMessageStickerPackThumbnailData(postbox: Postbox, representatio } } -func chatMessageAnimationData(postbox: Postbox, fileReference: FileMediaReference, synchronousLoad: Bool) -> Signal<(Data?, Bool), NoError> { +func chatMessageAnimationData(postbox: Postbox, fileReference: FileMediaReference, synchronousLoad: Bool) -> Signal { + let maybeFetched = postbox.mediaBox.cachedResourceRepresentation(fileReference.media.resource, representation: CachedAnimatedStickerRepresentation(), pathExtension: "mp4", complete: false, fetch: false, attemptSynchronously: synchronousLoad) + + return maybeFetched + |> take(1) + |> mapToSignal { maybeData in + if maybeData.complete { + return .single(maybeData) + } else { + return postbox.mediaBox.cachedResourceRepresentation(fileReference.media.resource, representation: CachedAnimatedStickerRepresentation(), pathExtension: "mp4", complete: false) + } + } +} + +func chatMessageAnimatedStrickerBackingData(postbox: Postbox, fileReference: FileMediaReference, synchronousLoad: Bool) -> Signal<(Data?, Bool), NoError> { let resource = fileReference.media.resource let maybeFetched = postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) @@ -144,8 +157,8 @@ func chatMessageAnimationData(postbox: Postbox, fileReference: FileMediaReferenc return .single((loadedData, true)) } else { let fullSizeData = postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) - |> map { next -> (Data?, Bool) in - return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) + |> map { next -> (Data?, Bool) in + return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } return fullSizeData } diff --git a/TelegramUI/TelegramUIPrivate/module.modulemap b/TelegramUI/TelegramUIPrivate/module.modulemap index 1c26f10ede..84f9825481 100644 --- a/TelegramUI/TelegramUIPrivate/module.modulemap +++ b/TelegramUI/TelegramUIPrivate/module.modulemap @@ -22,6 +22,7 @@ module TelegramUIPrivateModule { header "../EDSunriseSet.h" header "../TGBridgeAudioDecoder.h" header "../TGBridgeAudioEncoder.h" + header "../GZip.h" private header "../../third-party/libjpeg-turbo/turbojpeg.h" private header "../../third-party/libjpeg-turbo/jpeglib.h" }