From 4925af69e2afd273b0fc81274eb788b5182c152e Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Wed, 19 Apr 2017 23:25:48 +0300 Subject: [PATCH] Add Calls::Panel and handle incoming calls. --- Telegram/Resources/colors.palette | 12 + Telegram/Resources/icons/call_answer.png | Bin 0 -> 456 bytes Telegram/Resources/icons/call_answer@2x.png | Bin 0 -> 1205 bytes Telegram/Resources/icons/call_discard.png | Bin 0 -> 405 bytes Telegram/Resources/icons/call_discard@2x.png | Bin 0 -> 736 bytes .../Resources/icons/call_record_active.png | Bin 0 -> 412 bytes .../Resources/icons/call_record_active@2x.png | Bin 0 -> 800 bytes .../Resources/icons/call_record_muted.png | Bin 0 -> 459 bytes .../Resources/icons/call_record_muted@2x.png | Bin 0 -> 843 bytes Telegram/Resources/icons/call_shadow_left.png | Bin 0 -> 100 bytes .../Resources/icons/call_shadow_left@2x.png | Bin 0 -> 125 bytes Telegram/Resources/icons/call_shadow_top.png | Bin 0 -> 103 bytes .../Resources/icons/call_shadow_top@2x.png | Bin 0 -> 127 bytes .../Resources/icons/call_shadow_top_left.png | Bin 0 -> 295 bytes .../icons/call_shadow_top_left@2x.png | Bin 0 -> 559 bytes Telegram/SourceFiles/base/openssl_help.h | 146 ++++++ Telegram/SourceFiles/base/weak_unique_ptr.h | 8 +- Telegram/SourceFiles/calls/calls.style | 80 ++++ Telegram/SourceFiles/calls/calls_call.cpp | 423 +++++++++++++----- Telegram/SourceFiles/calls/calls_call.h | 81 +++- Telegram/SourceFiles/calls/calls_instance.cpp | 63 ++- Telegram/SourceFiles/calls/calls_instance.h | 9 +- Telegram/SourceFiles/calls/calls_panel.cpp | 320 +++++++++++++ Telegram/SourceFiles/calls/calls_panel.h | 89 ++++ Telegram/SourceFiles/core/utils.h | 4 + Telegram/SourceFiles/messenger.cpp | 9 + Telegram/SourceFiles/messenger.h | 1 + Telegram/SourceFiles/structs.h | 3 +- Telegram/SourceFiles/ui/widgets/widgets.style | 5 + Telegram/gyp/Telegram.gyp | 1 + Telegram/gyp/telegram_sources.txt | 3 + 31 files changed, 1101 insertions(+), 156 deletions(-) create mode 100644 Telegram/Resources/icons/call_answer.png create mode 100644 Telegram/Resources/icons/call_answer@2x.png create mode 100644 Telegram/Resources/icons/call_discard.png create mode 100644 Telegram/Resources/icons/call_discard@2x.png create mode 100644 Telegram/Resources/icons/call_record_active.png create mode 100644 Telegram/Resources/icons/call_record_active@2x.png create mode 100644 Telegram/Resources/icons/call_record_muted.png create mode 100644 Telegram/Resources/icons/call_record_muted@2x.png create mode 100644 Telegram/Resources/icons/call_shadow_left.png create mode 100644 Telegram/Resources/icons/call_shadow_left@2x.png create mode 100644 Telegram/Resources/icons/call_shadow_top.png create mode 100644 Telegram/Resources/icons/call_shadow_top@2x.png create mode 100644 Telegram/Resources/icons/call_shadow_top_left.png create mode 100644 Telegram/Resources/icons/call_shadow_top_left@2x.png create mode 100644 Telegram/SourceFiles/base/openssl_help.h create mode 100644 Telegram/SourceFiles/calls/calls.style create mode 100644 Telegram/SourceFiles/calls/calls_panel.cpp create mode 100644 Telegram/SourceFiles/calls/calls_panel.h diff --git a/Telegram/Resources/colors.palette b/Telegram/Resources/colors.palette index 787617cba..370c2fbb2 100644 --- a/Telegram/Resources/colors.palette +++ b/Telegram/Resources/colors.palette @@ -507,3 +507,15 @@ mediaviewTransparentFg: #cccccc; // another transparent filling part // notification notificationBg: windowBg; // custom notification window background + +// calls +callBg: #26282cf2; +callNameFg: #ffffff; +callFingerprintBg: #00000066; +callStatusFg: #aaabac; +callIconFg: #ffffff; +callAnswerBg: #64c15b; +callAnswerRipple: #52b149; +callHangupBg: #d75a5a; +callHangupRipple: #c04646; +callMuteRipple: #ffffff12; diff --git a/Telegram/Resources/icons/call_answer.png b/Telegram/Resources/icons/call_answer.png new file mode 100644 index 0000000000000000000000000000000000000000..f5a9ceb7329b3844598e6db730463b754b85c8c9 GIT binary patch literal 456 zcmV;(0XP1MP)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv0004xNkl<ZNQvE* zF|Wa37==$=A+%kJk&=kTV6m7D#9E7g;V0b0Y!r!v@N@b}*tPKw41~ys<lKp<l8d^_ zJ;@o~<m>zNyl=l$0YLnrLVwx(p8|vskWyYenM|N*S|A9oEv4kOY};lom*efZXYEU{ zTCF^FDwRS!9)J7oeE`>WJ#{*rhH09g15Bq=PaO`210?`%W>&3MJv$f-1~d%g?H%|4 zi9~{q<6N4nsv4yE{U!JNeE`5_v%zk+3p5^I0su5kldkJ>y<W3cs|8AM7XW~kWpTgX zQwYIAq3|}~3&8iiSdQZ$5{aPIYN1#x!nSSC9np0imSrKE%_16&LRD3qPA41=hu=qZ zwyx{!^?D@9*=)voy-om(#bO+dMwc&bxm;2R!SQ(f{D{uhG>x53ha|b(ZrSa2IUEl8 zNG}5bFcb>0*=%yLSUhY@l2Xd|0B2`18MfQ)%RRZ(@p$w-qns(vN>LOThJjM4ghr!* ya=DCrK96KFiTQkvN~PjG|0>{B{lj18r}YExsHWP4P`tqa0000<MNUMnLSTZZr_sp( literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/call_answer@2x.png b/Telegram/Resources/icons/call_answer@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d6eb7c010c9437be2876f6187ec32f8dd5e9be33 GIT binary patch literal 1205 zcmV;m1WNmfP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm000DhNkl<ZXo2mS zJ5MrO6o%g!9R~#>0wQ=L5W>Yv)EFp?u`ya`VWWl7SlH1R3xB|grG-8I27iLl-gu2B zN&*<MFeo7iLAeaGPT}!OIQp>%1v%e|PqHg!R^Er%VXfJ400@6De7ysFl^=n6^CM7i zegx_bQ{eLQ5|Si=h~6z(mXXP15C{Y?Ffi~Pc|U~8ElHAyMKl^Euh&bWD3+~Ni^Y5b zJkQ=`EiEn3>2wf9k^Oa58D=^n_N2k{JX9(b1VN}us7hc8$g<2VA_xMEMk5*<8><qk z5|{$HT#i{p5CqumcBoXUs)VWpra(HKW)=|y0WOydYPA|oO-)q^rWB??JRWBk;dwZn zPG~fmZ{J2tflw%<sF1;6fJ&tT$8lc{RS8UiU@(Y6p};J3cXx+GB7tJDSe0N(VG3Md zUo)RVBoe{)_BLX%7_!-H)xs%(DR6UhgTuqa-+ms6M6kKJiKC+<JU%{FHJnn)wrZ!- zNq)bda=F~Q5h4@{(c<DFSuB>ij1m8=@)wv)CK?|fr}OjklJK!ujMmrJsjI82Ucr^k zLa9^=(P$KVdwV5andf;VlgV%2^Z%v+z+dGqkR%DREaUn489tv6?d|OVfad0ANRou( z<6}HLJbXjck6{V`Kt7*GE|)`ldpo>dFL<5@$8oUPYzT!yxWB(gDwV2z+7D19004zT z0jX39UauEsvl+na2;p|Sae8`+$H&LIY$wG6k|ZIY&m)`7Vr*>e*WlLH)&`5kg0r(T zB$G)+O|P0L764u+(|9}%tyYWC(a~RHgVkz<Mx#M67{t@l6C_Du))GY#EiEk&1OXh! zeY%5JB=GhQe0O&Tm&*m4%?1GAI1V0<2hGjR2#3Q+r_*Jhgu!5d&1Qqs>4aXdheo4; zTCIjkrGhAm5Cj24q)4hF0RSi#i^%12xV^o_@bEB9CKG_ypVj{Ue(3dj+}_?IlgZ%a z<>lS|-QC@on3%xU))tnRmoYUpg`uG#Sglr=OeW~{dYH{-=yW>hbUH=d{ZkFZXf)FN z{5(Y>k&*@{NfI3#9MJ6SEOmExm)v8qSZH;1m2PfsO71BZi<He~DV<JJB9WkYJWkPQ zlmdak*E?~!)!EreD=RA$4u?wu$+Aq5NQBnb)@Wd0fV#T6sHdlgmX?;@36~?2$<$T= z0GZ8ZT3A@1i;D}AWw|84^Yb(9?(UM`@28E8jd!9-sJXz~cAB1^rv3eWDijLuLd4^7 zdU$xKIQ+K>0HD^^RvH-@p`D!_N+c4W3R_nJ03-+kIUEl1`~7rsazgohzUBhln?*J0 z=;(mM;lSM79A;)_;PH4Ao$!4G>m9o$0>EphH5d$Vxm=i?oyFwjB>MXLDn53po4^|y zjRppT0lmGw7#tjg*XxDP=YzxHV9FGWMdmS0%>{thcL%Lj3!P2}v)K&0-43VI3Aft~ zhr<D*(Fm<p3sDr&(9i&m;~>j2uCA^yI5=2;fm-ta|A*$k&W}L7`4Ol$f4soo)AnM) TB`juJ00000NkvXXu0mjf!sjcP literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/call_discard.png b/Telegram/Resources/icons/call_discard.png new file mode 100644 index 0000000000000000000000000000000000000000..312a8be7116e84bc4e1a1c52a31b961715336035 GIT binary patch literal 405 zcmV;G0c!q<P)<h;3K|Lk000e1NJLTq000;O000mO1^@s63?#pm00048Nkl<ZIE~#@ zF^a-a6r3N0kisf;3L%1p*lCq6AP5@c5d@JSimk1cp2AZIqJ@oESO~!bSSVh=LL_3w zV$n4QQP@qfJ22H}X5RmA_yPbCA*3kH|3^Ul)q$pID3wYu3<KG07Mi9Zkw^dl4u=DF zyB*f+HRkg<Mxzn-`~Axh-YA4%p-|vtGNJGL{1m?Lb37i?G))R2_~>~LNT<`B&1M1N zVzHp(ILzg8R8^IdBrz6?QB_su^LaL#O)i(qfMz<KGL=dN0~(D6Pp4C8vU<J#JWYtL zRx6}C9*?ZmY6QS|zu!l<+x^iZpP?uUhQlF}$t0w9yZx)?kLYwdK($)s`FsvtbF<k{ z*Y&qgl*wec-EKpA&+}+m*4GYf+rEcfE*JKCJ<78D*8E0AQ8*Y3?#=T&kCjR#*doia zxLU1PEEYdDex_~PbX}L_a{0UZx0I;x_y3|R{gPc17{5ai00000NkvXXu0mjfz5ums literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/call_discard@2x.png b/Telegram/Resources/icons/call_discard@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8fffeb4128c24c6eca35f74eae29aa62c4b33d28 GIT binary patch literal 736 zcmV<60w4W}P)<h;3K|Lk000e1NJLTq001xm001Be1^@s6RMh(%0007~Nkl<ZScUDE zze{pa7{|Xy19Ql5Xb8+9BDhFhhcIbzYVa?}h@yX?#ZeFm?$FR2gwYVu5d8y9k%Kr0 zN<yJf4i!q}aQy+=_Z@oWMPIL7f7C7T3(t1WbH1PRJolV)1pwj~%Ve~HK{Wy}S|b3X zH3BeNV}ndKo6V3U2_BCJ3kwUFpPz@*>BQ936ecGpVX;^M0JT~TrBVrnLIL;p_qe^i zMJkm-JRV0TlfnD@`#=!vC2TeuH#RnSc6P>>mlqDIVzJ26(^IamuXAEzq9-qZ<MTm6 z2ztF<9v>gGTrLl6T1%>`^62P@9*<`@073}*{eH$`u`e^NrK_teuCA_92+@6Wn-uMG zyWQB|--plV>)L;BZ*RE1zD6REz|GAKve_&iA0P4h`ifeu1^}>FEU??{n4X@-?CdOD zE*F-Tmaww20=wPbHUH)1C3bdpkV>UA$FvYxtyb>s?Xl5lblstchX;DSUYgD3e)^UG zXfm0&yu8eOKHqh%dc96XQ8+$6-o1rnS!O1a(N*F^BGI?;_DCj^y4Fmm({#C9odGsC zH(9M#btN(ifF#T1G6R9YzxhH4Mj{d235*74DijJ)2*EK$Q4kCUH9wG_s&F{`aZ&ct zHb1EX<VQQ1a=BbzJ3w|keZN(AcXu$Yt*xP0EcQG(7K;tdNI$urpPvy31ONbBTwG+K zP|#I2RaLpWyQ_63_Z3#Fm5QRUQmN?j@bvUVNs?NRQ9Tgn=jWW6nHie#_Ha6#ytugN z#eC~UrUjv@Dz~<_K7E%ABZLs#-rlCFs(Q?~0(64N=kt_h`OA&Bhb+r2PYez7{{sM^ zBuP9uIpN{qAsr6Kx0`Q<!{Ok;!2zSuDCg$pI#;Yo@muxl|A|KcMr#CMwEh5*r`7CK S6KdE10000<MNUMnLSTZ+Dpzm- literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/call_record_active.png b/Telegram/Resources/icons/call_record_active.png new file mode 100644 index 0000000000000000000000000000000000000000..4659bbb6c867209b1b34b08c808ba6e3e32dda8f GIT binary patch literal 412 zcmV;N0b~A&P)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv0004FNkl<ZNQu={ zJC4FI6r325P@tRv(L#|XAmt7@Ln_XIL`^}76qG6zoPvfcq(jsZePUA(s@Vo2?Pl4S zpOhWxNw)m_MsMsl1_0uN5T7irP0;Cd5JeG`Qjk(&wOYZlENv3jMHEH+i7*Un?_Hxl zNhwt!$n#u_z__ve*&=^WF$|+to~IxA9_iZ%bkW=nnk`XL6xeRJ<z6XO$;q;;T<>-} zoX=-{g2Uke$8peVwEzH{&8CvGUatWF?RFcE<6ytv-|eC@9JASsBpCz&0noB6hGED& z&zWTzm&+wh(<A^+CKHn6bUM|aWZSlRJRT{f<Y+XyJEflI@pL*dO;dKe-N&qAJRVai z#iA%UpU>Ir^~$q;zt6>D!OP`BDJ5Okea?%9!y%I-Dg8%E`OTM-B;jB%c+KpD5cGYY zaU7S0<2a`8`xHXF$Lz}Aa2+lkle?k0UZjhMBzgw_U!pIGE>l2Piw}wb0000<MNUMn GLSTYD(6{vf literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/call_record_active@2x.png b/Telegram/Resources/icons/call_record_active@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8ce748f9c971094f756bdd99346b1de56d5630a7 GIT binary patch literal 800 zcmV+*1K<3KP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0008#Nkl<ZXo2mR z&r2#%6vvN`awZKb-H4*D#ig|9Z)nrXRkVws1WBQt(4Wwz{~$Lm{0CaNC@Rz$DKy&# zL4hbjFpe$!zQubEPB-g1UY`zczHs3(_uO;N$1^`X*F{7i8|&A+6PQ*Gz`P|KK$d07 zX0xQKD(SjTx~`L=D3r-$NS0;M;fUzZLo8%OA`vti4UAL0UPm|_78dXC5aURe<!_T8 zg^(m^@c^>f?2pM0b$oohcmS%ZasyPWRbc?GA;q-Pb)6(h;<D}adgSqVM1&<_0KjB! zTrQW0u=Dv2ObYv*oDZ25*}ej{2M{Tz0#;;u0FiPkU`4hE5GihekB<)mKmh3Q@Q_*W z_xs#IHBDo#rPFD{|0KS+0ov`hF%JX+%zC|E=MMUnZ7>)#=B-wX+ZQ)Lv)MG}i9~`~ zzqq*I4w}#BnQNmypPrt$eGzds4yUK5Mnj@$8rIg<OzV;);o;$dtC?9^S~BhR`~B$m z`-V*}mlNNS#p7{PA}1#&j7=yM!u|dI*yI&OK_C!d_T_RplU*zpn@vkuDwT{N-EJ45 zP>8XSBnjzs8kI@~nx;Y1G?dF_q*5s?FE2Cp(P$L!@9&1+%gf72+e&`pt*tE#1_LA5 z&CLx~R#q(cvtF+kl}g3%Hy8}Cxw$zz03yQq`MIgwrBVqS8ygcR@Avz0b#-O(b#``U z<%gJTgDlIqy}dOBetv$&_VzZnkDZ+zyu7@ad|Y2&!|is@9)O78^ZB4C3iFWO-Q8h- zfB(zVw`eqqgM$N9t5s%Sxm?Ej`nuIy(c1mHy1I&DvB)GdRJ+~A<KrWm%_cgX&RDxb zp#ZPfYc+Z60K>7jw}-d4x3LM1)9dRic6WEDiY)|yh~V*fkV>UctJQu?Tvb&Z9UV>e z_7V;-I>O;FlF1~Kcruxs#mTg|qs7m{r~mw-PU1f6|LF%7RSv+sWqW{`y@mdjY+nHf eVBT^7=B?ka-YosA6<DtT0000<MNUMnLSTZLaD}k| literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/call_record_muted.png b/Telegram/Resources/icons/call_record_muted.png new file mode 100644 index 0000000000000000000000000000000000000000..0747c8afa4b5c00fcfff75f64be02c363e4bc195 GIT binary patch literal 459 zcmV;+0W|)JP)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv0004!Nkl<ZNQuqW zJ&uAv6bJApYH*103?4vR?M*B>gSW7-_XH3s4&X;cs6_Ar7J3L9E11y&tN&KRF0hNB znCwfYm>KeZ$$M`?gNQ&M4CRx_O9P!whb+q?Ap~*G$uvz;Rkbt%xu(-89LE7;3_s5; z%R)8`!$^Hou)a5r;{X6W&r2afh#U|_QFVYZ2G8?y$ZEC90RXB4e<96g6YKT59w3YC z_j@qL&}y}E$jd+xve|6v0g90Ac3Tf15gA$JWgw1Y>~_0k&pA&J-}jTo%lpIOfcyPk z9yp)RxL&VG64NwO&6Nldq1|pH3_~1`$MV2pu>b(fW-|~GR8@sx7>J?>kH;g0$myET z=jDN3uZPR!0?s)`qfxFWzVGMCrD<BVCXdHs2q6&1F_z0E`u%=#HyjQThGDK;m8WPh z7~pg|CAH_Ary72{-2ebV5ELQBW2PtybX|vS+eu>EwxR1fy4@~<Ab5t9nq?5tuP<2B zv}eeBKo)Ubw|+n(uIpklnWWAeQu$Q;kDvZK@dYkG0lOkgLDv8P002ovPDHLkV1n?I B#!UbK literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/call_record_muted@2x.png b/Telegram/Resources/icons/call_record_muted@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4d7d690e989ea96d3a5ff9be8903282a223d16a8 GIT binary patch literal 843 zcmV-R1GM~!P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0009LNkl<ZXo2n7 z%}VP)6bJC*U6?3RV^{5{UB;qI-S`SVfbLufE|dzDF5HPP;Ul>91q5ej7crGa>&MPb zRVso=3GoB}cX4lmF=>;SNz;M{28Pa@=KRRagtS9M1RCJ@lXd}d;}?*22nWcrOqEK7 zYPA|!mPM9jk*@2sy}eD6B#FKp5&hf5LRz6vK%>z>r#L@9$JEr6u<QMAod+3X;G9EM z)xa;3Ww~qdb{K{MNs^KWFvf6ma{~ZqHk*NnN~IE5ydC9oIe7r*+;!0)M6Ff}4bU_# zd4Q^_Xf~Un5te0z2AHPVn>`Q(p=nxZ0D#^Aq97DSiG=7GASpyo0ZAZw4iJq<1t1cU z8bA~xRe%UY>VUY2z5v8Q^bNp9e13jLLi81Yi16|8;aTV15IqOHy}dbwZEkLQ#@p?- z?`h+Q$Ye5}{H?7mC+GeBJv1QnE9KMElhe=Ta-Q)<qv4y!7$eR(sjBLH_VV)LnU~M! zo&MwFW7x97zY||wT{->v`FYRy!NEbtTtCG3SXfwa`j?lNVap<-khf}YZ_l~KrfFh) zeB3oYJUongz3#h=x-BOnLN=Sl>+7pCZ+CZB{2rK_n{ySjv$NxwGdVd4!!RNv6h(oq z>&~2Fv6##$^z}9K`T5!R7Y0d^P%f9DX&Ow^glU?d4ep1Sn3%xn=_!tmj*>ZrhzN^| zi>~v&TCHMaWF#=JSS;fH{yx;5n4O(X4nRcM-`{r?a(sLYMNxzye$b*N$g+%TwdyM5 z_VyM_OG{}Xq9<juS=Xks<K*N7Yiny@jCpbxV_08b$Hm1(sEwT{fQT?QHipB)Lr*bw zv|23~hJkv$j#jJHah?D~to4W0)m1z^JOmc#SFoVv6c0c|7#$r&sZ_$**;!!Wx~^km zV*|1*LseDW-QC4S#0PwB)6>&fUS9STzOu67``i$K5DT!|_vkivW@aW1B7HziM1KHm z#1A%h{{T86IOl@{AaXykudlC>&*ujVU?Vu^$mMcf=MCA4L5<)4iPMf>K-w|zfWMJu Vkz32R+aCY`002ovPDHLkV1k!LlCJ;& literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/call_shadow_left.png b/Telegram/Resources/icons/call_shadow_left.png new file mode 100644 index 0000000000000000000000000000000000000000..74864ad4b58577ca3a3d1856559ea719998b84c5 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^5<twz!3HFctjk*sq_jO<978x}=5`r!F(?QemhyV} zKYq>S&IOG=oClave$R06yz;H^)?a3(FUic47BRliXODFQYGd$p^>bP0l+XkK{<R)! literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/call_shadow_left@2x.png b/Telegram/Resources/icons/call_shadow_left@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6a0e6e4c537f3e8ab7f3005d58f347ea1afbd90f GIT binary patch literal 125 zcmeAS@N?(olHy`uVBq!ia0vp^20+Zj!3HGdHpD9eDHl%{#}J9BXM3&r8XQEJ+Rxwn ze<JVurdye+XD0|a9AH)8^klf<cFBNqTYIjjlhCb8)3#pwVQgLPnYVjeW!?U%_FL<g Z@XlJt_-eZ>=Q^Ny44$rjF6*2UngGhbD!Kpw literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/call_shadow_top.png b/Telegram/Resources/icons/call_shadow_top.png new file mode 100644 index 0000000000000000000000000000000000000000..653e0afa994e839c76ca58e1a20333f70799a448 GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^j6f{G!3HF)&rH7sr1U&p978x}=AL%sVld!1a-g^S z$A9tb-4}xPoY534U<$ZVck;}_Rb_df_8w5kT*`Q0HIwMytuLj4IvG4&{an^LB{Ts5 D!GI#Y literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/call_shadow_top@2x.png b/Telegram/Resources/icons/call_shadow_top@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..47c672ca3b028dbf40d89d5aa5825b95344fc3d6 GIT binary patch literal 127 zcmeAS@N?(olHy`uVBq!ia0vp^Oh9bF!3HF)SyndzDK}3S#}J9BdnXw3F(_~_dpG{K zj}$-JG121Wl{=0c91Uy?M>K>d+UmF(eVyYHI@zjormCo72uu0?l}USU$Sv8CG->+w bKRmJqnM^Htf2OejO=R$N^>bP0l+XkK@!lpq literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/call_shadow_top_left.png b/Telegram/Resources/icons/call_shadow_top_left.png new file mode 100644 index 0000000000000000000000000000000000000000..baba49364f09da2655e03782fd31be9da4594658 GIT binary patch literal 295 zcmV+?0oeYDP)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv0002)Nkl<ZNQtGC z;SPi_2!l76@Bhf>uNtfcakpfN-YxxTVa_qeZ)~hNXY1VB5$cMUfGE;=&cG6YgDi$! zop}W)fof;M5>SbnnS9UOJOG(EgxvjlXIOOe$pOU7Y`FXFS-E1J3!s|G!fUPJ?l%y- zQN=D6=6E2E4M+r4G07=H<?en7AII^4#JE}j<sd=;!f$|7eMlpUJ%C_z5dmssTTPBJ zFo=7RPEjV#pcqw*NC9LI)J5ZQj8y}|5xq89xltEwYl_%GbvI(=Z3kaK-EWbxAV<Y8 tRY<k0{5Ns-WtD*4s@s5ngtq`Y>jc4T2IpqmK|lZi002ovPDHLkV1fWgeZv3% literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/call_shadow_top_left@2x.png b/Telegram/Resources/icons/call_shadow_top_left@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0d9672fb51b6dd970b3413917c45f15c5453534d GIT binary patch literal 559 zcmV+~0?_@5P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0005@Nkl<ZXo2mT z;f{ka41){o{a^X+7gCAh*hvH2q_b4Px>Ec#O%U3)F~-mMV(o7mSk>o^n{M0IHrH(c zt0vX!+W^=NJ6C<y5<q0C`dxNh2>?y(_1ivWOh##dtVwi^=2E_!B?DxP+oO73kp#e2 z4jHciMSY$qfU-TjqteIbZvjxuN9PQR8D~Ht01G)XjLfSr89{)^JTg7T82i3UFbG8P zBLOH-s6`Mez)}*kL{&`BpAnFS$QTa+keGRPWYK5`U`c3Ao^AfDM57fzNv6m=1F-<! z0<i@R0qkJ5caixB>b%Y~@0pCiN;%*q?A(syxSU5@N&$#qXbwax?`@;g{0xD%R!1IO z09Y8K>f=R#K~y3T0(e5nX19+8IgX<h0QHASY#%TRh-cdNDdkjE`h6fuUK9y63h16G z05p-e>L?0cn?YD5$|(ILMtzm%!FinLi+rR2qKl$LR$*)<WA~&0cAwjGWea(w6_wk^ z$|!?F=DlcmV^_>m0G7}e!h?!T-%^DT1T)CYD-ac>--)9!tSJl727nAJhE$`dFQc|! zXjsIe)n18!44|4q$*s&)(JjA`Rn1g%0f@?`RmP-J1gdf)@Z<`vDz%0FB3Nq<Fu*$& xpah^Ac_L!BJO`dsdl`Ki<m>+{@elB}egPU73!TJ&h#UX_002ovPDHLkV1h57@K68% literal 0 HcmV?d00001 diff --git a/Telegram/SourceFiles/base/openssl_help.h b/Telegram/SourceFiles/base/openssl_help.h new file mode 100644 index 000000000..90639e241 --- /dev/null +++ b/Telegram/SourceFiles/base/openssl_help.h @@ -0,0 +1,146 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#pragma once + +#include <openssl/bn.h> +#include <openssl/sha.h> + +namespace openssl { + +class Context { +public: + Context() : _data(BN_CTX_new()) { + } + Context(const Context &other) = delete; + Context(Context &&other) : _data(base::take(other._data)) { + } + Context &operator=(const Context &other) = delete; + Context &operator=(Context &&other) { + _data = base::take(other._data); + return *this; + } + ~Context() { + if (_data) { + BN_CTX_free(_data); + } + } + + BN_CTX *raw() const { + return _data; + } + +private: + BN_CTX *_data = nullptr; + +}; + +class BigNum { +public: + BigNum() { + BN_init(raw()); + } + BigNum(const BigNum &other) : BigNum() { + *this = other; + } + BigNum &operator=(const BigNum &other) { + if (other.failed() || !BN_copy(raw(), other.raw())) { + _failed = true; + } + return *this; + } + ~BigNum() { + BN_clear_free(raw()); + } + + explicit BigNum(unsigned int word) : BigNum() { + setWord(word); + } + explicit BigNum(base::const_byte_span bytes) : BigNum() { + setBytes(bytes); + } + + void setWord(unsigned int word) { + if (!BN_set_word(raw(), word)) { + _failed = true; + } + } + void setBytes(base::const_byte_span bytes) { + if (!BN_bin2bn(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size(), raw())) { + _failed = true; + } + } + void setModExp(const BigNum &a, const BigNum &p, const BigNum &m, const Context &context = Context()) { + if (a.failed() || p.failed() || m.failed()) { + _failed = true; + } else if (a.isNegative() || p.isNegative() || m.isNegative()) { + _failed = true; + } else if (!BN_mod_exp(raw(), a.raw(), p.raw(), m.raw(), context.raw())) { + _failed = true; + } else if (isNegative()) { + _failed = true; + } + } + + bool isNegative() const { + return BN_is_negative(raw()); + } + + std::vector<gsl::byte> getBytes() const { + if (failed()) { + return std::vector<gsl::byte>(); + } + auto length = BN_num_bytes(raw()); + auto result = std::vector<gsl::byte>(length, gsl::byte()); + auto resultSize = BN_bn2bin(raw(), reinterpret_cast<unsigned char*>(result.data())); + t_assert(resultSize == length); + return result; + } + + BIGNUM *raw() { + return &_data; + } + const BIGNUM *raw() const { + return &_data; + } + + bool failed() const { + return _failed; + } + +private: + BIGNUM _data; + bool _failed = false; + +}; + +inline std::array<gsl::byte, SHA256_DIGEST_LENGTH> Sha256(base::const_byte_span bytes) { + auto result = std::array<gsl::byte, SHA256_DIGEST_LENGTH>(); + SHA256(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size(), reinterpret_cast<unsigned char*>(result.data())); + return result; +} + +inline std::array<gsl::byte, SHA_DIGEST_LENGTH> Sha1(base::const_byte_span bytes) { + auto result = std::array<gsl::byte, SHA_DIGEST_LENGTH>(); + SHA1(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size(), reinterpret_cast<unsigned char*>(result.data())); + return result; +} + +} // namespace openssl diff --git a/Telegram/SourceFiles/base/weak_unique_ptr.h b/Telegram/SourceFiles/base/weak_unique_ptr.h index ce0b7b8ce..b4dbac92d 100644 --- a/Telegram/SourceFiles/base/weak_unique_ptr.h +++ b/Telegram/SourceFiles/base/weak_unique_ptr.h @@ -24,7 +24,7 @@ namespace base { class enable_weak_from_this; -template <typename T, typename = std::enable_if_t<std::is_base_of<enable_weak_from_this, T>::value>> +template <typename T> class weak_unique_ptr; class enable_weak_from_this { @@ -42,12 +42,12 @@ public: } private: - template <typename Child, typename> + template <typename Child> friend class weak_unique_ptr; std::shared_ptr<enable_weak_from_this*> getGuarded() { if (!_guarded) { - _guarded = std::make_shared<enable_weak_from_this*>(this); + _guarded = std::make_shared<enable_weak_from_this*>(static_cast<enable_weak_from_this*>(this)); } return _guarded; } @@ -56,7 +56,7 @@ private: }; -template <typename T, typename> +template <typename T> class weak_unique_ptr { public: weak_unique_ptr() = default; diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style new file mode 100644 index 000000000..46f389802 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls.style @@ -0,0 +1,80 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +using "basic.style"; + +using "ui/widgets/widgets.style"; + +callWidth: 300px; +callHeight: 470px; +callShadow: Shadow { + left: icon {{ "call_shadow_left", windowShadowFg }}; + topLeft: icon {{ "call_shadow_top_left", windowShadowFg }}; + top: icon {{ "call_shadow_top", windowShadowFg }}; + topRight: icon {{ "call_shadow_top_left-flip_horizontal", windowShadowFg }}; + right: icon {{ "call_shadow_left-flip_horizontal", windowShadowFg }}; + bottomRight: icon {{ "call_shadow_top_left-flip_vertical-flip_horizontal", windowShadowFg }}; + bottom: icon {{ "call_shadow_top-flip_vertical", windowShadowFg }}; + bottomLeft: icon {{ "call_shadow_top_left-flip_vertical", windowShadowFg }}; + extend: margins(9px, 8px, 9px, 10px); + fallback: windowShadowFgFallback; +} + +callButton: IconButton { + width: 64px; + height: 64px; + + iconPosition: point(20px, 20px); + + rippleAreaPosition: point(8px, 8px); + rippleAreaSize: 48px; + ripple: defaultRippleAnimation; +} + +callAnswer: CallButton { + button: IconButton(callButton) { + icon: icon {{ "call_answer", callIconFg }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: callAnswerRipple; + } + } + bg: callAnswerBg; +} +callHangup: CallButton { + button: IconButton(callButton) { + icon: icon {{ "call_discard", callIconFg }}; + iconPosition: point(20px, 24px); + ripple: RippleAnimation(defaultRippleAnimation) { + color: callHangupRipple; + } + } + bg: callHangupBg; +} +callMuteToggle: IconButton(callButton) { + icon: icon {{ "call_record_active", callIconFg }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: callMuteRipple; + } +} +callUnmuteIcon: icon {{ "call_record_muted", callIconFg }}; + +callControlsTop: 84px; +callControlsSkip: 8px; +callMuteRight: 12px; diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index 745ddfbc0..42a5e719f 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -21,9 +21,10 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "calls/calls_call.h" #include "auth_session.h" +#include "mainwidget.h" #include "calls/calls_instance.h" +#include "base/openssl_help.h" -#include <openssl/bn.h> #include <openssl/rand.h> #include <openssl/sha.h> @@ -45,86 +46,195 @@ namespace { constexpr auto kMinLayer = 65; constexpr auto kMaxLayer = 65; // MTP::CurrentLayer? +constexpr auto kHangupTimeoutMs = 5000; // TODO read from server config using tgvoip::Endpoint; void ConvertEndpoint(std::vector<tgvoip::Endpoint> &ep, const MTPDphoneConnection &mtc) { - if (mtc.vpeer_tag.v.length() != 16) + if (mtc.vpeer_tag.v.length() != 16) { return; + } auto ipv4 = tgvoip::IPv4Address(std::string(mtc.vip.v.constData(), mtc.vip.v.size())); auto ipv6 = tgvoip::IPv6Address(std::string(mtc.vipv6.v.constData(), mtc.vipv6.v.size())); ep.push_back(Endpoint((int64_t)mtc.vid.v, (uint16_t)mtc.vport.v, ipv4, ipv6, EP_TYPE_UDP_RELAY, (unsigned char*)mtc.vpeer_tag.v.data())); } +std::vector<gsl::byte> ComputeModExp(const DhConfig &config, const openssl::BigNum &base, const std::array<gsl::byte, Call::kRandomPowerSize> &randomPower) { + using namespace openssl; + + BigNum resultBN; + resultBN.setModExp(base, BigNum(randomPower), BigNum(config.p)); + auto result = resultBN.getBytes(); + constexpr auto kMaxModExpSize = 256; + t_assert(result.size() <= kMaxModExpSize); + return result; +} + +std::vector<gsl::byte> ComputeModExpFirst(const DhConfig &config, const std::array<gsl::byte, Call::kRandomPowerSize> &randomPower) { + return ComputeModExp(config, openssl::BigNum(config.g), randomPower); +} + +std::vector<gsl::byte> ComputeModExpFinal(const DhConfig &config, base::const_byte_span first, const std::array<gsl::byte, Call::kRandomPowerSize> &randomPower) { + return ComputeModExp(config, openssl::BigNum(first), randomPower); +} + +constexpr auto kFingerprintDataSize = 256; +uint64 ComputeFingerprint(const std::array<gsl::byte, kFingerprintDataSize> &authKey) { + auto hash = openssl::Sha1(authKey); + return (gsl::to_integer<uint64>(hash[19]) << 56) + | (gsl::to_integer<uint64>(hash[18]) << 48) + | (gsl::to_integer<uint64>(hash[17]) << 40) + | (gsl::to_integer<uint64>(hash[16]) << 32) + | (gsl::to_integer<uint64>(hash[15]) << 24) + | (gsl::to_integer<uint64>(hash[14]) << 16) + | (gsl::to_integer<uint64>(hash[13]) << 8) + | (gsl::to_integer<uint64>(hash[12])); +} + } // namespace -Call::Call(gsl::not_null<Delegate*> delegate, gsl::not_null<UserData*> user) +Call::Call(gsl::not_null<Delegate*> delegate, gsl::not_null<UserData*> user, Type type) : _delegate(delegate) -, _user(user) { +, _user(user) +, _type(type) { + if (_type == Type::Outgoing) { + setState(State::Requesting); + } +} + +void Call::generateRandomPower(base::const_byte_span random) { + Expects(random.size() == _randomPower.size()); + memset_rand(_randomPower.data(), _randomPower.size()); + for (auto i = 0, count = int(_randomPower.size()); i != count; i++) { + _randomPower[i] ^= random[i]; + } +} + +void Call::start(base::const_byte_span random) { // Save config here, because it is possible that it changes between // different usages inside the same call. _dhConfig = _delegate->getDhConfig(); -} + t_assert(_dhConfig.g != 0); + t_assert(!_dhConfig.p.empty()); -void Call::generateSalt(base::const_byte_span random) { - Expects(random.size() == _salt.size()); - memset_rand(_salt.data(), _salt.size()); - for (auto i = 0, count = int(_salt.size()); i != count; i++) { - _salt[i] ^= random[i]; + generateRandomPower(random); + + if (_type == Type::Outgoing) { + startOutgoing(); + } else { + startIncoming(); } } -void Call::startOutgoing(base::const_byte_span random) { - generateSalt(random); - - BN_CTX* ctx = BN_CTX_new(); - BN_CTX_init(ctx); - BIGNUM i_g_a; - BN_init(&i_g_a); - BN_set_word(&i_g_a, _dhConfig.g); - BIGNUM tmp; - BN_init(&tmp); - BIGNUM saltBN; - BN_init(&saltBN); - BN_bin2bn(reinterpret_cast<const unsigned char*>(_salt.data()), _salt.size(), &saltBN); - BIGNUM pbytesBN; - BN_init(&pbytesBN); - BN_bin2bn(reinterpret_cast<const unsigned char*>(_dhConfig.p.data()), _dhConfig.p.size(), &pbytesBN); - BN_mod_exp(&tmp, &i_g_a, &saltBN, &pbytesBN, ctx); - auto g_a_length = BN_num_bytes(&tmp); - _g_a = std::vector<gsl::byte>(g_a_length, gsl::byte()); - BN_bn2bin(&tmp, reinterpret_cast<unsigned char*>(_g_a.data())); - constexpr auto kMaxGASize = 256; - if (_g_a.size() > kMaxGASize) { - auto slice = gsl::make_span(_g_a).subspan(1, kMaxGASize); - _g_a = std::vector<gsl::byte>(slice.begin(), slice.end()); +void Call::startOutgoing() { + _ga = ComputeModExpFirst(_dhConfig, _randomPower); + if (_ga.empty()) { + LOG(("Call Error: Could not compute mod-exp first.")); + setState(State::Failed); + return; } - BN_CTX_free(ctx); - + _gaHash = openssl::Sha256(_ga); auto randomID = rand_value<int32>(); - auto g_a_hash = std::array<gsl::byte, SHA256_DIGEST_LENGTH>(); - SHA256(reinterpret_cast<const unsigned char*>(_g_a.data()), _g_a.size(), reinterpret_cast<unsigned char*>(g_a_hash.data())); - request(MTPphone_RequestCall(_user->inputUser, MTP_int(randomID), MTP_bytes(g_a_hash), MTP_phoneCallProtocol(MTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p | MTPDphoneCallProtocol::Flag::f_udp_reflector), MTP_int(kMinLayer), MTP_int(kMaxLayer)))).done([this](const MTPphone_PhoneCall &result) { + setState(State::Requesting); + request(MTPphone_RequestCall(_user->inputUser, MTP_int(randomID), MTP_bytes(_gaHash), MTP_phoneCallProtocol(MTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p | MTPDphoneCallProtocol::Flag::f_udp_reflector), MTP_int(kMinLayer), MTP_int(kMaxLayer)))).done([this](const MTPphone_PhoneCall &result) { Expects(result.type() == mtpc_phone_phoneCall); auto &call = result.c_phone_phoneCall(); App::feedUsers(call.vusers); if (call.vphone_call.type() != mtpc_phoneCallWaiting) { - LOG(("API Error: Expected phoneCallWaiting in response to phone.requestCall()")); - failed(); + LOG(("Call Error: Expected phoneCallWaiting in response to phone.requestCall()")); + setState(State::Failed); return; } + + setState(State::Waiting); + if (_finishAfterRequestingCall) { + hangup(); + return; + } + auto &phoneCall = call.vphone_call.c_phoneCallWaiting(); _id = phoneCall.vid.v; _accessHash = phoneCall.vaccess_hash.v; + handleUpdate(call.vphone_call); }).fail([this](const RPCError &error) { - failed(); + setState(State::Failed); }).send(); } +void Call::startIncoming() { + setState(State::Ringing); +} + +void Call::answer() { + Expects(_type == Type::Incoming); + _gb = ComputeModExpFirst(_dhConfig, _randomPower); + if (_gb.empty()) { + LOG(("Call Error: Could not compute mod-exp first.")); + setState(State::Failed); + return; + } + + setState(State::ExchangingKeys); + request(MTPphone_AcceptCall(MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)), MTP_bytes(_gb), _protocol)).done([this](const MTPphone_PhoneCall &result) { + Expects(result.type() == mtpc_phone_phoneCall); + auto &call = result.c_phone_phoneCall(); + App::feedUsers(call.vusers); + if (call.vphone_call.type() != mtpc_phoneCallWaiting) { + LOG(("Call Error: Expected phoneCallWaiting in response to phone.acceptCall()")); + setState(State::Failed); + return; + } + + handleUpdate(call.vphone_call); + }).fail([this](const RPCError &error) { + setState(State::Failed); + }).send(); +} + +void Call::setMute(bool mute) { + _mute = mute; + if (_controller) { + _controller->SetMicMute(_mute); + } +} + +void Call::hangup() { + auto missed = (_state == State::Ringing || (_state == State::Waiting && _type == Type::Outgoing)); + auto reason = missed ? MTP_phoneCallDiscardReasonMissed() : MTP_phoneCallDiscardReasonHangup(); + finish(reason); +} + +void Call::decline() { + finish(MTP_phoneCallDiscardReasonBusy()); +} + bool Call::handleUpdate(const MTPPhoneCall &call) { switch (call.type()) { - case mtpc_phoneCallRequested: Unexpected("phoneCallRequested call inside an existing call handleUpdate()"); + case mtpc_phoneCallRequested: { + auto &data = call.c_phoneCallRequested(); + if (_type != Type::Incoming + || _id != 0 + || peerToUser(_user->id) != data.vadmin_id.v) { + Unexpected("phoneCallRequested call inside an existing call handleUpdate()"); + } + if (AuthSession::CurrentUserId() != data.vparticipant_id.v) { + LOG(("Call Error: Wrong call participant_id %1, expected %2.").arg(data.vparticipant_id.v).arg(AuthSession::CurrentUserId())); + setState(State::Failed); + return true; + + } + _id = data.vid.v; + _accessHash = data.vaccess_hash.v; + _protocol = data.vprotocol; + auto gaHashBytes = bytesFromMTP(data.vg_a_hash); + if (gaHashBytes.size() != _gaHash.size()) { + LOG(("Call Error: Wrong g_a_hash size %1, expected %2.").arg(gaHashBytes.size()).arg(_gaHash.size())); + setState(State::Failed); + return true; + } + base::copy_bytes(gsl::make_span(_gaHash), gaHashBytes); + } return true; case mtpc_phoneCallEmpty: { auto &data = call.c_phoneCallEmpty(); @@ -132,7 +242,7 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { return false; } LOG(("Call Error: phoneCallEmpty received.")); - failed(); + setState(State::Failed); } return true; case mtpc_phoneCallWaiting: { @@ -147,6 +257,9 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { if (data.vid.v != _id) { return false; } + if (_type == Type::Incoming && _state == State::ExchangingKeys) { + startConfirmedCall(data); + } } return true; case mtpc_phoneCallDiscarded: { @@ -154,7 +267,17 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { if (data.vid.v != _id) { return false; } - _delegate->callFinished(this, data.vreason); + if (data.is_need_debug()) { + auto debugLog = _controller ? _controller->GetDebugLog() : std::string(); + if (!debugLog.empty()) { + MTP::send(MTPphone_SaveCallDebug(MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)), MTP_dataJSON(MTP_string(debugLog)))); + } + } + if (data.has_reason() && data.vreason.type() == mtpc_phoneCallDiscardReasonBusy) { + setState(State::Busy); + } else { + setState(State::Ended); + } } return true; case mtpc_phoneCallAccepted: { @@ -162,7 +285,10 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { if (data.vid.v != _id) { return false; } - if (checkCallFields(data)) { + if (_type != Type::Outgoing) { + LOG(("Call Error: Unexpected phoneCallAccepted for an incoming call.")); + setState(State::Failed); + } else if (checkCallFields(data)) { confirmAcceptedCall(data); } } return true; @@ -172,69 +298,83 @@ bool Call::handleUpdate(const MTPPhoneCall &call) { } void Call::confirmAcceptedCall(const MTPDphoneCallAccepted &call) { + Expects(_type == Type::Outgoing); + // TODO check isGoodGaAndGb - - BN_CTX *ctx = BN_CTX_new(); - BN_CTX_init(ctx); - BIGNUM p; - BIGNUM i_authKey; - BIGNUM res; - BIGNUM salt; - BN_init(&p); - BN_init(&i_authKey); - BN_init(&res); - BN_init(&salt); - BN_bin2bn(reinterpret_cast<const unsigned char*>(_dhConfig.p.data()), _dhConfig.p.size(), &p); - BN_bin2bn(reinterpret_cast<const unsigned char*>(call.vg_b.v.constData()), call.vg_b.v.length(), &i_authKey); - BN_bin2bn(reinterpret_cast<const unsigned char*>(_salt.data()), _salt.size(), &salt); - - BN_mod_exp(&res, &i_authKey, &salt, &p, ctx); - BN_CTX_free(ctx); - auto realAuthKeyLength = BN_num_bytes(&res); - auto realAuthKeyBytes = QByteArray(realAuthKeyLength, Qt::Uninitialized); - BN_bn2bin(&res, reinterpret_cast<unsigned char*>(realAuthKeyBytes.data())); - - if (realAuthKeyLength > kAuthKeySize) { - memcpy(_authKey.data(), realAuthKeyBytes.constData() + (realAuthKeyLength - kAuthKeySize), kAuthKeySize); - } else if (realAuthKeyLength < kAuthKeySize) { - memset(_authKey.data(), 0, kAuthKeySize - realAuthKeyLength); - memcpy(_authKey.data() + (kAuthKeySize - realAuthKeyLength), realAuthKeyBytes.constData(), realAuthKeyLength); - } else { - memcpy(_authKey.data(), realAuthKeyBytes.constData(), kAuthKeySize); + auto computedAuthKey = ComputeModExpFinal(_dhConfig, byteVectorFromMTP(call.vg_b), _randomPower); + if (computedAuthKey.empty()) { + LOG(("Call Error: Could not compute mod-exp final.")); + setState(State::Failed); + return; } - unsigned char authKeyHash[SHA_DIGEST_LENGTH]; - SHA1(reinterpret_cast<const unsigned char*>(_authKey.data()), _authKey.size(), authKeyHash); + auto computedAuthKeySize = computedAuthKey.size(); + t_assert(computedAuthKeySize <= kAuthKeySize); + auto authKeyBytes = gsl::make_span(_authKey); + if (computedAuthKeySize < kAuthKeySize) { + base::set_bytes(authKeyBytes.subspan(0, kAuthKeySize - computedAuthKeySize), gsl::byte()); + base::copy_bytes(authKeyBytes.subspan(kAuthKeySize - computedAuthKeySize), computedAuthKey); + } else { + base::copy_bytes(authKeyBytes, computedAuthKey); + } + _keyFingerprint = ComputeFingerprint(_authKey); - _keyFingerprint = ((uint64)authKeyHash[19] << 56) - | ((uint64)authKeyHash[18] << 48) - | ((uint64)authKeyHash[17] << 40) - | ((uint64)authKeyHash[16] << 32) - | ((uint64)authKeyHash[15] << 24) - | ((uint64)authKeyHash[14] << 16) - | ((uint64)authKeyHash[13] << 8) - | ((uint64)authKeyHash[12]); - - request(MTPphone_ConfirmCall(MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)), MTP_bytes(_g_a), MTP_long(_keyFingerprint), MTP_phoneCallProtocol(MTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p | MTPDphoneCallProtocol::Flag::f_udp_reflector), MTP_int(kMinLayer), MTP_int(kMaxLayer)))).done([this](const MTPphone_PhoneCall &result) { + setState(State::ExchangingKeys); + request(MTPphone_ConfirmCall(MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)), MTP_bytes(_ga), MTP_long(_keyFingerprint), MTP_phoneCallProtocol(MTP_flags(MTPDphoneCallProtocol::Flag::f_udp_p2p | MTPDphoneCallProtocol::Flag::f_udp_reflector), MTP_int(kMinLayer), MTP_int(kMaxLayer)))).done([this](const MTPphone_PhoneCall &result) { Expects(result.type() == mtpc_phone_phoneCall); auto &call = result.c_phone_phoneCall(); App::feedUsers(call.vusers); if (call.vphone_call.type() != mtpc_phoneCall) { - LOG(("API Error: Expected phoneCall in response to phone.confirmCall()")); - failed(); + LOG(("Call Error: Expected phoneCall in response to phone.confirmCall()")); + setState(State::Failed); return; } + createAndStartController(call.vphone_call.c_phoneCall()); }).fail([this](const RPCError &error) { - failed(); + setState(State::Failed); }).send(); } +void Call::startConfirmedCall(const MTPDphoneCall &call) { + Expects(_type == Type::Incoming); + + auto firstBytes = bytesFromMTP(call.vg_a_or_b); + if (_gaHash != openssl::Sha256(firstBytes)) { + LOG(("Call Error: Wrong g_a hash received.")); + setState(State::Failed); + return; + } + + // TODO check isGoodGaAndGb + auto computedAuthKey = ComputeModExpFinal(_dhConfig, firstBytes, _randomPower); + if (computedAuthKey.empty()) { + LOG(("Call Error: Could not compute mod-exp final.")); + setState(State::Failed); + return; + } + + auto computedAuthKeySize = computedAuthKey.size(); + t_assert(computedAuthKeySize <= kAuthKeySize); + auto authKeyBytes = gsl::make_span(_authKey); + if (computedAuthKeySize < kAuthKeySize) { + base::set_bytes(authKeyBytes.subspan(0, kAuthKeySize - computedAuthKeySize), gsl::byte()); + base::copy_bytes(authKeyBytes.subspan(kAuthKeySize - computedAuthKeySize), computedAuthKey); + } else { + base::copy_bytes(authKeyBytes, computedAuthKey); + } + _keyFingerprint = ComputeFingerprint(_authKey); + + createAndStartController(call); +} + void Call::createAndStartController(const MTPDphoneCall &call) { if (!checkCallFields(call)) { return; } + setState(State::Established); + voip_config_t config; config.data_saving = DATA_SAVING_NEVER; config.enableAEC = true; @@ -250,10 +390,13 @@ void Call::createAndStartController(const MTPDphoneCall &call) { } _controller = std::make_unique<tgvoip::VoIPController>(); + if (_mute) { + _controller->SetMicMute(_mute); + } _controller->implData = static_cast<void*>(this); _controller->SetRemoteEndpoints(endpoints, true); _controller->SetConfig(&config); - _controller->SetEncryptionKey(reinterpret_cast<char*>(_authKey.data()), true); + _controller->SetEncryptionKey(reinterpret_cast<char*>(_authKey.data()), (_type == Type::Outgoing)); _controller->SetStateCallback([](tgvoip::VoIPController *controller, int state) { static_cast<Call*>(controller->implData)->handleControllerStateChange(controller, state); }); @@ -263,47 +406,52 @@ void Call::createAndStartController(const MTPDphoneCall &call) { void Call::handleControllerStateChange(tgvoip::VoIPController *controller, int state) { // NB! Can be called from an arbitrary thread! - Expects(controller == _controller.get()); + // Expects(controller == _controller.get()); This can be called from ~VoIPController()! Expects(controller->implData == static_cast<void*>(this)); switch (state) { case STATE_WAIT_INIT: { - DEBUG_LOG(("Call Info: State changed to Established.")); + DEBUG_LOG(("Call Info: State changed to WaitingInit.")); + setStateQueued(State::WaitingInit); } break; case STATE_WAIT_INIT_ACK: { - DEBUG_LOG(("Call Info: State changed to Established.")); + DEBUG_LOG(("Call Info: State changed to WaitingInitAck.")); + setStateQueued(State::WaitingInitAck); } break; case STATE_ESTABLISHED: { DEBUG_LOG(("Call Info: State changed to Established.")); + setStateQueued(State::Established); } break; case STATE_FAILED: { DEBUG_LOG(("Call Info: State changed to Failed.")); - failed(); + setStateQueued(State::Failed); } break; default: LOG(("Call Error: Unexpected state in handleStateChange: %1").arg(state)); } } -template <typename Type> -bool Call::checkCallCommonFields(const Type &call) { +template <typename T> +bool Call::checkCallCommonFields(const T &call) { auto checkFailed = [this] { - failed(); + setState(State::Failed); return false; }; if (call.vaccess_hash.v != _accessHash) { - LOG(("API Error: Wrong call access_hash.")); + LOG(("Call Error: Wrong call access_hash.")); return checkFailed(); } - if (call.vadmin_id.v != AuthSession::CurrentUserId()) { - LOG(("API Error: Wrong call admin_id %1, expected %2.").arg(call.vadmin_id.v).arg(AuthSession::CurrentUserId())); + auto adminId = (_type == Type::Outgoing) ? AuthSession::CurrentUserId() : peerToUser(_user->id); + auto participantId = (_type == Type::Outgoing) ? peerToUser(_user->id) : AuthSession::CurrentUserId(); + if (call.vadmin_id.v != adminId) { + LOG(("Call Error: Wrong call admin_id %1, expected %2.").arg(call.vadmin_id.v).arg(adminId)); return checkFailed(); } - if (call.vparticipant_id.v != peerToUser(_user->id)) { - LOG(("API Error: Wrong call participant_id %1, expected %2.").arg(call.vparticipant_id.v).arg(peerToUser(_user->id))); + if (call.vparticipant_id.v != participantId) { + LOG(("Call Error: Wrong call participant_id %1, expected %2.").arg(call.vparticipant_id.v).arg(participantId)); return checkFailed(); } return true; @@ -314,8 +462,8 @@ bool Call::checkCallFields(const MTPDphoneCall &call) { return false; } if (call.vkey_fingerprint.v != _keyFingerprint) { - LOG(("API Error: Wrong call fingerprint.")); - failed(); + LOG(("Call Error: Wrong call fingerprint.")); + setState(State::Failed); return false; } return true; @@ -325,7 +473,64 @@ bool Call::checkCallFields(const MTPDphoneCallAccepted &call) { return checkCallCommonFields(call); } -void Call::destroyController() { +void Call::setState(State state) { + if (_state != state) { + _state = state; + _stateChanged.notify(state, true); + + switch (_state) { + case State::WaitingInit: + case State::WaitingInitAck: + case State::Established: + _startTime = getms(true); + break; + case State::Ended: + _delegate->callFinished(this); + break; + case State::Failed: + _delegate->callFailed(this); + break; + case State::Busy: + _hangupByTimeoutTimer.call(kHangupTimeoutMs, [this] { setState(State::Ended); }); + // TODO play sound + break; + } + } +} + +void Call::finish(const MTPPhoneCallDiscardReason &reason) { + if (_state == State::Requesting) { + _hangupByTimeoutTimer.call(kHangupTimeoutMs, [this] { setState(State::Ended); }); + _finishAfterRequestingCall = true; + return; + } + if (_state == State::HangingUp || _state == State::Ended) { + return; + } + if (!_id) { + setState(State::Ended); + return; + } + + setState(State::HangingUp); + auto duration = _startTime ? static_cast<int>((getms(true) - _startTime) / 1000) : 0; + auto connectionId = _controller ? _controller->GetPreferredRelayID() : 0; + _hangupByTimeoutTimer.call(kHangupTimeoutMs, [this] { setState(State::Ended); }); + request(MTPphone_DiscardCall(MTP_inputPhoneCall(MTP_long(_id), MTP_long(_accessHash)), MTP_int(duration), reason, MTP_long(connectionId))).done([this](const MTPUpdates &result) { + // This could be destroyed by updates, so we set Ended after + // updates being handled, but in a guarded way. + InvokeQueued(this, [this] { setState(State::Ended); }); + App::main()->sentUpdatesReceived(result); + }).fail([this](const RPCError &error) { + setState(State::Ended); + }).send(); +} + +void Call::setStateQueued(State state) { + InvokeQueued(this, [this, state] { setState(state); }); +} + +Call::~Call() { if (_controller) { DEBUG_LOG(("Call Info: Destroying call controller..")); _controller.reset(); @@ -333,12 +538,4 @@ void Call::destroyController() { } } -void Call::failed() { - InvokeQueued(this, [this] { _delegate->callFailed(this); }); -} - -Call::~Call() { - destroyController(); -} - } // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index 350e255d2..bb6e274b7 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -22,6 +22,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "base/weak_unique_ptr.h" #include "mtproto/sender.h" +#include "base/timer.h" namespace tgvoip { class VoIPController; @@ -40,43 +41,97 @@ public: class Delegate { public: virtual DhConfig getDhConfig() const = 0; - virtual void callFinished(gsl::not_null<Call*> call, const MTPPhoneCallDiscardReason &reason) = 0; + virtual void callFinished(gsl::not_null<Call*> call) = 0; virtual void callFailed(gsl::not_null<Call*> call) = 0; }; - static constexpr auto kSaltSize = 256; + static constexpr auto kRandomPowerSize = 256; - Call(gsl::not_null<Delegate*> instance, gsl::not_null<UserData*> user); + enum class Type { + Incoming, + Outgoing, + }; + Call(gsl::not_null<Delegate*> delegate, gsl::not_null<UserData*> user, Type type); - void startOutgoing(base::const_byte_span random); + Type type() const { + return _type; + } + gsl::not_null<UserData*> user() const { + return _user; + } + + void start(base::const_byte_span random); bool handleUpdate(const MTPPhoneCall &call); + enum State { + WaitingInit, + WaitingInitAck, + Established, + Failed, + HangingUp, + Ended, + ExchangingKeys, + Waiting, + Requesting, + WaitingIncoming, + Ringing, + Busy, + }; + State state() const { + return _state; + } + base::Observable<State> &stateChanged() { + return _stateChanged; + } + void setMute(bool mute); + + void answer(); + void hangup(); + void decline(); + ~Call(); private: static constexpr auto kAuthKeySize = 256; + static constexpr auto kSha256Size = 32; - void generateSalt(base::const_byte_span random); + void finish(const MTPPhoneCallDiscardReason &reason); + void startOutgoing(); + void startIncoming(); + + void generateRandomPower(base::const_byte_span random); void handleControllerStateChange(tgvoip::VoIPController *controller, int state); void createAndStartController(const MTPDphoneCall &call); - void destroyController(); - template <typename Type> - bool checkCallCommonFields(const Type &call); + template <typename T> + bool checkCallCommonFields(const T &call); bool checkCallFields(const MTPDphoneCall &call); bool checkCallFields(const MTPDphoneCallAccepted &call); void confirmAcceptedCall(const MTPDphoneCallAccepted &call); + void startConfirmedCall(const MTPDphoneCall &call); + void setState(State state); + void setStateQueued(State state); - void failed(); - - DhConfig _dhConfig; gsl::not_null<Delegate*> _delegate; gsl::not_null<UserData*> _user; - std::vector<gsl::byte> _g_a; - std::array<gsl::byte, kSaltSize> _salt; + Type _type = Type::Outgoing; + State _state = State::WaitingInit; + bool _finishAfterRequestingCall = false; + base::Observable<State> _stateChanged; + TimeMs _startTime = 0; + base::DelayedCallTimer _hangupByTimeoutTimer; + bool _mute = false; + + DhConfig _dhConfig; + std::vector<gsl::byte> _ga; + std::vector<gsl::byte> _gb; + std::array<gsl::byte, kSha256Size> _gaHash; + std::array<gsl::byte, kRandomPowerSize> _randomPower; std::array<gsl::byte, kAuthKeySize> _authKey; + MTPPhoneCallProtocol _protocol; + uint64 _id = 0; uint64 _accessHash = 0; uint64 _keyFingerprint = 0; diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index 6a98c96f3..ce222b154 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -23,6 +23,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "mtproto/connection.h" #include "auth_session.h" #include "calls/calls_call.h" +#include "calls/calls_panel.h" namespace Calls { @@ -32,14 +33,32 @@ void Instance::startOutgoingCall(gsl::not_null<UserData*> user) { if (_currentCall) { return; // Already in a call. } + createCall(user, Call::Type::Outgoing); +} - _currentCall = std::make_unique<Call>(getCallDelegate(), user); - request(MTPmessages_GetDhConfig(MTP_int(_dhConfig.version), MTP_int(Call::kSaltSize))).done([this, call = base::weak_unique_ptr<Call>(_currentCall)](const MTPmessages_DhConfig &result) { - if (!call) { - DEBUG_LOG(("API Warning: call was destroyed before got dhConfig.")); - return; - } +void Instance::callFinished(gsl::not_null<Call*> call) { + if (_currentCall.get() == call) { + _currentCallPanel.reset(); + _currentCall.reset(); + } +} +void Instance::callFailed(gsl::not_null<Call*> call) { + if (_currentCall.get() == call) { + _currentCallPanel.reset(); + _currentCall.reset(); + } +} + +void Instance::createCall(gsl::not_null<UserData*> user, Call::Type type) { + _currentCall = std::make_unique<Call>(getCallDelegate(), user, type); + _currentCallPanel = std::make_unique<Panel>(_currentCall.get()); + refreshDhConfig(); +} + +void Instance::refreshDhConfig() { + Expects(_currentCall != nullptr); + request(MTPmessages_GetDhConfig(MTP_int(_dhConfig.version), MTP_int(Call::kRandomPowerSize))).done([this, call = base::weak_unique_ptr<Call>(_currentCall)](const MTPmessages_DhConfig &result) { auto random = base::const_byte_span(); switch (result.type()) { case mtpc_messages_dhConfig: { @@ -67,32 +86,22 @@ void Instance::startOutgoingCall(gsl::not_null<UserData*> user) { default: Unexpected("Type in messages.getDhConfig"); } - if (random.size() != Call::kSaltSize) { + if (random.size() != Call::kRandomPowerSize) { LOG(("API Error: dhConfig random bytes wrong size: %1").arg(random.size())); callFailed(call.get()); return; } - call->startOutgoing(random); + if (call) { + call->start(random); + } }).fail([this, call = base::weak_unique_ptr<Call>(_currentCall)](const RPCError &error) { if (!call) { DEBUG_LOG(("API Warning: call was destroyed before got dhConfig.")); return; } - callFailed(call.get()); }).send(); -} -void Instance::callFinished(gsl::not_null<Call*> call, const MTPPhoneCallDiscardReason &reason) { - if (_currentCall.get() == call) { - _currentCall.reset(); - } -} - -void Instance::callFailed(gsl::not_null<Call*> call) { - if (_currentCall.get() == call) { - _currentCall.reset(); - } } void Instance::handleUpdate(const MTPDupdatePhoneCall& update) { @@ -101,10 +110,18 @@ void Instance::handleUpdate(const MTPDupdatePhoneCall& update) { void Instance::handleCallUpdate(const MTPPhoneCall &call) { if (call.type() == mtpc_phoneCallRequested) { - if (_currentCall) { - // discard ? + auto &phoneCall = call.c_phoneCallRequested(); + auto user = App::userLoaded(phoneCall.vadmin_id.v); + if (!user) { + LOG(("API Error: User not loaded for phoneCallRequested.")); + } else if (user->isSelf()) { + LOG(("API Error: Self found in phoneCallRequested.")); + } + if (_currentCall || !user || user->isSelf()) { + request(MTPphone_DiscardCall(MTP_inputPhoneCall(phoneCall.vid, phoneCall.vaccess_hash), MTP_int(0), MTP_phoneCallDiscardReasonBusy(), MTP_long(0))).send(); } else { - // show call + createCall(user, Call::Type::Incoming); + _currentCall->handleUpdate(call); } } else if (!_currentCall || !_currentCall->handleUpdate(call)) { DEBUG_LOG(("API Warning: unexpected phone call update %1").arg(call.type())); diff --git a/Telegram/SourceFiles/calls/calls_instance.h b/Telegram/SourceFiles/calls/calls_instance.h index ed3846ffe..406fbecef 100644 --- a/Telegram/SourceFiles/calls/calls_instance.h +++ b/Telegram/SourceFiles/calls/calls_instance.h @@ -25,7 +25,9 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org namespace Calls { -class Instance : private MTP::Sender, private Call::Delegate { +class Panel; + +class Instance : private MTP::Sender, private Call::Delegate, private base::Subscriber { public: Instance(); @@ -42,14 +44,17 @@ private: DhConfig getDhConfig() const override { return _dhConfig; } - void callFinished(gsl::not_null<Call*> call, const MTPPhoneCallDiscardReason &reason) override; + void callFinished(gsl::not_null<Call*> call) override; void callFailed(gsl::not_null<Call*> call) override; + void createCall(gsl::not_null<UserData*> user, Call::Type type); + void refreshDhConfig(); void handleCallUpdate(const MTPPhoneCall &call); DhConfig _dhConfig; std::unique_ptr<Call> _currentCall; + std::unique_ptr<Panel> _currentCallPanel; }; diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp new file mode 100644 index 000000000..3d1cf7fa7 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -0,0 +1,320 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#include "calls/calls_panel.h" + +#include "calls/calls_call.h" +#include "styles/style_calls.h" +#include "styles/style_history.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/effects/ripple_animation.h" +#include "ui/widgets/shadow.h" +#include "messenger.h" +#include "auth_session.h" +#include "apiwrap.h" +#include "platform/platform_specific.h" + +namespace Calls { + +class Panel::Button : public Ui::RippleButton { +public: + Button(QWidget *parent, const style::CallButton &st); + +protected: + void paintEvent(QPaintEvent *e) override; + + void onStateChanged(State was, StateChangeSource source) override; + + QImage prepareRippleMask() const override; + QPoint prepareRippleStartPosition() const override; + +private: + const style::CallButton &_st; + QPixmap _bg; + +}; + +Panel::Button::Button(QWidget *parent, const style::CallButton &st) : Ui::RippleButton(parent, st.button.ripple) +, _st(st) { + resize(_st.button.width, _st.button.height); + _bg = App::pixmapFromImageInPlace(style::colorizeImage(prepareRippleMask(), _st.bg)); +} + +void Panel::Button::paintEvent(QPaintEvent *e) { + Painter p(this); + + p.drawPixmap(myrtlpoint(_st.button.rippleAreaPosition), _bg); + + auto ms = getms(); + + paintRipple(p, _st.button.rippleAreaPosition.x(), _st.button.rippleAreaPosition.y(), ms); + + auto down = isDown(); + auto position = _st.button.iconPosition; + _st.button.icon.paint(p, position, width()); +} + +void Panel::Button::onStateChanged(State was, StateChangeSource source) { + RippleButton::onStateChanged(was, source); + + auto over = isOver(); + auto wasOver = static_cast<bool>(was & StateFlag::Over); + if (over != wasOver) { + update(); + } +} + +QPoint Panel::Button::prepareRippleStartPosition() const { + return mapFromGlobal(QCursor::pos()) - _st.button.rippleAreaPosition; +} + +QImage Panel::Button::prepareRippleMask() const { + return Ui::RippleAnimation::ellipseMask(QSize(_st.button.rippleAreaSize, _st.button.rippleAreaSize)); +} + +Panel::Panel(gsl::not_null<Call*> call) +: _call(call) +, _user(call->user()) +, _hangup(this, st::callHangup) +, _mute(this, st::callMuteToggle) +, _name(this) +, _status(this) { + initControls(); + initLayout(); + show(); +} + +void Panel::initControls() { + subscribe(_call->stateChanged(), [this](Call::State state) { + if (state == Call::State::Failed || state == Call::State::Ended) { + callDestroyed(); + } + }); + _hangup->setClickedCallback([this] { + if (_call) { + _call->hangup(); + } + }); + if (_call->type() == Call::Type::Incoming) { + _answer.create(this, st::callAnswer); + _answer->setClickedCallback([this] { + if (_call) { + _call->answer(); + } + }); + } +} + +void Panel::initLayout() { + hide(); + + setWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint) | /*Qt::WindowStaysOnTopHint | */Qt::BypassWindowManagerHint | Qt::NoDropShadowWindowHint | Qt::Tool); + setAttribute(Qt::WA_MacAlwaysShowToolWindow); + setAttribute(Qt::WA_NoSystemBackground, true); + setAttribute(Qt::WA_TranslucentBackground, true); + + initGeometry(); + + processUserPhoto(); + subscribe(AuthSession::Current().api().fullPeerUpdated(), [this](PeerData *peer) { + if (peer == _user) { + processUserPhoto(); + } + }); + subscribe(AuthSession::CurrentDownloaderTaskFinished(), [this] { + refreshUserPhoto(); + }); + createDefaultCacheImage(); +} + +void Panel::processUserPhoto() { + if (!_user->userpicLoaded()) { + _user->loadUserpic(true); + } + auto photo = (_user->photoId && _user->photoId != UnknownPeerPhotoId) ? App::photo(_user->photoId) : nullptr; + if (isGoodUserPhoto(photo)) { + photo->full->load(true); + } else { + if ((_user->photoId == UnknownPeerPhotoId) || (_user->photoId && (!photo || !photo->date))) { + App::api()->requestFullPeer(_user); + } + } + refreshUserPhoto(); +} + +void Panel::refreshUserPhoto() { + auto photo = (_user->photoId && _user->photoId != UnknownPeerPhotoId) ? App::photo(_user->photoId) : nullptr; + if (isGoodUserPhoto(photo) && photo->full->loaded() && (photo->id != _userPhotoId || !_userPhotoFull)) { + _userPhotoId = photo->id; + _userPhotoFull = true; + createUserpicCache(photo->full); + } else if (_userPhoto.isNull()) { + if (auto userpic = _user->currentUserpic()) { + createUserpicCache(userpic); + } + } +} + +void Panel::createUserpicCache(ImagePtr image) { + auto size = st::callWidth * cIntRetinaFactor(); + auto options = _useTransparency ? (Images::Option::RoundedLarge | Images::Option::RoundedTopLeft | Images::Option::RoundedTopRight | Images::Option::Smooth) : 0; + auto width = image->width(); + auto height = image->height(); + if (width > height) { + width = qMax((width * size) / height, 1); + height = size; + } else { + height = qMax((height * size) / width, 1); + width = size; + } + _userPhoto = image->pixNoCache(width, height, options, size, size); + if (cRetina()) _userPhoto.setDevicePixelRatio(cRetinaFactor()); + + refreshCacheImageUserPhoto(); + + update(); +} + +bool Panel::isGoodUserPhoto(PhotoData *photo) { + if (!photo || !photo->date) { + return false; + } + auto badAspect = [](int a, int b) { + return a > 10 * b; + }; + auto width = photo->full->width(); + auto height = photo->full->height(); + return !badAspect(width, height) && !badAspect(height, width); +} + +void Panel::initGeometry() { + auto center = Messenger::Instance().getPointForCallPanelCenter(); + _useTransparency = Platform::TransparentWindowsSupported(center); + _padding = _useTransparency ? st::callShadow.extend : style::margins(); + _contentTop = _padding.top() + st::callWidth; + auto screen = QApplication::desktop()->screenGeometry(center); + auto rect = QRect(0, 0, st::callWidth, st::callHeight); + setGeometry(rect.translated(center - rect.center()).marginsAdded(_padding)); + createBottomImage(); +} + +void Panel::createBottomImage() { + if (!_useTransparency) { + return; + } + auto bottomWidth = width(); + auto bottomHeight = height() - _padding.top() - st::callWidth; + auto image = QImage(QSize(bottomWidth, bottomHeight) * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + { + Painter p(&image); + Ui::Shadow::paint(p, QRect(_padding.left(), 0, st::callWidth, bottomHeight - _padding.bottom()), width(), st::callShadow, Ui::Shadow::Side::Left | Ui::Shadow::Side::Right | Ui::Shadow::Side::Bottom); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.setBrush(st::callBg); + p.setPen(Qt::NoPen); + PainterHighQualityEnabler hq(p); + p.drawRoundedRect(myrtlrect(_padding.left(), -st::historyMessageRadius, st::callWidth, bottomHeight - _padding.bottom() + st::historyMessageRadius), st::historyMessageRadius, st::historyMessageRadius); + } + _bottomCache = App::pixmapFromImageInPlace(std::move(image)); +} + +void Panel::createDefaultCacheImage() { + if (!_useTransparency || !_cache.isNull()) { + return; + } + auto cache = QImage(size(), QImage::Format_ARGB32_Premultiplied); + cache.fill(Qt::transparent); + { + Painter p(&cache); + auto inner = rect().marginsRemoved(_padding); + Ui::Shadow::paint(p, inner, width(), st::callShadow); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.setBrush(st::callBg); + p.setPen(Qt::NoPen); + PainterHighQualityEnabler hq(p); + p.drawRoundedRect(myrtlrect(inner), st::historyMessageRadius, st::historyMessageRadius); + } + _cache = App::pixmapFromImageInPlace(std::move(cache)); +} + +void Panel::refreshCacheImageUserPhoto() { + auto cache = QImage(size(), QImage::Format_ARGB32_Premultiplied); + cache.fill(Qt::transparent); + { + Painter p(&cache); + Ui::Shadow::paint(p, QRect(_padding.left(), _padding.top(), st::callWidth, st::callWidth), width(), st::callShadow, Ui::Shadow::Side::Top | Ui::Shadow::Side::Left | Ui::Shadow::Side::Right); + p.drawPixmapLeft(_padding.left(), _padding.top(), width(), _userPhoto); + p.drawPixmapLeft(0, _padding.top() + st::callWidth, width(), _bottomCache); + } + _cache = App::pixmapFromImageInPlace(std::move(cache)); +} + +void Panel::resizeEvent(QResizeEvent *e) { + auto controlsTop = _contentTop + st::callControlsTop; + if (_answer) { + auto bothWidth = _answer->width() + st::callControlsSkip + _hangup->width(); + _hangup->moveToLeft((width() - bothWidth) / 2, controlsTop); + _answer->moveToRight((width() - bothWidth) / 2, controlsTop); + } else { + _hangup->moveToLeft((width() - _hangup->width()) / 2, controlsTop); + } + _mute->moveToRight(_padding.right() + st::callMuteRight, controlsTop); +} + +void Panel::paintEvent(QPaintEvent *e) { + Painter p(this); + if (_useTransparency) { + p.drawPixmapLeft(0, 0, width(), _cache); + } else { + p.drawPixmapLeft(0, 0, width(), _userPhoto); + p.fillRect(myrtlrect(0, st::callWidth, width(), height() - st::callWidth), st::callBg); + } +} + +void Panel::mousePressEvent(QMouseEvent *e) { + auto dragArea = myrtlrect(_padding.left(), _padding.top(), st::callWidth, st::callWidth); + if (e->button() == Qt::LeftButton && dragArea.contains(e->pos())) { + _dragging = true; + _dragStartMousePosition = e->globalPos(); + _dragStartMyPosition = QPoint(x(), y()); + } +} + +void Panel::mouseMoveEvent(QMouseEvent *e) { + if (_dragging) { + if (!(e->buttons() & Qt::LeftButton)) { + _dragging = false; + } else { + move(_dragStartMyPosition + (e->globalPos() - _dragStartMousePosition)); + } + } +} + +void Panel::mouseReleaseEvent(QMouseEvent *e) { + if (e->button() == Qt::LeftButton) { + _dragging = false; + } +} + +void Panel::callDestroyed() { +} + +} // namespace Calls diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h new file mode 100644 index 000000000..bd1235ed0 --- /dev/null +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -0,0 +1,89 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#pragma once + +#include "base/weak_unique_ptr.h" + +namespace Ui { +class IconButton; +class FlatLabel; +} // namespace Ui + +namespace Calls { + +class Call; + +class Panel : public TWidget, private base::Subscriber { +public: + Panel(gsl::not_null<Call*> call); + +protected: + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + +private: + void initControls(); + void initLayout(); + void initGeometry(); + void createBottomImage(); + void createDefaultCacheImage(); + void refreshCacheImageUserPhoto(); + + void processUserPhoto(); + void refreshUserPhoto(); + bool isGoodUserPhoto(PhotoData *photo); + void createUserpicCache(ImagePtr image); + + void callDestroyed(); + + base::weak_unique_ptr<Call> _call; + gsl::not_null<UserData*> _user; + + bool _useTransparency = true; + style::margins _padding; + int _contentTop = 0; + + bool _dragging = false; + QPoint _dragStartMousePosition; + QPoint _dragStartMyPosition; + + class Button; + object_ptr<Ui::IconButton> _close = { nullptr }; + object_ptr<Button> _answer = { nullptr }; + object_ptr<Button> _hangup; + object_ptr<Ui::IconButton> _mute; + object_ptr<Ui::FlatLabel> _name; + object_ptr<Ui::FlatLabel> _status; + std::vector<EmojiPtr> _fingerprint; + + QPixmap _userPhoto; + PhotoId _userPhotoId = 0; + bool _userPhotoFull = false; + + QPixmap _bottomCache; + QPixmap _cache; + +}; + +} // namespace Calls diff --git a/Telegram/SourceFiles/core/utils.h b/Telegram/SourceFiles/core/utils.h index 8dd907f28..c320b8815 100644 --- a/Telegram/SourceFiles/core/utils.h +++ b/Telegram/SourceFiles/core/utils.h @@ -210,6 +210,10 @@ inline void copy_bytes(byte_span destination, const_byte_span source) { memcpy(destination.data(), source.data(), source.size()); } +inline void set_bytes(byte_span destination, gsl::byte value) { + memset(destination.data(), gsl::to_integer<unsigned char>(value), destination.size()); +} + } // namespace base // using for_const instead of plain range-based for loop to ensure usage of const_iterator diff --git a/Telegram/SourceFiles/messenger.cpp b/Telegram/SourceFiles/messenger.cpp index 3b4cc7fa0..252783009 100644 --- a/Telegram/SourceFiles/messenger.cpp +++ b/Telegram/SourceFiles/messenger.cpp @@ -777,3 +777,12 @@ Messenger::~Messenger() { MainWindow *Messenger::mainWindow() { return _window.get(); } + +QPoint Messenger::getPointForCallPanelCenter() const { + Expects(_window != nullptr); + Expects(_window->windowHandle() != nullptr); + if (_window->isActive()) { + return _window->geometry().center(); + } + return _window->windowHandle()->screen()->geometry().center(); +} diff --git a/Telegram/SourceFiles/messenger.h b/Telegram/SourceFiles/messenger.h index 152935815..a4ea8032e 100644 --- a/Telegram/SourceFiles/messenger.h +++ b/Telegram/SourceFiles/messenger.h @@ -50,6 +50,7 @@ public: ~Messenger(); MainWindow *mainWindow(); + QPoint getPointForCallPanelCenter() const; static Messenger *InstancePointer(); static Messenger &Instance() { diff --git a/Telegram/SourceFiles/structs.h b/Telegram/SourceFiles/structs.h index 2c4eb2803..e660fb7f2 100644 --- a/Telegram/SourceFiles/structs.h +++ b/Telegram/SourceFiles/structs.h @@ -377,11 +377,12 @@ public: return _openLink; } + ImagePtr currentUserpic() const; + protected: void updateNameDelayed(const QString &newName, const QString &newNameOrPhone, const QString &newUsername); ImagePtr _userpic; - ImagePtr currentUserpic() const; mutable EmptyUserpic _userpicEmpty; private: diff --git a/Telegram/SourceFiles/ui/widgets/widgets.style b/Telegram/SourceFiles/ui/widgets/widgets.style index 6472d7390..51626c789 100644 --- a/Telegram/SourceFiles/ui/widgets/widgets.style +++ b/Telegram/SourceFiles/ui/widgets/widgets.style @@ -352,6 +352,11 @@ MultiSelect { fieldCancelSkip: pixels; } +CallButton { + button: IconButton; + bg: color; +} + Menu { skip: pixels; diff --git a/Telegram/gyp/Telegram.gyp b/Telegram/gyp/Telegram.gyp index cee607fe8..49b2e030a 100644 --- a/Telegram/gyp/Telegram.gyp +++ b/Telegram/gyp/Telegram.gyp @@ -37,6 +37,7 @@ '<(res_loc)/colors.palette', '<(res_loc)/basic.style', '<(src_loc)/boxes/boxes.style', + '<(src_loc)/calls/calls.style', '<(src_loc)/dialogs/dialogs.style', '<(src_loc)/history/history.style', '<(src_loc)/intro/intro.style', diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt index 5dffcd437..dc6e4082e 100644 --- a/Telegram/gyp/telegram_sources.txt +++ b/Telegram/gyp/telegram_sources.txt @@ -3,6 +3,7 @@ <(src_loc)/base/observer.cpp <(src_loc)/base/observer.h <(src_loc)/base/ordered_set.h +<(src_loc)/base/openssl_help.h <(src_loc)/base/parse_helper.cpp <(src_loc)/base/parse_helper.h <(src_loc)/base/qthelp_regex.h @@ -83,6 +84,8 @@ <(src_loc)/calls/calls_call.h <(src_loc)/calls/calls_instance.cpp <(src_loc)/calls/calls_instance.h +<(src_loc)/calls/calls_panel.cpp +<(src_loc)/calls/calls_panel.h <(src_loc)/chat_helpers/bot_keyboard.cpp <(src_loc)/chat_helpers/bot_keyboard.h <(src_loc)/chat_helpers/emoji_list_widget.cpp