From 158aa3abc4a2e918caf64a5771cae62b551bd936 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 11 Jul 2018 09:58:02 -0400 Subject: [PATCH] Tweak system messages; incomplete vs. missed calls. --- .../Contents.json | 23 -- .../system_message_group@1x.png | Bin 1484 -> 0 bytes .../system_message_group@2x.png | Bin 1924 -> 0 bytes .../system_message_group@3x.png | Bin 2393 -> 0 bytes .../Contents.json | 23 -- .../system_message_info@1x.png | Bin 1614 -> 0 bytes .../system_message_info@2x.png | Bin 2234 -> 0 bytes .../system_message_info@3x.png | Bin 2860 -> 0 bytes Signal/src/AppDelegate.m | 4 + .../Cells/ConversationViewCell.h | 14 +- .../Cells/OWSSystemMessageCell.m | 350 ++++++++++++++---- .../ConversationViewController.m | 97 ----- .../ViewControllers/DebugUI/DebugUIMessages.m | 8 +- Signal/src/call/CallService.swift | 14 +- .../translations/en.lproj/Localizable.strings | 17 +- .../src/Messages/OWSIncompleteCallsJob.h | 29 ++ .../src/Messages/OWSIncompleteCallsJob.m | 150 ++++++++ SignalServiceKit/src/Messages/TSCall.h | 10 +- SignalServiceKit/src/Messages/TSCall.m | 59 ++- .../src/Storage/OWSPrimaryStorage.m | 4 +- SignalServiceKit/src/Storage/OWSStorage.m | 6 +- 21 files changed, 572 insertions(+), 236 deletions(-) delete mode 100644 Signal/Images.xcassets/system_message_group.imageset/Contents.json delete mode 100644 Signal/Images.xcassets/system_message_group.imageset/system_message_group@1x.png delete mode 100644 Signal/Images.xcassets/system_message_group.imageset/system_message_group@2x.png delete mode 100644 Signal/Images.xcassets/system_message_group.imageset/system_message_group@3x.png delete mode 100644 Signal/Images.xcassets/system_message_info.imageset/Contents.json delete mode 100644 Signal/Images.xcassets/system_message_info.imageset/system_message_info@1x.png delete mode 100644 Signal/Images.xcassets/system_message_info.imageset/system_message_info@2x.png delete mode 100644 Signal/Images.xcassets/system_message_info.imageset/system_message_info@3x.png create mode 100644 SignalServiceKit/src/Messages/OWSIncompleteCallsJob.h create mode 100644 SignalServiceKit/src/Messages/OWSIncompleteCallsJob.m diff --git a/Signal/Images.xcassets/system_message_group.imageset/Contents.json b/Signal/Images.xcassets/system_message_group.imageset/Contents.json deleted file mode 100644 index b08517114..000000000 --- a/Signal/Images.xcassets/system_message_group.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "system_message_group@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "system_message_group@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "system_message_group@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Signal/Images.xcassets/system_message_group.imageset/system_message_group@1x.png b/Signal/Images.xcassets/system_message_group.imageset/system_message_group@1x.png deleted file mode 100644 index 5ed66389379dbcb2fb83698b75f3b030e7ad174e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1484 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~&H|6fVg?3o zVGw3ym^DX&fq_LSGbExU!q>+tIX_n~F(p4KRj(qq0H~UQ!KT6r$jnVGNmQuF&B-ga zs<2f8tFQvHLBje<3ScEA*|tg%z5xo(`9-M;rg|oN21<5Z3JMA~MJZ`kK`w4k?LeNb zQbtKhft9{~d3m{Bxv^e;QM$gNrKP35fswwEkuFe$ZgFK^Nn(X=Ua>O75STeGsl~}f znFS@8`FRQ;a}$&DOG|8(lt3220mPjpnP~`{@`|C}0(wv%B%^PrXP}QwTWUon4s9SA zoZ3>7;l3&;Ey@A=DJ5AyH77MUHLs)?sLv3qb-=KNYeaEmMPdQOGH@V5{AL4kxm8eV zaehuICu1<^J5 z7i9u{nh0{2ogvf$WHEI0k=QIi7DUnj3VN%6%!<^U2$xJ?fP#HtWMHF@RRUe3bAC>K zQE)+Gaw^DSU@b^O=&HfiMB=grNdie@O0rdPX;M~datTsw0pkpu_MH;b^^t^a^s%b8 z0j2~i-~5!!v`Ux6l2kh*14DCN12bI%(-1>LD+4nt3!rz=)F8P4B;#C^npl!w6q28x zV+Zy{E{LmOtY-$%i7XG*YNHRzMM${{l4Zf7z&vZm1=I=4|8`vZKMbOQ#fh`0i(`m| z;M+@k^;iN$4t)GLafL?2HeubRPLApqSD5~0`@-eo|4{bDHRg^B9&1`MIy72bnp(Cl z67W|1`1Qa4GqvkC*l#5Hf4W`$Zf5oSGjndLWV&81U7-BkCHYKx-~rXT3mSLKn}zNb z_SYLaF6HCDsMO<`lX|9?wWai(nrHqZAr%Ovd!`Q>NKU(i;X^MKXvAjiE8Z1Mkjwr@DU+QLOD_ol^} z9ZdFKJ5KWcw6QVxlGlI7R?ddyG57HgTlFW1Je+$+!aAXD-s;M`v#vkd`fOg{m&|7Y zPBPbh>I{W9T? u{}+SVZj5&&S_<2(=56c^-?HeY^#^W^ThT|EGlpi<;HsXMd|v6mX?-X(Gs7c7{+3kj2o|M`E)8SrADBDCn&MGAmMZB3v?o0SfkoiGhtiRta>C&iOg{ zMZpD$$*CZRfwdqBp{oX46N$?jBnc#qDalsFrAb+-$t6g!1&lLr+ILD!*GCez(Z{OV z2AC48eDhN>(<)sOOH%EO3=GY64a{^6OhXI}tqjbp3`}hF(bOQh03_pFl$uzQUlfv` zpJNC1MJ|Y|V60~b(TOY%)oP;;%0)=I3X)~PqQE?B#|6|0%l~#JL-XTXc)LIQ&IrK7Eq`e(CUOMK)tI^c@i@zt-AzIfZz`9jL zsi{|?V?oA3#_20oJm|`P^zQpa>%^k_d*AQf({R=HLu%T&vh&u)Gpk>};?lGVC|U5r z;bP+=X62U$W^f(cydl5!?goaq!|D>9Iw}?HZ&*I>Z?Tiuwnljo`#-lCIo3}+nb}SX zC!ZC3zUO?EZ=A!tI=!~#0_t3!^zVZWemM17*QF+`ki(l?tSO%N^YtIQ{yZKO}c6iknzue8C=2LkmBA)owj-wvFycv3{S>D69e8-ByGgIN)*x)(SuAB-2i zCvz|V_JXiXr47>eR5?vQ&!GSS diff --git a/Signal/Images.xcassets/system_message_group.imageset/system_message_group@3x.png b/Signal/Images.xcassets/system_message_group.imageset/system_message_group@3x.png deleted file mode 100644 index 4e941066508e171db0138a42e693f8bc5aba7836..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2393 zcmZ`*4?L6kAD=}2mW-_OyPI`!DSI|F%*?SVe)*R_#ZPxrsv zYxH0+n4y;^IRLs6l&6k1^o{@U2N$|%@B+5F!^BeaA!wn?@eJj`VES<7sR28mgMh)7 zq8LFT{16{+JQZZ2DKs#Gjux>vP&Txa2oK$|=zI!N#A32}c##WAm4S!ul`w!ps#5rS zTu>oC{zwwYr6V_^?a>$%Q4fhk61cQTd;odN935J@prZMF4jur6LLpjchX%P(0M^OL z3BcF_wzf7*O;c zKP$0$bG9IZfHDGL(HLMM8ePO#grG%qNmSCddusP^ffeVJeCmG(TG<6S|m;T0o6RqvCmjh~0E5U+rl@&rJ31yHlb7 zHJAV_m`#A*1iTMOV?@S~DSSH77K3%L!QgB#n}e`eJO+oy;RwLI9JLCR5fYb9;e*^D z5M&ZnE?CMX(hfbxRFBV534n5n)TU~7WK}3MvqU{e^W*#z_3XPP@1S3c`(9+XplGep ztkARzfruZX3egXe$eVJTylswEtY3e2l^NR8Ua&pFtLpdBL^A4R;R&OiO-W7M&>o|| zsw~7yO^=3L^hgc5l3RA#`q7TmLB=$=Up6VNoU#*3vkBw5;}?YQqz9zJ>5SEg=2II_ zEOKBlo#op4W#4OLHfVI}$Q%4#B{b`P6{;)C)9{$iy;2Yq2_IdFNJ&6leNbn1JaNdF z+PkdREhoX?$xV#@C9=$DAovLU)>KrPQ?*+Yg}G~e*|%0rvD$sHduQm(U0F83z-ssn?J>!VOz0WaISIetf^4SfF;v6_N{~nil)^eofVXOalCI*x3k9Szd z4O@O-v@+SPJ_qkzbE{#D=hWX1Nzj_IyxSraXTMU=bgcX{U*$GGZFLLNjl=ab+b;Fd zT%&jAl!x8@CWAC696_fh?jpGgs(zVxf4HxtrE`PAUG7_j=8qJ_nMn`i>wMuV#O|Yd zCHCKsfv1iJ#|X*|9fWm;Q{^ zZmZC28+A^*xsjvmoxKV9I<$1D4Au!8^*&|wloq*9OFqeC4Oi3+K#v3H37qx zwI=dz$sronT%TUP>{nnKUkm#V+uHc(8bbQQkUj9uLiF{SJ40WChl0cNWu$X2W(;d} zWitlt808XJ#LInti&4%q&jz_?~*@+HEuWfOEQg|0prBtgg=E z2eID@q?N$t_msEw!JL;#g*@-I!c)Ze`|gHKI;E^EG^sn8p3fP6-IFv^!f|1a7;VdY z6mCEj#H2LbzUEjEFm`z22#{7o5cbs>cSt0}h@m|O)YX?;YfOmJqKS#h-;%ax7PwCD zycwOhx1q=H?}#L{tYF$(tCrsXH?BJRwpM+v?h^he&pFHXPVCQtOu6b^k`{ zxw5+yv3{6#X~?ux@h?AbktXZhil{@qZMK|VAhBPvXTUpaVyt~;RdRUsEpyqARv#Ry zHJ=SDu7fH6eFmS@!AaPTn|yZsuef(fDZ;l)E@_Rm=(q2vhbKjxzp%pi?PeXoIOT<0 z(@GJRew~nEyUvMAtB3nG9WR`2?>Z>c=UBCArW(ta29-3P;n^OjKVx_M>=%S;uMAW6 znwV9|z8$&G?hK2K1b-O_f5a8kyJIT5?`1!XFD(?GhVv`pM=4>sxKOUvV#lS6OJ#r$;fenM>}AT8 diff --git a/Signal/Images.xcassets/system_message_info.imageset/Contents.json b/Signal/Images.xcassets/system_message_info.imageset/Contents.json deleted file mode 100644 index 17efdbeb5..000000000 --- a/Signal/Images.xcassets/system_message_info.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "system_message_info@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "system_message_info@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "system_message_info@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Signal/Images.xcassets/system_message_info.imageset/system_message_info@1x.png b/Signal/Images.xcassets/system_message_info.imageset/system_message_info@1x.png deleted file mode 100644 index ae5a57b65f32da3a65f4756001dcc04530f65fe0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1614 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~&H|6fVg?3o zVGw3ym^DX&fq_LSGbExU!q>+tIX_n~F(p4KRj(qq0H~UQ!KT6r$jnVGNmQuF&B-ga zs<2f8tFQvHLBje<3ScEA*|tg%z5xo(`9-M;rg|oN21<5Z3JMA~MJZ`kK`w4k?LeNb zQbtKhft9{~d3m{Bxv^e;QM$gNrKP35fswwEkuFe$ZgFK^Nn(X=Ua>O75STeGsl~}f znFS@8`FRQ;a}$&DOG|8(lt3220mPjpnP~`{@`|C}0(wv%B%^PrXP}QwTWUon4s9SA zoZ3>7;l3&;Ey@A=DJ5AyH77MUHLs)?sLv3qb-=KNYeaEmMPdQOGH@V5{AL4kxm8eV zaehuICu1<^J5 z7i9u{nh0{2ogvf$WHEI0k=QIi7DUnj3VN%6%!<^U2$xJ?fP#HtWMHF@RRUe3bAC>K zQE)+Gaw^DSU@b^O=&HfiMB=grNdie@O0rdPX;M~datTsw0pkpu_MH;b^^t^a^s%b8 z0j2~i-~5!!v`Ux6l2kh*14DCN12bI%(-1==D+4nt1EAZ{)F8P4B;#C^npl!w6q28x zV+Zy{E{LmOtY-$%i7XG*YNHRzMM${{l4Zf7z&vZm1=I=4|8`v893+562jfvs7sn6_ z!MRiSd&ve0w670TmQZoy4pdwlCBZH4aPZh6JzuTMFS<3l@)f2n@=v&+)bWeK?N4)p zu&CqFQw~xG_ut=sChlEqd3eEt+c%9r&%U`i{ro$#kgm0zR}^>-UY`*Afd2u9oYJ>t z*)E2hyFbJqZModXsY!5{C;01B-=X)0Xru+VjGK+3Z#EABRj`_ATE z*Pb9Xe=8@`{mqWYf)j$X-v*jqSDqVbDxtpihxZTmw}PunKl7~F>YgNT`R=e~P1~BC zAG|kkm2T!(XqukDUmeHQD+W%jvFe)}Qvd-DOMGmhS7l8?8Tz1-^a zLGTyXk@$}IgS<6b7b3FzMbm#w`|Do7U9ResAFgF~hx2~#`OR!iL22)t+W|~=Vac?cC#=netD+Io^RpmFGZiY zb6(AgGGSwGe17cA%D`;jl|Pq$3%le$vo#XPjZRPfeY#YZrJnI`U}=+oJ>z%5)y4XH W=QD&0cd|?X74n|0elF{r5}E+_FE45U diff --git a/Signal/Images.xcassets/system_message_info.imageset/system_message_info@2x.png b/Signal/Images.xcassets/system_message_info.imageset/system_message_info@2x.png deleted file mode 100644 index 06cde5bd6c4e85210e3d4152d315f129c0f0ea2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2234 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEjKx9jP7LeL$-D$|I14-?iy0WW zg+Z8+Vb&Z81_llpi<;HsXMd|v6mX?-X(Gs7c7{+3kj2o|M`E)8SrADBDCn&MGAmMZB3v?o0SfkoiGhtiRta>C&iOg{ zMZpD$$*CZRfwdqBp{oX46N$?jBnc#qDalsFrAb+-$t6g!1&lLr+ILD!*GCez(Z{OV z2AC48eDhN>(<)sOOH%EO3=GY64a{^6OhXKftPISoOn`1jQ-kCJkc@LtYGO%#QAmD% zjvd$+xgf5Bv7Q-3C$c;NR|bQ0`sgL7f>fG|J!k`saI41mKKJdE{-7) zoOfrOULPAQb3A{l+R_Z~i_w>!FKB9XQ{`aJYLZycJMn+Rw#nN#+k}+Vgg9N7F!#?0 zO>#IZc~$$>Rk7Tw4lkJ(u6^`*=ehS!wx7TGd(PJLdu;{pKdHW+|Nq+it=FTf*OtCJ za-?2n^RmeMu0Pf{Xg^fhBhsb(!Qc-2_rt;kIXfo!d_Nri(ENkRpWTh$G8eD4`!&z> zjPj?$ABt~;Trk+X@0dZDpx5#W*^BdGSf6`R_EDB?)-`bJ)i_EkW1qq4?Wc)xH~dnCkui&7Sq=&eQK|ho8^l$^NqX z!cV8bId9jRmiewqpS<4G;%{w%e>cnV3Ddtv@`P15{b8tY@;`8MgNV1`A4#{m?3agU zXB84r*Zx9;y+}tWBDcPdF{J{>ath9kuhIjaoNzqDf-jX zB(v73e@c0GsWhFQaC;tK-@pC~j+?}?Up;bMBC(f$LV9U+8@GSbC(%Eq7fow;bC`?w z@h|4fekG^hUHjqz!&D!WU0i`A|T=F5YnXGEPAR9SC%f1mCi%m0(+ z{PQZ3{+MC3FWi#L=U3Qb@w(E&Mfq3CvSzwI*d9FPr>ETZ8T;?dRqJQ^+bFz)YvEaU zp}NbtXEWa&2=NVEaNK*1^!A=f&2~&@L#jC&&*-i>I%}8m57VNaM@mmj%lagrnQY$x zbg$Tp_pIL+9AH-L+4wa*Q%5&@<1(&KsRu7VIpy{K0lU9kZPH}h-kQ4!)4kpc&3hzb z==L`CpmyKyNR>*qog#9^%e=AgUa*1C z-qxCb4E_3(bAPz(PH&iRSgsadJZm>cf3m1&4(}5?(_n)zy>l|pbdKfd#5e8qSpMkQ zBaX9e<`<_fzbf<0C|&!^!C$U3y1yL$_&Sd7c9JCDFDd2YCha@jn%oyU*9$#ckf*7C zc=tn51G9)DdRJ~P@7-1@Z8XiWSN6DWlKm^D@6og~x^=RX(R$9-FR8;3>AdfPUq8(L5dA|vN9&SZ?v|_R zFXY;NS(gUK6ngQ;8T<`ZS5J(Y{!K}~?YEVU-|zoN>`QC{d!0|pgW4nvp00i_>zopr E00W6_?*IS* diff --git a/Signal/Images.xcassets/system_message_info.imageset/system_message_info@3x.png b/Signal/Images.xcassets/system_message_info.imageset/system_message_info@3x.png deleted file mode 100644 index eef10d74496dec0ce9cd55b570a5be4a68c105c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2860 zcmZ`*2UJt(5)O)jfXK>0se&lQRnkC^K!P+;F~TBXqzEA-A(&=vA`z8cKoKc%-vbR* zL==%;1gwP4J|R?TOEJKTum-TLRACdae@Qv?vlAA$p|XnOGe*g-ityY^3fo0wH=9n<2V7%Mj)%q%H>S ztYb@K;C1$bO~6oHl$?%^jwQpNfN-%p@R=@JA$0?oOgaJr2?`1V2N{ED3?c+(Zf*{N z!Xa?DkqBV~oTW0cY$GbLVQJpoCh1VP+-gfoeaCu8hLqJ}P4 zVG6Z`eAls>XiFp07{};X9A2E*GDX6)8oexY;zu$P4hf<^4gs+I0`WMe#BPc9Z28@{ z!4e@7W=qI-(Uzh&0fDCZlL%++uuMD(4uzQ+K~0UIdtG601k@B^W@ZUlg^|cW?6GCw zu}m7nl}00@maCwMmpaB^(-o$q|1;GRBAz0NsahG?WvggrQF0>Ym-$D@f%hN04Favt zIAmw-8nA96$urB{UNs``{vL}Hgmu>lheoA-zIVIB{!BY(>t+?#<2KUP2bpmsdwZG4 zu}s-LW3u)Je~j%sId)0qT0(>DfW_L5XYuXAfR>U0+=y^?>FHu=7jNKk@X&zq>*U#r z*+Eu9V(B6>p!HXU`46FkHL4>IsM{RPHMdFC5oUJugLvbk?$657wZb;(^cf3W+1oTX zU5%KpAGyyfs|L(yszuzf|9S;=E1naQc~FRCV-HKnhHWe9c+ zXddd-xZZ)?vT(DU_F} zX=Mqqvfmf1^VKQX!R-iBqS%8h?9HlM(>zebxRz zqs2?0C_Hx9A#aksWqVhZ7j@K24veHux8Kz2(XT3|=bSZovgwAWsceX{z$0Ol*gQJ8 z*{kPxjY&dzd!mv3pC#}4nsW@L;YI6D)|fM-#F&ED!I4`J1Jj9+n9Jj8DYhhrv7EzL zZ(D4g6SOwEF(>#8g#0k#29G$LovzehFuEBax~(f8(u|II-QvC>W@<-bPoUxb=^NbB zCqwzM`&Ifwfj#hio~s_T;<EaVC%y6-PoBhE=TQbSW^cVyIx@n$!skY2(}(AkGT(N_*M(vRbTbu?6VGwY5N{tY zU063Lce(lz-8{5?q4Rdxwtd=;!H&7L?nE2p`n+DjD?J}ATw9=v=hoqWTIvuNtU|Q< zINshewLU&-K8|p=xPla9p_1cyLW*zmcw5erMzg;Cv_jFA(uvcsfotT02o^)g^iffC zz3A4kVX-DFH@wKVlr`~Web6)_wB5-Fc797rSI)4}ng=E`0W+3&-uO3#R?xabDbllR zIGnJ1=-6aj)p&b}E+fk&_40cYwlUd6D#^5`|J-PkLYrwv65)#7cdt3ZbQ&JAySKL)(Hc_ zz)dmf`lYPxM?AJItnryBUVPu6)ibfA`Efq!VSUmO3_7sUZ%@|;<2;{D;AbDH{GQ3( z!cO$Bp)+d9NlC+M9wUAHIRpAMrS19S(<3C4y^rqD$U^rlyy4U9l_pcfX!hfwH%+&j zf#`i^K!&W zOA}Q|P4opf`O^dFv;PgF8S;Di4!fG_c1uS!s`MDdM`5nUwGfWxnkNAyh5C*(IIbvt za^k63*^QG=`zhp;5L+N$+Nz!>AKv=l!`;|(*xn5?-PQD%b2sCNDYjY3I4vtpB|^6q zV7htYH|-kI8M1QBr=Jvt+uQpix{+nXsXG1eq>C@=Gi|x)*-+7EF;On&D*s;DZpU0? zp|%6yOZ~`+!yHa~NS7n)sm=eLL8Cb)nkPh?t3*I#@PgVQtA{aDb-i(|QmLKCtB#I! zU#2)Kgzpd-j?GOrS*d@#&UGBwGqX)-{GiZEy4>c{KkKv_VV;Aof@2y_o;lSR6?)%_ zolrdoOb};~u1qq|UE2rKkg+78M{8e+3*q&cQM#aCd NhwP8q<=gm%{R`#_(ait= diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 888d9417c..2871f021e 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -40,6 +40,7 @@ #import #import #import +#import #import #import #import @@ -596,6 +597,9 @@ static NSTimeInterval launchStartedAt; // Mark all "attempting out" messages as "unsent", i.e. any messages that were not successfully // sent before the app exited should be marked as failures. [[[OWSFailedMessagesJob alloc] initWithPrimaryStorage:[OWSPrimaryStorage sharedManager]] run]; + // Mark all "incomplete" calls as missed, e.g. any incoming or outgoing calls that were not + // connected, failed or hung up before the app existed should be marked as missed. + [[[OWSIncompleteCallsJob alloc] initWithPrimaryStorage:[OWSPrimaryStorage sharedManager]] run]; [[[OWSFailedAttachmentDownloadsJob alloc] initWithPrimaryStorage:[OWSPrimaryStorage sharedManager]] run]; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h index b092f241e..d1ccf8da5 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h @@ -11,11 +11,16 @@ NS_ASSUME_NONNULL_BEGIN @class OWSContactsManager; @class TSAttachmentPointer; @class TSAttachmentStream; +@class TSCall; +@class TSErrorMessage; @class TSInteraction; +@class TSInvalidIdentityKeyErrorMessage; +@class TSInvalidIdentityKeyErrorMessage; @class TSMessage; @class TSOutgoingMessage; @class TSQuotedMessage; @class YapDatabaseReadTransaction; +@class YapDatabaseReadTransaction; @protocol ConversationViewCellDelegate @@ -27,8 +32,13 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - System Cell -// TODO: We might want to decompose this method. -- (void)didTapSystemMessageWithInteraction:(TSInteraction *)interaction; +- (void)tappedNonBlockingIdentityChangeForRecipientId:(nullable NSString *)signalId; +- (void)tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)errorMessage; +- (void)tappedCorruptedMessage:(TSErrorMessage *)message; +- (void)resendGroupUpdateForErrorMessage:(TSErrorMessage *)message; +- (void)showFingerprintWithRecipientId:(NSString *)recipientId; +- (void)showConversationSettings; +- (void)handleCallTap:(TSCall *)call; #pragma mark - Offers diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m index 014d07120..902fc6bcf 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m @@ -17,14 +17,41 @@ NS_ASSUME_NONNULL_BEGIN +typedef void (^SystemMessageActionBlock)(void); + +@interface SystemMessageAction : NSObject + +@property (nonatomic) NSString *title; +@property (nonatomic) SystemMessageActionBlock block; + +@end + +#pragma mark - + +@implementation SystemMessageAction + ++ (SystemMessageAction *)actionWithTitle:(NSString *)title block:(SystemMessageActionBlock)block +{ + SystemMessageAction *action = [SystemMessageAction new]; + action.title = title; + action.block = block; + return action; +} + +@end + +#pragma mark - + @interface OWSSystemMessageCell () @property (nonatomic, nullable) TSInteraction *interaction; -@property (nonatomic) UIImageView *imageView; +@property (nonatomic) UIImageView *iconView; @property (nonatomic) UILabel *titleLabel; -@property (nonatomic) UIStackView *stackView; +@property (nonatomic) UIButton *button; +@property (nonatomic) UIStackView *vStackView; @property (nonatomic) NSArray *layoutConstraints; +@property (nonatomic, nullable) SystemMessageAction *action; @end @@ -44,38 +71,71 @@ NS_ASSUME_NONNULL_BEGIN - (void)commontInit { - OWSAssert(!self.imageView); + OWSAssert(!self.iconView); self.layoutMargins = UIEdgeInsetsZero; self.contentView.layoutMargins = UIEdgeInsetsZero; - self.imageView = [UIImageView new]; - [self.imageView autoSetDimension:ALDimensionWidth toSize:self.iconSize]; - [self.imageView autoSetDimension:ALDimensionHeight toSize:self.iconSize]; - [self.imageView setContentHuggingHigh]; + self.iconView = [UIImageView new]; + [self.iconView autoSetDimension:ALDimensionWidth toSize:self.iconSize]; + [self.iconView autoSetDimension:ALDimensionHeight toSize:self.iconSize]; + [self.iconView setContentHuggingHigh]; self.titleLabel = [UILabel new]; self.titleLabel.numberOfLines = 0; self.titleLabel.lineBreakMode = NSLineBreakByWordWrapping; + self.titleLabel.textAlignment = NSTextAlignmentCenter; - self.stackView = [[UIStackView alloc] initWithArrangedSubviews:@[ - self.imageView, + UIStackView *contentStackView = [[UIStackView alloc] initWithArrangedSubviews:@[ + self.iconView, self.titleLabel, ]]; - self.stackView.axis = UILayoutConstraintAxisHorizontal; - self.stackView.spacing = self.hSpacing; - self.stackView.alignment = UIStackViewAlignmentCenter; - [self.contentView addSubview:self.stackView]; - - UITapGestureRecognizer *tap = - [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; - [self addGestureRecognizer:tap]; + contentStackView.axis = UILayoutConstraintAxisVertical; + contentStackView.spacing = self.iconVSpacing; + contentStackView.alignment = UIStackViewAlignmentCenter; + + self.button = [UIButton buttonWithType:UIButtonTypeCustom]; + [self.button setTitleColor:[UIColor ows_darkSkyBlueColor] forState:UIControlStateNormal]; + self.button.titleLabel.textAlignment = NSTextAlignmentCenter; + [self.button setBackgroundColor:[UIColor ows_light02Color]]; + self.button.layer.cornerRadius = 4.f; + [self.button addTarget:self action:@selector(buttonWasPressed:) forControlEvents:UIControlEventTouchUpInside]; + [self.button autoSetDimension:ALDimensionHeight toSize:self.buttonHeight]; + + self.vStackView = [[UIStackView alloc] initWithArrangedSubviews:@[ + contentStackView, + self.button, + ]]; + self.vStackView.axis = UILayoutConstraintAxisVertical; + self.vStackView.spacing = self.buttonVSpacing; + self.vStackView.alignment = UIStackViewAlignmentCenter; + [self.contentView addSubview:self.vStackView]; UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; [self addGestureRecognizer:longPress]; } +- (CGFloat)buttonVSpacing +{ + return 7.f; +} + +- (CGFloat)iconVSpacing +{ + return 9.f; +} + +- (CGFloat)buttonHeight +{ + return 40.f; +} + +- (CGFloat)buttonHPadding +{ + return 20.f; +} + - (void)configureFonts { // Update cell to reflect changes in dynamic text. @@ -95,27 +155,43 @@ NS_ASSUME_NONNULL_BEGIN TSInteraction *interaction = self.viewItem.interaction; - UIImage *icon = [self iconForInteraction:interaction]; - self.imageView.image = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - self.imageView.tintColor = [self iconColorForInteraction:interaction]; + self.action = [self actionForInteraction:interaction]; + + UIImage *_Nullable icon = [self iconForInteraction:interaction]; + if (icon) { + self.iconView.image = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + self.iconView.hidden = NO; + self.iconView.tintColor = [self iconColorForInteraction:interaction]; + } else { + self.iconView.hidden = YES; + } + self.titleLabel.textColor = [self textColor]; [self applyTitleForInteraction:interaction label:self.titleLabel transaction:transaction]; - CGSize titleSize = [self titleSize]; + if (self.action) { + [self.button setTitle:self.action.title forState:UIControlStateNormal]; + UIFont *buttonFont = UIFont.ows_dynamicTypeSubheadlineFont.ows_mediumWeight; + self.button.titleLabel.font = buttonFont; + self.button.hidden = NO; + } else { + self.button.hidden = YES; + } + CGSize buttonSize = [self.button sizeThatFits:CGSizeZero]; + [NSLayoutConstraint deactivateConstraints:self.layoutConstraints]; self.layoutConstraints = @[ [self.titleLabel autoSetDimension:ALDimensionWidth toSize:titleSize.width], - [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.topVMargin], - [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.bottomVMargin], - // H-center the stack. - [self.stackView autoHCenterInSuperview], - [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeLeading - withInset:self.conversationStyle.fullWidthGutterLeading - relation:NSLayoutRelationGreaterThanOrEqual], - [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTrailing - withInset:self.conversationStyle.fullWidthGutterTrailing - relation:NSLayoutRelationGreaterThanOrEqual], + + [self.button autoSetDimension:ALDimensionWidth toSize:buttonSize.width + self.buttonHPadding * 2.f], + + [self.vStackView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.topVMargin], + [self.vStackView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.bottomVMargin], + [self.vStackView autoPinEdgeToSuperviewEdge:ALEdgeLeading + withInset:self.conversationStyle.fullWidthGutterLeading], + [self.vStackView autoPinEdgeToSuperviewEdge:ALEdgeTrailing + withInset:self.conversationStyle.fullWidthGutterTrailing], ]; } @@ -131,11 +207,10 @@ NS_ASSUME_NONNULL_BEGIN return [UIColor ows_light60Color]; } -- (UIImage *)iconForInteraction:(TSInteraction *)interaction +- (nullable UIImage *)iconForInteraction:(TSInteraction *)interaction { UIImage *result = nil; - // TODO: Don't cast. if ([interaction isKindOfClass:[TSErrorMessage class]]) { switch (((TSErrorMessage *)interaction).errorType) { case TSErrorMessageNonBlockingIdentityChange: @@ -150,8 +225,7 @@ NS_ASSUME_NONNULL_BEGIN case TSErrorMessageInvalidVersion: case TSErrorMessageUnknownContactBlockOffer: case TSErrorMessageGroupCreationFailed: - result = [UIImage imageNamed:@"system_message_info"]; - break; + return nil; } } else if ([interaction isKindOfClass:[TSInfoMessage class]]) { switch (((TSInfoMessage *)interaction).messageType) { @@ -161,30 +235,14 @@ NS_ASSUME_NONNULL_BEGIN case TSInfoMessageAddToContactsOffer: case TSInfoMessageAddUserToProfileWhitelistOffer: case TSInfoMessageAddGroupToProfileWhitelistOffer: - result = [UIImage imageNamed:@"system_message_info"]; - break; case TSInfoMessageTypeGroupUpdate: case TSInfoMessageTypeGroupQuit: - result = [UIImage imageNamed:@"system_message_group"]; - break; case TSInfoMessageTypeDisappearingMessagesUpdate: - result = [UIImage imageNamed:@"ic_timer"]; - break; case TSInfoMessageVerificationStateChange: - result = [UIImage imageNamed:@"system_message_verified"]; - - OWSAssert([interaction isKindOfClass:[OWSVerificationStateChangeMessage class]]); - if ([interaction isKindOfClass:[OWSVerificationStateChangeMessage class]]) { - OWSVerificationStateChangeMessage *message = (OWSVerificationStateChangeMessage *)interaction; - BOOL isVerified = message.verificationState == OWSVerificationStateVerified; - if (!isVerified) { - result = [UIImage imageNamed:@"system_message_info"]; - } - } - break; + return nil; } } else if ([interaction isKindOfClass:[TSCall class]]) { - result = [UIImage imageNamed:@"system_message_call"]; + return nil; } else { OWSFail(@"Unknown interaction type: %@", [interaction class]); return nil; @@ -268,9 +326,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(self.conversationStyle); OWSAssert(self.viewItem); - CGFloat hMargins = (self.conversationStyle.fullWidthGutterLeading + self.conversationStyle.fullWidthGutterTrailing); - CGFloat maxTitleWidth - = (CGFloat)floor(self.conversationStyle.fullWidthContentWidth - (hMargins + self.iconSize + self.hSpacing)); + CGFloat maxTitleWidth = (CGFloat)floor(self.conversationStyle.fullWidthContentWidth); return [self.titleLabel sizeThatFits:CGSizeMake(maxTitleWidth, CGFLOAT_MAX)]; } @@ -283,11 +339,21 @@ NS_ASSUME_NONNULL_BEGIN CGSize result = CGSizeMake(self.conversationStyle.viewWidth, 0); - [self applyTitleForInteraction:interaction label:self.titleLabel transaction:transaction]; + UIImage *_Nullable icon = [self iconForInteraction:interaction]; + if (icon) { + result.height += self.iconSize + self.iconVSpacing; + } + [self applyTitleForInteraction:interaction label:self.titleLabel transaction:transaction]; CGSize titleSize = [self titleSize]; - CGFloat contentHeight = ceil(MAX([self iconSize], titleSize.height)); - result.height = (contentHeight + self.topVMargin + self.bottomVMargin); + result.height += titleSize.height; + + SystemMessageAction *_Nullable action = [self actionForInteraction:interaction]; + if (action) { + result.height += self.buttonHeight + self.buttonVSpacing; + } + + result.height += self.topVMargin + self.bottomVMargin; return result; } @@ -334,19 +400,153 @@ NS_ASSUME_NONNULL_BEGIN return YES; } -#pragma mark - Gesture recognizers +#pragma mark - Actions -- (void)handleTapGesture:(UITapGestureRecognizer *)sender +- (nullable SystemMessageAction *)actionForInteraction:(TSInteraction *)interaction { - OWSAssert(self.delegate); + OWSAssertIsOnMainThread(); + OWSAssert(interaction); + + if ([interaction isKindOfClass:[TSErrorMessage class]]) { + return [self actionForErrorMessage:(TSErrorMessage *)interaction]; + } else if ([interaction isKindOfClass:[TSInfoMessage class]]) { + return [self actionForInfoMessage:(TSInfoMessage *)interaction]; + } else if ([interaction isKindOfClass:[TSCall class]]) { + return [self actionForCall:(TSCall *)interaction]; + } else { + OWSFail(@"Tap for system messages of unknown type: %@", [interaction class]); + return nil; + } +} + +- (nullable SystemMessageAction *)actionForErrorMessage:(TSErrorMessage *)message +{ + OWSAssert(message); + + __weak OWSSystemMessageCell *weakSelf = self; + switch (message.errorType) { + case TSErrorMessageInvalidKeyException: + return nil; + case TSErrorMessageNonBlockingIdentityChange: + return [SystemMessageAction + actionWithTitle:NSLocalizedString(@"SYSTEM_MESSAGE_ACTION_VERIFY_SAFETY_NUMBER", + @"Label for button to verify a user's safety number.") + block:^{ + [weakSelf.delegate tappedNonBlockingIdentityChangeForRecipientId:message.recipientId]; + }]; + case TSErrorMessageWrongTrustedIdentityKey: + return [SystemMessageAction + actionWithTitle:NSLocalizedString(@"SYSTEM_MESSAGE_ACTION_VERIFY_SAFETY_NUMBER", + @"Label for button to verify a user's safety number.") + block:^{ + [weakSelf.delegate + tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)message]; + }]; + case TSErrorMessageMissingKeyId: + case TSErrorMessageNoSession: + return nil; + case TSErrorMessageInvalidMessage: + return [SystemMessageAction actionWithTitle:NSLocalizedString(@"FINGERPRINT_SHRED_KEYMATERIAL_BUTTON", @"") + block:^{ + [weakSelf.delegate tappedCorruptedMessage:message]; + }]; + case TSErrorMessageDuplicateMessage: + case TSErrorMessageInvalidVersion: + return nil; + case TSErrorMessageUnknownContactBlockOffer: + OWSFail(@"TSErrorMessageUnknownContactBlockOffer"); + return nil; + case TSErrorMessageGroupCreationFailed: + return [SystemMessageAction actionWithTitle:CommonStrings.retryButton + block:^{ + [weakSelf.delegate resendGroupUpdateForErrorMessage:message]; + }]; + } + + DDLogWarn(@"%@ Unhandled tap for error message:%@", self.logTag, message); +} - if (sender.state == UIGestureRecognizerStateRecognized) { - TSInteraction *interaction = self.viewItem.interaction; - OWSAssert(interaction); - [self.delegate didTapSystemMessageWithInteraction:interaction]; +- (nullable SystemMessageAction *)actionForInfoMessage:(TSInfoMessage *)message +{ + OWSAssert(message); + + __weak OWSSystemMessageCell *weakSelf = self; + switch (message.messageType) { + case TSInfoMessageUserNotRegistered: + case TSInfoMessageTypeSessionDidEnd: + return nil; + case TSInfoMessageTypeUnsupportedMessage: + // Unused. + return nil; + case TSInfoMessageAddToContactsOffer: + // Unused. + OWSFail(@"TSInfoMessageAddToContactsOffer"); + return nil; + case TSInfoMessageAddUserToProfileWhitelistOffer: + // Unused. + OWSFail(@"TSInfoMessageAddUserToProfileWhitelistOffer"); + return nil; + case TSInfoMessageAddGroupToProfileWhitelistOffer: + // Unused. + OWSFail(@"TSInfoMessageAddGroupToProfileWhitelistOffer"); + return nil; + case TSInfoMessageTypeGroupUpdate: + return [SystemMessageAction + actionWithTitle:NSLocalizedString(@"CONVERSATION_SETTINGS", @"title for conversation settings screen") + block:^{ + [weakSelf.delegate showConversationSettings]; + }]; + case TSInfoMessageTypeGroupQuit: + return nil; + case TSInfoMessageTypeDisappearingMessagesUpdate: + return [SystemMessageAction + actionWithTitle:NSLocalizedString(@"CONVERSATION_SETTINGS", @"title for conversation settings screen") + block:^{ + [weakSelf.delegate showConversationSettings]; + }]; + case TSInfoMessageVerificationStateChange: + return [SystemMessageAction + actionWithTitle:NSLocalizedString(@"SHOW_SAFETY_NUMBER_ACTION", @"Action sheet item") + block:^{ + [weakSelf.delegate + showFingerprintWithRecipientId:((OWSVerificationStateChangeMessage *)message) + .recipientId]; + }]; } + + DDLogInfo(@"%@ Unhandled tap for info message:%@", self.logTag, message); } +- (nullable SystemMessageAction *)actionForCall:(TSCall *)call +{ + OWSAssert(call); + + __weak OWSSystemMessageCell *weakSelf = self; + switch (call.callType) { + case RPRecentCallTypeIncoming: + case RPRecentCallTypeIncomingMissed: + case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity: + case RPRecentCallTypeIncomingDeclined: + return + [SystemMessageAction actionWithTitle:NSLocalizedString(@"CALLBACK_BUTTON_TITLE", @"notification action") + block:^{ + [weakSelf.delegate handleCallTap:call]; + }]; + case RPRecentCallTypeOutgoing: + case RPRecentCallTypeOutgoingMissed: + return [SystemMessageAction actionWithTitle:NSLocalizedString(@"CALL_AGAIN_BUTTON_TITLE", + @"Label for button that lets users call a contact again.") + block:^{ + [weakSelf.delegate handleCallTap:call]; + }]; + case RPRecentCallTypeOutgoingIncomplete: + case RPRecentCallTypeIncomingIncomplete: + return nil; + } +} + +#pragma mark - Events + - (void)handleLongPressGesture:(UILongPressGestureRecognizer *)longPress { OWSAssert(self.delegate); @@ -359,6 +559,24 @@ NS_ASSUME_NONNULL_BEGIN } } +- (void)buttonWasPressed:(id)sender +{ + if (!self.action.block) { + OWSFail(@"%@ Missing action", self.logTag); + } else { + self.action.block(); + } +} + +#pragma mark - Reuse + +- (void)prepareForReuse +{ + [super prepareForReuse]; + + self.action = nil; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index f3a408ae4..a61cfb319 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -1863,45 +1863,6 @@ typedef enum : NSUInteger { [self presentViewController:actionSheetController animated:YES completion:nil]; } -- (void)handleErrorMessageTap:(TSErrorMessage *)message -{ - OWSAssert(message); - - switch (message.errorType) { - case TSErrorMessageInvalidKeyException: - break; - case TSErrorMessageNonBlockingIdentityChange: - [self tappedNonBlockingIdentityChangeForRecipientId:message.recipientId]; - return; - case TSErrorMessageWrongTrustedIdentityKey: - OWSAssert([message isKindOfClass:[TSInvalidIdentityKeyErrorMessage class]]); - [self tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)message]; - return; - case TSErrorMessageMissingKeyId: - // Unused. - break; - case TSErrorMessageNoSession: - break; - case TSErrorMessageInvalidMessage: - [self tappedCorruptedMessage:message]; - return; - case TSErrorMessageDuplicateMessage: - // Unused. - break; - case TSErrorMessageInvalidVersion: - break; - case TSErrorMessageUnknownContactBlockOffer: - // Unused. - OWSFail(@"TSErrorMessageUnknownContactBlockOffer"); - return; - case TSErrorMessageGroupCreationFailed: - [self resendGroupUpdateForErrorMessage:message]; - return; - } - - DDLogWarn(@"%@ Unhandled tap for error message:%@", self.logTag, message); -} - - (void)tappedNonBlockingIdentityChangeForRecipientId:(nullable NSString *)signalId { if (signalId == nil) { @@ -1920,46 +1881,6 @@ typedef enum : NSUInteger { [self showFingerprintWithRecipientId:signalId]; } -- (void)handleInfoMessageTap:(TSInfoMessage *)message -{ - OWSAssert(message); - - switch (message.messageType) { - case TSInfoMessageUserNotRegistered: - break; - case TSInfoMessageTypeSessionDidEnd: - break; - case TSInfoMessageTypeUnsupportedMessage: - // Unused. - break; - case TSInfoMessageAddToContactsOffer: - // Unused. - OWSFail(@"TSInfoMessageAddToContactsOffer"); - return; - case TSInfoMessageAddUserToProfileWhitelistOffer: - // Unused. - OWSFail(@"TSInfoMessageAddUserToProfileWhitelistOffer"); - return; - case TSInfoMessageAddGroupToProfileWhitelistOffer: - // Unused. - OWSFail(@"TSInfoMessageAddGroupToProfileWhitelistOffer"); - return; - case TSInfoMessageTypeGroupUpdate: - [self showConversationSettings]; - return; - case TSInfoMessageTypeGroupQuit: - break; - case TSInfoMessageTypeDisappearingMessagesUpdate: - [self showConversationSettings]; - return; - case TSInfoMessageVerificationStateChange: - [self showFingerprintWithRecipientId:((OWSVerificationStateChangeMessage *)message).recipientId]; - break; - } - - DDLogInfo(@"%@ Unhandled tap for info message:%@", self.logTag, message); -} - - (void)tappedCorruptedMessage:(TSErrorMessage *)message { NSString *alertMessage = [NSString @@ -2518,24 +2439,6 @@ typedef enum : NSUInteger { [self.inputToolbar beginEditingTextMessage]; } -#pragma mark - System Messages - -- (void)didTapSystemMessageWithInteraction:(TSInteraction *)interaction -{ - OWSAssertIsOnMainThread(); - OWSAssert(interaction); - - if ([interaction isKindOfClass:[TSErrorMessage class]]) { - [self handleErrorMessageTap:(TSErrorMessage *)interaction]; - } else if ([interaction isKindOfClass:[TSInfoMessage class]]) { - [self handleInfoMessageTap:(TSInfoMessage *)interaction]; - } else if ([interaction isKindOfClass:[TSCall class]]) { - [self handleCallTap:(TSCall *)interaction]; - } else { - OWSFail(@"Tap for system messages of unknown type: %@", [interaction class]); - } -} - #pragma mark - ContactEditingDelegate - (void)didFinishEditingContact diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index c2fd38668..cfc010998 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -3411,11 +3411,11 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac inThread:contactThread]]; [result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] withCallNumber:@"+19174054215" - callType:RPRecentCallTypeMissed + callType:RPRecentCallTypeIncomingMissed inThread:contactThread]]; [result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] withCallNumber:@"+19174054215" - callType:RPRecentCallTypeMissedBecauseOfChangedIdentity + callType:RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity inThread:contactThread]]; [result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] withCallNumber:@"+19174054215" @@ -3425,6 +3425,10 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac withCallNumber:@"+19174054215" callType:RPRecentCallTypeIncomingIncomplete inThread:contactThread]]; + [result addObject:[[TSCall alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + withCallNumber:@"+19174054215" + callType:RPRecentCallTypeOutgoingMissed + inThread:contactThread]]; } { diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 481a2db15..b991847ba 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -515,12 +515,12 @@ private class SignalCallData: NSObject { // Insert missed call record if let callRecord = call.callRecord { if callRecord.callType == RPRecentCallTypeIncoming { - callRecord.updateCallType(RPRecentCallTypeMissed) + callRecord.updateCallType(RPRecentCallTypeIncomingMissed) } } else { call.callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(), withCallNumber: call.thread.contactIdentifier(), - callType: RPRecentCallTypeMissed, + callType: RPRecentCallTypeIncomingMissed, in: call.thread) } @@ -602,7 +602,7 @@ private class SignalCallData: NSObject { let callRecord = TSCall(timestamp: NSDate.ows_millisecondTimeStamp(), withCallNumber: thread.contactIdentifier(), - callType: RPRecentCallTypeMissedBecauseOfChangedIdentity, + callType: RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity, in: thread) assert(newCall.callRecord == nil) newCall.callRecord = callRecord @@ -1107,6 +1107,14 @@ private class SignalCallData: NSObject { call.state = .localHangup + if let callRecord = call.callRecord { + if callRecord.callType == RPRecentCallTypeOutgoingIncomplete { + callRecord.updateCallType(RPRecentCallTypeOutgoingMissed) + } + } else { + owsFail("\(self.logTag) missing call record in \(#function)") + } + // TODO something like this lifted from Signal-Android. // this.accountManager.cancelInFlightRequests(); // this.messageSender.cancelInFlightRequests(); diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index a0e5686bf..65cb7ad18 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -284,6 +284,9 @@ /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Select"; +/* Label for button that lets users call a contact again. */ +"CALL_AGAIN_BUTTON_TITLE" = "Call Again"; + /* Alert message when calling and permissions for microphone are missing */ "CALL_AUDIO_PERMISSION_MESSAGE" = "Signal requires access to your microphone to make calls and record voice messages. You can grant this permission in the Settings app."; @@ -871,7 +874,7 @@ "ERROR_MESSAGE_INVALID_KEY_EXCEPTION" = "The recipient's key is not valid."; /* No comment provided by engineer. */ -"ERROR_MESSAGE_INVALID_MESSAGE" = "Received message was out of sync. Tap to reset your secure session."; +"ERROR_MESSAGE_INVALID_MESSAGE" = "Received message was out of sync."; /* No comment provided by engineer. */ "ERROR_MESSAGE_INVALID_VERSION" = "Received a message not compatible with this version."; @@ -889,7 +892,7 @@ "ERROR_MESSAGE_UNKNOWN_ERROR" = "An unknown error occurred."; /* No comment provided by engineer. */ -"ERROR_MESSAGE_WRONG_TRUSTED_IDENTITY_KEY" = "Safety number changed. Tap to verify."; +"ERROR_MESSAGE_WRONG_TRUSTED_IDENTITY_KEY" = "Safety number changed."; /* Format string for 'unregistered user' error. Embeds {{the unregistered user's name or signal id}}. */ "ERROR_UNREGISTERED_USER_FORMAT" = "Unregistered User: %@"; @@ -1054,7 +1057,7 @@ "INCOMING_DECLINED_CALL" = "You declined a call"; /* No comment provided by engineer. */ -"INCOMING_INCOMPLETE_CALL" = "Incomplete incoming call from"; +"INCOMING_INCOMPLETE_CALL" = "Incoming call"; /* info message text shown in conversation view */ "INFO_MESSAGE_MISSED_CALL_DUE_TO_CHANGED_IDENITY" = "Missed call because their safety number has changed."; @@ -1425,7 +1428,10 @@ "OUTGOING_CALL" = "Outgoing call"; /* No comment provided by engineer. */ -"OUTGOING_INCOMPLETE_CALL" = "Unanswered outgoing call"; +"OUTGOING_INCOMPLETE_CALL" = "Outgoing call"; + +/* info message recorded in conversation history when local user tries and fails to call another user. */ +"OUTGOING_MISSED_CALL" = "Unanswered outgoing call"; /* A display format for oversize text messages. */ "OVERSIZE_TEXT_DISPLAY_FORMAT" = "%@…"; @@ -2120,6 +2126,9 @@ /* No comment provided by engineer. */ "SUCCESSFUL_VERIFICATION_TITLE" = "Safety Number Matches!"; +/* Label for button to verify a user's safety number. */ +"SYSTEM_MESSAGE_ACTION_VERIFY_SAFETY_NUMBER" = "Verify Safety Number"; + /* {{number of days}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 days}}'. See other *_TIME_AMOUNT strings */ "TIME_AMOUNT_DAYS" = "%@ days"; diff --git a/SignalServiceKit/src/Messages/OWSIncompleteCallsJob.h b/SignalServiceKit/src/Messages/OWSIncompleteCallsJob.h new file mode 100644 index 000000000..76c401a44 --- /dev/null +++ b/SignalServiceKit/src/Messages/OWSIncompleteCallsJob.h @@ -0,0 +1,29 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@class OWSPrimaryStorage; +@class OWSStorage; + +@interface OWSIncompleteCallsJob : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER; + +- (void)run; + ++ (NSString *)databaseExtensionName; ++ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage; + +#ifdef DEBUG +/** + * Only use the sync version for testing, generally we'll want to register extensions async + */ +- (void)blockingRegisterDatabaseExtensions; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/OWSIncompleteCallsJob.m b/SignalServiceKit/src/Messages/OWSIncompleteCallsJob.m new file mode 100644 index 000000000..5592bf8d1 --- /dev/null +++ b/SignalServiceKit/src/Messages/OWSIncompleteCallsJob.m @@ -0,0 +1,150 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSIncompleteCallsJob.h" +#import "OWSPrimaryStorage.h" +#import "TSCall.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const OWSIncompleteCallsJobCallTypeColumn = @"call_type"; +static NSString *const OWSIncompleteCallsJobCallTypeIndex = @"index_calls_on_call_type"; + +@interface OWSIncompleteCallsJob () + +@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; + +@end + +#pragma mark - + +@implementation OWSIncompleteCallsJob + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + if (!self) { + return self; + } + + _primaryStorage = primaryStorage; + + return self; +} + +- (NSArray *)fetchIncompleteCallIdsWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssert(transaction); + + NSMutableArray *messageIds = [NSMutableArray new]; + + NSString *formattedString = [NSString stringWithFormat:@"WHERE %@ == %d OR %@ == %d", + OWSIncompleteCallsJobCallTypeColumn, + (int)RPRecentCallTypeOutgoingIncomplete, + OWSIncompleteCallsJobCallTypeColumn, + (int)RPRecentCallTypeIncomingIncomplete]; + YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString]; + [[transaction ext:OWSIncompleteCallsJobCallTypeIndex] + enumerateKeysMatchingQuery:query + usingBlock:^void(NSString *collection, NSString *key, BOOL *stop) { + [messageIds addObject:key]; + }]; + + return [messageIds copy]; +} + +- (void)enumerateIncompleteCallsWithBlock:(void (^)(TSCall *call))block + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssert(transaction); + + // Since we can't directly mutate the enumerated "incomplete" calls, we store only their ids in hopes + // of saving a little memory and then enumerate the (larger) TSCall objects one at a time. + for (NSString *callId in [self fetchIncompleteCallIdsWithTransaction:transaction]) { + TSCall *_Nullable call = [TSCall fetchObjectWithUniqueID:callId transaction:transaction]; + if ([call isKindOfClass:[TSCall class]]) { + block(call); + } else { + DDLogError(@"%@ unexpected object: %@", self.logTag, call); + } + } +} + +- (void)run +{ + __block uint count = 0; + + [[self.primaryStorage newDatabaseConnection] readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self + enumerateIncompleteCallsWithBlock:^(TSCall *call) { + if (call.callType == RPRecentCallTypeOutgoingIncomplete) { + DDLogDebug(@"%@ marking call as missed: %@", self.logTag, call.uniqueId); + [call updateCallType:RPRecentCallTypeOutgoingMissed transaction:transaction]; + OWSAssert(call.callType == RPRecentCallTypeOutgoingMissed); + } else if (call.callType == RPRecentCallTypeIncomingIncomplete) { + DDLogDebug(@"%@ marking call as missed: %@", self.logTag, call.uniqueId); + [call updateCallType:RPRecentCallTypeIncomingMissed transaction:transaction]; + OWSAssert(call.callType == RPRecentCallTypeIncomingMissed); + } else { + OWSProdLogAndFail( + @"%@ call has unexpected call type: %@", self.logTag, NSStringFromCallType(call.callType)); + return; + } + count++; + } + transaction:transaction]; + }]; + + DDLogDebug(@"%@ Marked %u calls as missed", self.logTag, count); +} + +#pragma mark - YapDatabaseExtension + ++ (YapDatabaseSecondaryIndex *)indexDatabaseExtension +{ + YapDatabaseSecondaryIndexSetup *setup = [YapDatabaseSecondaryIndexSetup new]; + [setup addColumn:OWSIncompleteCallsJobCallTypeColumn withType:YapDatabaseSecondaryIndexTypeInteger]; + + YapDatabaseSecondaryIndexHandler *handler = + [YapDatabaseSecondaryIndexHandler withObjectBlock:^(YapDatabaseReadTransaction *transaction, + NSMutableDictionary *dict, + NSString *collection, + NSString *key, + id object) { + if (![object isKindOfClass:[TSCall class]]) { + return; + } + TSCall *call = (TSCall *)object; + + dict[OWSIncompleteCallsJobCallTypeColumn] = @(call.callType); + }]; + + return [[YapDatabaseSecondaryIndex alloc] initWithSetup:setup handler:handler versionTag:nil]; +} + +#ifdef DEBUG +// Useful for tests, don't use in app startup path because it's slow. +- (void)blockingRegisterDatabaseExtensions +{ + [self.primaryStorage registerExtension:[self.class indexDatabaseExtension] + withName:OWSIncompleteCallsJobCallTypeIndex]; +} +#endif + ++ (NSString *)databaseExtensionName +{ + return OWSIncompleteCallsJobCallTypeIndex; +} + ++ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage +{ + [storage asyncRegisterExtension:[self indexDatabaseExtension] withName:OWSIncompleteCallsJobCallTypeIndex]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/TSCall.h b/SignalServiceKit/src/Messages/TSCall.h index fdca7d4d1..b7c26b366 100644 --- a/SignalServiceKit/src/Messages/TSCall.h +++ b/SignalServiceKit/src/Messages/TSCall.h @@ -12,14 +12,17 @@ NS_ASSUME_NONNULL_BEGIN typedef enum { RPRecentCallTypeIncoming = 1, RPRecentCallTypeOutgoing, - RPRecentCallTypeMissed, + RPRecentCallTypeIncomingMissed, // These call types are used until the call connects. RPRecentCallTypeOutgoingIncomplete, RPRecentCallTypeIncomingIncomplete, - RPRecentCallTypeMissedBecauseOfChangedIdentity, - RPRecentCallTypeIncomingDeclined + RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity, + RPRecentCallTypeIncomingDeclined, + RPRecentCallTypeOutgoingMissed, } RPRecentCallType; +NSString *NSStringFromCallType(RPRecentCallType callType); + @interface TSCall : TSInteraction @property (nonatomic, readonly) RPRecentCallType callType; @@ -34,6 +37,7 @@ typedef enum { - (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; - (void)updateCallType:(RPRecentCallType)callType; +- (void)updateCallType:(RPRecentCallType)callType transaction:(YapDatabaseReadWriteTransaction *)transaction; @end diff --git a/SignalServiceKit/src/Messages/TSCall.m b/SignalServiceKit/src/Messages/TSCall.m index 20d805c09..706afc528 100644 --- a/SignalServiceKit/src/Messages/TSCall.m +++ b/SignalServiceKit/src/Messages/TSCall.m @@ -9,6 +9,28 @@ NS_ASSUME_NONNULL_BEGIN +NSString *NSStringFromCallType(RPRecentCallType callType) +{ + switch (callType) { + case RPRecentCallTypeIncoming: + return @"RPRecentCallTypeIncoming"; + case RPRecentCallTypeOutgoing: + return @"RPRecentCallTypeOutgoing"; + case RPRecentCallTypeIncomingMissed: + return @"RPRecentCallTypeIncomingMissed"; + case RPRecentCallTypeOutgoingIncomplete: + return @"RPRecentCallTypeOutgoingIncomplete"; + case RPRecentCallTypeIncomingIncomplete: + return @"RPRecentCallTypeIncomingIncomplete"; + case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity: + return @"RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity"; + case RPRecentCallTypeIncomingDeclined: + return @"RPRecentCallTypeIncomingDeclined"; + case RPRecentCallTypeOutgoingMissed: + return @"RPRecentCallTypeOutgoingMissed"; + } +} + NSUInteger TSCallCurrentSchemaVersion = 1; @interface TSCall () @@ -36,7 +58,11 @@ NSUInteger TSCallCurrentSchemaVersion = 1; _callSchemaVersion = TSCallCurrentSchemaVersion; _callType = callType; - if (_callType == RPRecentCallTypeMissed || _callType == RPRecentCallTypeMissedBecauseOfChangedIdentity) { + + // Ensure users are notified of missed calls. + BOOL isIncomingMissed = (_callType == RPRecentCallTypeIncomingMissed + || _callType == RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity); + if (isIncomingMissed) { _read = NO; } else { _read = YES; @@ -75,17 +101,20 @@ NSUInteger TSCallCurrentSchemaVersion = 1; return NSLocalizedString(@"INCOMING_CALL", @""); case RPRecentCallTypeOutgoing: return NSLocalizedString(@"OUTGOING_CALL", @""); - case RPRecentCallTypeMissed: + case RPRecentCallTypeIncomingMissed: return NSLocalizedString(@"MISSED_CALL", @""); case RPRecentCallTypeOutgoingIncomplete: return NSLocalizedString(@"OUTGOING_INCOMPLETE_CALL", @""); case RPRecentCallTypeIncomingIncomplete: return NSLocalizedString(@"INCOMING_INCOMPLETE_CALL", @""); - case RPRecentCallTypeMissedBecauseOfChangedIdentity: + case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity: return NSLocalizedString(@"INFO_MESSAGE_MISSED_CALL_DUE_TO_CHANGED_IDENITY", @"info message text shown in conversation view"); case RPRecentCallTypeIncomingDeclined: return NSLocalizedString(@"INCOMING_DECLINED_CALL", @"info message recorded in conversation history when local user declined a call"); + case RPRecentCallTypeOutgoingMissed: + return NSLocalizedString(@"OUTGOING_MISSED_CALL", + @"info message recorded in conversation history when local user tries and fails to call another user."); } } @@ -125,20 +154,28 @@ NSUInteger TSCallCurrentSchemaVersion = 1; - (void)updateCallType:(RPRecentCallType)callType { - DDLogInfo(@"%@ updating call type of call: %d with uniqueId: %@ which has timestamp: %llu", + [self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self updateCallType:callType transaction:transaction]; + }]; +} + +- (void)updateCallType:(RPRecentCallType)callType transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssert(transaction); + + DDLogInfo(@"%@ updating call type of call: %@ -> %@ with uniqueId: %@ which has timestamp: %llu", self.logTag, - (int)self.callType, + NSStringFromCallType(_callType), + NSStringFromCallType(callType), self.uniqueId, self.timestamp); _callType = callType; - [self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { - [self saveWithTransaction:transaction]; - - // redraw any thread-related unread count UI. - [self touchThreadWithTransaction:transaction]; - }]; + [self saveWithTransaction:transaction]; + + // redraw any thread-related unread count UI. + [self touchThreadWithTransaction:transaction]; } @end diff --git a/SignalServiceKit/src/Storage/OWSPrimaryStorage.m b/SignalServiceKit/src/Storage/OWSPrimaryStorage.m index d3f43d360..9cfacba44 100644 --- a/SignalServiceKit/src/Storage/OWSPrimaryStorage.m +++ b/SignalServiceKit/src/Storage/OWSPrimaryStorage.m @@ -11,6 +11,7 @@ #import "OWSFailedMessagesJob.h" #import "OWSFileSystem.h" #import "OWSIncomingMessageFinder.h" +#import "OWSIncompleteCallsJob.h" #import "OWSMediaGalleryFinder.h" #import "OWSMessageReceiver.h" #import "OWSStorage+Subclass.h" @@ -64,9 +65,10 @@ void RunAsyncRegistrationsForStorage(OWSStorage *storage, dispatch_block_t compl [TSDatabaseView asyncRegisterSecondaryDevicesDatabaseView:storage]; [OWSDisappearingMessagesFinder asyncRegisterDatabaseExtensions:storage]; [OWSFailedMessagesJob asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage]; + [OWSIncompleteCallsJob asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage]; [OWSFailedAttachmentDownloadsJob asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage]; [OWSMediaGalleryFinder asyncRegisterDatabaseExtensionsWithPrimaryStorage:storage]; - + // NOTE: Always pass the completion to the _LAST_ of the async database // view registrations. [TSDatabaseView asyncRegisterLazyRestoreAttachmentsDatabaseView:storage completion:completion]; diff --git a/SignalServiceKit/src/Storage/OWSStorage.m b/SignalServiceKit/src/Storage/OWSStorage.m index 4ed028586..459e10606 100644 --- a/SignalServiceKit/src/Storage/OWSStorage.m +++ b/SignalServiceKit/src/Storage/OWSStorage.m @@ -234,7 +234,11 @@ NSString *const kNSUserDefaults_DatabaseExtensionVersionMap = @"kNSUserDefaults_ cannotDecodeObjectOfClassName:(NSString *)name originalClasses:(NSArray *)classNames { - OWSProdLogAndFail(@"%@ Could not decode object: %@", self.logTag, name); + if ([name isEqualToString:@"TSRecipient"]) { + DDLogError(@"%@ Could not decode object: %@", self.logTag, name); + } else { + OWSProdLogAndFail(@"%@ Could not decode object: %@", self.logTag, name); + } OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotDecodeClass]); return [OWSUnknownDBObject class]; }