From fd5fbe47e8e067e3e7af05d1780c404ca62426b0 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 27 Mar 2019 17:06:57 -0300 Subject: [PATCH] Use FlatList in RoomView (#762) --- .../__snapshots__/Storyshots.test.js.snap | 2260 ++++++----------- .../res/drawable-xxxhdpi/message_empty.png | Bin 0 -> 77516 bytes app/actions/actionsTypes.js | 3 +- app/actions/room.js | 7 - app/constants/colors.js | 4 +- app/containers/message/Message.js | 2 +- app/containers/message/index.js | 1 - app/containers/message/styles.js | 11 +- app/lib/rocketchat.js | 34 +- app/presentation/RoomItem.js | 2 +- app/reducers/room.js | 8 +- app/views/LegalView.js | 5 +- app/views/LoginSignupView.js | 3 +- app/views/LoginView.js | 2 +- app/views/RoomActionsView/styles.js | 3 +- app/views/RoomInfoEditView/styles.js | 4 +- app/views/RoomMembersView/styles.js | 3 +- app/views/RoomView/EmptyRoom.js | 23 + app/views/RoomView/Header/Icon.js | 50 + app/views/RoomView/Header/index.js | 71 +- app/views/RoomView/List.js | 138 + app/views/RoomView/ListView.js | 277 -- app/views/RoomView/ScrollBottomButton.js | 60 + app/views/RoomView/Separator.js | 11 +- app/views/RoomView/UploadProgress.js | 3 +- app/views/RoomView/index.js | 68 +- app/views/RoomView/styles.js | 12 +- app/views/RoomsListView/index.js | 3 +- app/views/RoomsListView/styles.js | 11 +- app/views/SearchMessagesView/styles.js | 3 +- app/views/SidebarView/styles.js | 3 +- e2e/07-createroom.spec.js | 12 +- e2e/12-broadcast.spec.js | 8 +- e2e/data.js | 2 +- storybook/stories/Message.js | 109 +- 35 files changed, 1311 insertions(+), 1905 deletions(-) create mode 100644 android/app/src/main/res/drawable-xxxhdpi/message_empty.png create mode 100644 app/views/RoomView/EmptyRoom.js create mode 100644 app/views/RoomView/Header/Icon.js create mode 100644 app/views/RoomView/List.js delete mode 100644 app/views/RoomView/ListView.js create mode 100644 app/views/RoomView/ScrollBottomButton.js diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 7757aba88..944d06ee6 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -219,15 +219,28 @@ exports[`Storyshots Message list 1`] = ` style={ Object { "flex": 1, - "transform": Array [ - Object { - "scaleY": -1, - }, - ], } } > + + Simple + - Simple + Long message - Long message + Grouped messages - Grouped messages + Without header - Without header + With alias - With alias + Edited - Edited + Static avatar - Static avatar + Full name - Full name + Mentions - Mentions + Emojis - Emojis + Custom Emojis - Custom Emojis + Time format - Time format + Reactions - Reactions + Multiple reactions - Multiple reactions + Intercalated users - Intercalated users + Date and Unread separators @@ -4889,17 +4707,12 @@ exports[`Storyshots Message list 1`] = ` Object { "flex": 1, "flexDirection": "column", - "paddingHorizontal": 15, - "paddingVertical": 5, - "transform": Array [ - Object { - "scaleY": -1, - }, - ], + "paddingHorizontal": 14, + "paddingVertical": 4, "width": "100%", }, Object { - "marginBottom": 10, + "marginTop": 10, }, undefined, undefined, @@ -4923,7 +4736,7 @@ exports[`Storyshots Message list 1`] = ` "width": 36, }, Object { - "marginTop": 5, + "marginTop": 4, }, ] } @@ -5063,14 +4876,9 @@ exports[`Storyshots Message list 1`] = ` Object { "alignItems": "center", "flexDirection": "row", - "marginBottom": 25, - "marginHorizontal": 15, - "marginTop": 15, - "transform": Array [ - Object { - "scaleY": -1, - }, - ], + "marginBottom": 4, + "marginHorizontal": 14, + "marginTop": 16, } } > @@ -5128,13 +4936,8 @@ exports[`Storyshots Message list 1`] = ` Object { "flex": 1, "flexDirection": "column", - "paddingHorizontal": 15, - "paddingVertical": 5, - "transform": Array [ - Object { - "scaleY": -1, - }, - ], + "paddingHorizontal": 14, + "paddingVertical": 4, "width": "100%", }, false, @@ -5219,17 +5022,12 @@ exports[`Storyshots Message list 1`] = ` Object { "flex": 1, "flexDirection": "column", - "paddingHorizontal": 15, - "paddingVertical": 5, - "transform": Array [ - Object { - "scaleY": -1, - }, - ], + "paddingHorizontal": 14, + "paddingVertical": 4, "width": "100%", }, Object { - "marginBottom": 10, + "marginTop": 10, }, undefined, undefined, @@ -5253,7 +5051,7 @@ exports[`Storyshots Message list 1`] = ` "width": 36, }, Object { - "marginTop": 5, + "marginTop": 4, }, ] } @@ -5393,14 +5191,9 @@ exports[`Storyshots Message list 1`] = ` Object { "alignItems": "center", "flexDirection": "row", - "marginBottom": 25, - "marginHorizontal": 15, - "marginTop": 15, - "transform": Array [ - Object { - "scaleY": -1, - }, - ], + "marginBottom": 4, + "marginHorizontal": 14, + "marginTop": 16, } } > @@ -5450,17 +5243,12 @@ exports[`Storyshots Message list 1`] = ` Object { "flex": 1, "flexDirection": "column", - "paddingHorizontal": 15, - "paddingVertical": 5, - "transform": Array [ - Object { - "scaleY": -1, - }, - ], + "paddingHorizontal": 14, + "paddingVertical": 4, "width": "100%", }, Object { - "marginBottom": 10, + "marginTop": 10, }, undefined, undefined, @@ -5484,7 +5272,7 @@ exports[`Storyshots Message list 1`] = ` "width": 36, }, Object { - "marginTop": 5, + "marginTop": 4, }, ] } @@ -5629,18 +5417,13 @@ exports[`Storyshots Message list 1`] = ` "marginTop": 30, }, Object { - "marginBottom": 30, - "marginTop": 0, - "transform": Array [ - Object { - "scaleY": -1, - }, - ], + "marginBottom": 0, + "marginTop": 30, }, ] } > - Date and Unread separators + With image - With image + With video - With video + With audio - With audio - - - - - - - - - - - - - - diego.mello - - - - 10:00 AM - - - - - - - Iā€™m fine! - - - - - View - - - - - - - - - - - - - - - - - diego.mello - - - - 10:00 AM - - - - - - - Iā€™m fine! - - - - - View - - - - - @@ -7678,17 +7023,12 @@ exports[`Storyshots Message list 1`] = ` Object { "flex": 1, "flexDirection": "column", - "paddingHorizontal": 15, - "paddingVertical": 5, - "transform": Array [ - Object { - "scaleY": -1, - }, - ], + "paddingHorizontal": 14, + "paddingVertical": 4, "width": "100%", }, Object { - "marginBottom": 10, + "marginTop": 10, }, undefined, undefined, @@ -7712,7 +7052,7 @@ exports[`Storyshots Message list 1`] = ` "width": 36, }, Object { - "marginTop": 5, + "marginTop": 4, }, ] } @@ -7813,7 +7153,221 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM + + + + + Iā€™m fine! + + + + View + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + + Iā€™m fine! + + + + View @@ -7829,13 +7383,8 @@ exports[`Storyshots Message list 1`] = ` "marginTop": 30, }, Object { - "marginBottom": 30, - "marginTop": 0, - "transform": Array [ - Object { - "scaleY": -1, - }, - ], + "marginBottom": 0, + "marginTop": 30, }, ] } @@ -7862,17 +7411,12 @@ exports[`Storyshots Message list 1`] = ` Object { "flex": 1, "flexDirection": "column", - "paddingHorizontal": 15, - "paddingVertical": 5, - "transform": Array [ - Object { - "scaleY": -1, - }, - ], + "paddingHorizontal": 14, + "paddingVertical": 4, "width": "100%", }, Object { - "marginBottom": 10, + "marginTop": 10, }, undefined, undefined, @@ -7896,7 +7440,7 @@ exports[`Storyshots Message list 1`] = ` "width": 36, }, Object { - "marginTop": 5, + "marginTop": 4, }, ] } @@ -7997,36 +7541,7 @@ exports[`Storyshots Message list 1`] = ` 10:00 AM - - - - - Message - - - - + View View @@ -8042,13 +7557,8 @@ exports[`Storyshots Message list 1`] = ` "marginTop": 30, }, Object { - "marginBottom": 30, - "marginTop": 0, - "transform": Array [ - Object { - "scaleY": -1, - }, - ], + "marginBottom": 0, + "marginTop": 30, }, ] } @@ -8075,17 +7585,12 @@ exports[`Storyshots Message list 1`] = ` Object { "flex": 1, "flexDirection": "column", - "paddingHorizontal": 15, - "paddingVertical": 5, - "transform": Array [ - Object { - "scaleY": -1, - }, - ], + "paddingHorizontal": 14, + "paddingVertical": 4, "width": "100%", }, Object { - "marginBottom": 10, + "marginTop": 10, }, undefined, undefined, @@ -8109,7 +7614,210 @@ exports[`Storyshots Message list 1`] = ` "width": 36, }, Object { - "marginTop": 5, + "marginTop": 4, + }, + ] + } + > + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + + Message + + + + + View + + + + + + Two short custom fields + + + + + - Two short custom fields + Broadcast - Broadcast + Archived - Archived + Error - Error + Temp - Temp + Editing - Editing + Removed - Removed + Joined - Joined + Room name changed - Room name changed + Message pinned - Message pinned + Has left the channel - Has left the channel + User removed - User removed + User added - User added + User muted - User muted + User unmuted - User unmuted + Role added - Role added + Role removed - Role removed + Changed description - Changed description + Changed announcement - Changed announcement + Changed topic - Changed topic + Changed type - Changed type + Custom style - Custom style + Markdown emphasis - Markdown emphasis + Markdown headers - Markdown headers + Markdown links - Markdown links + Markdown image - Markdown image + Markdown code - Markdown code + Markdown quote - Markdown quote + Markdown table - - Markdown table - `; diff --git a/android/app/src/main/res/drawable-xxxhdpi/message_empty.png b/android/app/src/main/res/drawable-xxxhdpi/message_empty.png new file mode 100644 index 0000000000000000000000000000000000000000..d07e5dcf3d63f34e0085711318a065c73204cbb7 GIT binary patch literal 77516 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

vaVC(RGF4W zMCT0VYYH+01rW1GTkj>&45#%@h@rOH%~-0^TuHvW*@R z;)tulY{&TBKYetgnP zQd&6jESrw&=I;<~6BkLxH4QWHk?nR(|HSG^>!zQ7A^Gy2JhnK4Mwnshmwioo{xNb?z zPpTC8XpH4fk(-?%6!5)X2he%eAxYS4rZd@n%*88j9ASnd&VR(eFbDlu$F;{r9v?r7GTzn?7I6V_PY&Ra`7xP;bZPw+R!S1SaxqRIY!&`}d z7ORwRR|Z`UyB?Wu^A4H2A#?5Z2cruNgZ*M`*!Bd#$W_a2%CD z*P~zg_CLYJNbC^2;Mqk04*HF`0^!o$K$uG*V{7drBrF{%+@m2p+EJ>$R~q}~Re#}&N;J>(=o`8F@$?{J(=p*M8A<1KB5wKx_Wbh` zJshPA1nU-p4`v4OdC*crTxIyDTJWuHK?j)Q3N}fu@=JHo%}VE)hO`xX0>}s)SyLlO zTAj9}O~|P+sq}lUHWj^gHBCJS<~M=g_$z@mLq{DJ`l){Td52r-g!`dM=QcVK>?t}a zQ4B!%ViaZv{fw|C(@wr{|K_!&Jzrs#AJb`D3r(#dNgx4-ZI1NNE`FQ40Uw%Vk8Pb> z;UDjblT|S{v#v!rG?p0LbA+#B&t98cU+awt_HoGr5$`^1&)=8hUsKq> zD=RMpaWH`@%ruMMdjl+1Qa3ghw(g6~8_*M^M6Vi4b)`1Vdvo;ezFei)Q&k=;tHCmw z2(`DzLF-0-sjRr~j+;P*XDxelC1@5N8rNvogE#E@6Zx3+_sObi^9Y!rS_M;~qFpfs z9&y)fBS~;Eb5Q0${p&`dJ5VT~&*yNq`Zw!Nw23$x-KIwd+8I~P=t{!rc$DCPWQjJ* z7Y*&>Ky;fbKDf}}mpB^wi=za+Y)LPc)-wLE;}^&M$jUzXwzbiFo0?Oa)V&_Q-KCN& z&Bm>+)C0!OZ|^5<#mj^3QYFNI1^$g7^Zn3o55{6qxX6%s$q3wWZ@H&It`O_O6St8Y z2xHG#6?aMi_IP0#>uZA3o5x&qs8}#ibMPMVo1MPnnF87EHL|9!y%TQv;)cUb49bn^ z*s(}V;D6Cy&08BrY}gwAC1C+AmpCD$ta+2q(ju$4r3mXeCD?EjYLIYavY6*J_>C?4 zMyPR??iF+ELRW!$9(>n6*1tRrvhI|!<3jV|)dg1by!+w{Eg0;%iCf^jaDkIIrM5uT zz6RbY&-12g^h^G;Qc$( zD8F(_Z&6?#nDzd4I6PDRwz@ zqbtW>5`31VQ$|P2OdF&_brmq&CNcg_7n4d51z5qR@T@wEN8K-K@#{b6NdH%qvi7{G zM11cU2dgD3o0qypWwCayoqK_3PUtw}2^@DtAtZU$;c~K94*tw-3k_z&Hcs5uAes2` zP9v-PrJXR{K0O3W_7`FyE%oWd;1)z;yViVrz480N0w4pDFMFpyOa4v86hpZeFezDH zGh8-x%93yE*ZZdR2Qz^{k;cbITl?;QuDZL&1tn~Z(tpk5$A0BFn6Qreql|M}_3I9* z-z`+bli+8^TJQ8MZqj7xfmygHFy9rXDMz)CI#_8XG0r^tofJ!qcfWdaF}SCn%tvpMts!tT0utmZrse~@k`pUA>4{b>a} zltxF)9P*TOCslMG04uq>`=k)7PWpGe{64X6m?pJ^fPll7-Nyz>9y@HrDrtzM%D|cp zNb#_lo$c`!J|m>hFV*tRJq*X}!$V}u+CkiYf|#80Q$^iI&c`7bFDk;x?qD5V$O_fp zEK6_7{t+BvE`FQ5-}CsmP%rO?jk?GsZg1{wL^CPtShUo2`FGP-oPLE6sJfB+?<`4Ofnq$TzJxUwS)n z%e+7q&H-!23-9zpk(zG>@=&T@AbaAP?A4z!RRK@<&c~{;$HP>g$a|h(_y>BS)T=f% zH=GOqdfPs}H9!ho3p1~YiM)@J|3R7T3XBq$dxv?poMO<9RdX5er-CBxE zJBb;!Y|&oNX2MCrm;=Mxv#ZR_gdkG_YGc1A>r;4Ia>f71TH@#73j_I^^8jLbH}&s7a_o?f4=N! za75Pcm)u2K87w$}qiLc{!5J~n83*J|irDenV?`?j^c0UZhkJOon`LW6N-FesvY)3y zj4zraeS&b4Nb?76hAO&-yifu*et*oIdUkZgTF&)o$JI1LLWSh%Oce1dIqZz-VPpE{ z*pK%5;=uFt72ShZOI-cxIfy7T)uoonA;&FkJtuO}ng#Pb)s(epBl#I+(#73JllnXU~q4pUX&m8%A zfFJ9d84Zf43*UY)0Dy+d$LgsfQZ6LBle9u$zWdV`s|v22CoW92=BHc&YI!_2{>p+bgZ zri-KD7C;qm^EfYBM9fkIoh8!&_wHR-KCUb0?u!UHNNc-W0{-O|H2^2)ng3j__K7DK zNYbbL@v}*~L;AOnCjVDRTh7k0$*J`(Ni5sKX zU!=3nP^#@Ty2izuQvTt+%?nvRW(?Id$kP^hKv1a>sp#Cz?FUbYtD9L)TKDj2<$&lX z{d~7iQG#+7R9<34x$T6nFAyt73VEaMwpDgJkOS5NNw2?~pE3C)>hS`!@j?(9UNvO)xJE%HGuC9rTLU8X*wb92hy9kocl5~R;iEgkZW_x4# zDd*5?peZL>I0Udys7{8CvR$|M9?qOz;!!w z?g;ZBSzK$_EJ{2mI-YqOCx!;F8ZY#ocTWFVe-yvB2j*1^HxcT54VNBGJW2XtP^I<< zaeDl_$%S9jchIxjovE<;ho$a+U8)`C&Z&?0akpVxtUsQe{&6L7=%@_I{|i>Q(@tBd zf8}(2onou*X7g?69?W%_<5!}Im-(-8qCbiIo;5WB-_)2=Ry(;oE6Ae!?ctx?y^{E1 zk4L>;^p`ERUCn3OpN?`F+0z9*YzA*$$rPWrX{P3$vmfNz9SX?2iGMb8 zi*!-1V^8WM%K!ROJHNW)@A~1ULz0oYF!i)#>8b)%n3DGo9`fT81ZnY)1}88`|8=MY`u)t#N-r zR5x6Se|0Www)5x~vh~0S^2a7I?QW>GD24|)dfJGBA*0-?X`Zia1wdPRHU!sB$GOSl zZ!A<^yhP4M`M!XFHJ8?;->y_z@Vkjl%g}q7jLv6yGTwXZ=yc1=4`RsAk`UyVFYCeUf`gDn68{#hiNP?c7-!#>;+AF_h?NjjcIuX?)B7!OP6@Dj)=f8ZOkh+654n~z@epiwA($;e=@_2cb?1VbhmT=G-q%XIAt5)yWg!jtBSrn+45Czd+CC)s zI)XWtC{|-6@_I?_?G5r{Kkq80jl(tF8TwBS6v|SV8X@7+Bx_z)i-q1rl23|IkF^3I z8gl)j^7<`NfEZ1*^ok>Vg3SAS9F;e4=~>Cw)xV6}`u!R1x<6m?bb0u7wJ2#Ou-dl; z8f{(;TEG9=z(HBsOt4#qF$2nz@4@Dqv#(?K^#QXOZ-16Da7cO$B-qWf@RSQ3JKC>W zlIDL=ryKN~$j7dk1Wx#7>K4rr4%8HM~R&mmb(es9ihkq=Bf*g|B|EFVR2lfavV~E>Uj6lO9B&R3p=5_A|-^fS-{D#W@2P z-xekDqq#ybJWW&>?4O-yV3-GicTyeit?*V_i@7^_Rvy>m(N0tEDorc;UKk~=?39Nz z*^#blhSazJ>;UHJb+Y%Wz#1#!ei!pJwcO5!8vbIn5kG|ZarvRj*i{qbi<|%`-Cgf z>-_{3%Ti~%sbQ4<_SGOws&8i_iKcG(%PCkoli1I_MxGT49xuUoJq!;zF31Afk~?qZ zH03$5b=Iwx4h88$u2}+G}MEUfQMJjyGa$6rDU* z*@ekvjvaPq!Rj!>j2G5}K)e(CwC=$3t(jZAsnbpZ0CQy|w+EOZKdGqezKo#h8Bs9M zy4OTW-0<<~&XZ2jU9R=u<;l8cAOfy$Wf1ns&@Pos5T{z>_`urN2DT#1KN`myfhKw$ z_Q%gcs%2C7c4nXC?u-6csseJd3fbL_0s>lF*UwbGNd_LbCZ7JTSl)0Z5~Awr`G_37 zqJ3Rq5HBmzWo!2#J2uI9fE*E9SgA4WCoup|pd)SwdAeJ^XjPB!-lx_iRz~@}*=e&k zqZzbiqCYMaJSxN7qHir!DfxT>@Q8^RqwLRB^;VbIxO?T>lMv2z2JhmiDx8eRnDeR+ zCWQkM8Ef|Lg!N9MiZT#&2dN@pDp>nw^fB%K3{9`hj#l4w`TemC+pAv_XFa1O?=P$R zp~!Rq_PLBhqUZNFyooRcZJF*u;zhXVqXJhSzz+hs@t}#~zB^doP&hK`ZR|40AG*AB z&KJ$xcqsUhm`a24dw<*%b#nrz=FTk{hdCE3xdvLP^9x@GP6Swh>{y1V1^Q_<-+DST zM|`4r1KYI6NnCZi^ZP)X@-FUa!E*Blwanu?J|jjwZ=kfdsw(EM1(n>Nkx_4jxONq(4C!YTXHJMrv3JjQo!dl4IG za_{gT{}RewG>3-P=%+uQkfJNYvR08dS}#shwJCtIISYNLAg1$73OSg zI7ETqX^s2SdBEU^VNEUAi#>JUE15~}&1rNHTG+9Lf1YF?pRZwJji9D<;S`}Z)+H0- zf#l+l;MA6eLCvSW-Ej3o_fdv~Dt{ZpdU5fFLK{7G0>&XFuJj;h@wxVH;*szUJGx^f z-6uaC@SUN_%XAT>h5|wJ#-w*vk*(j;biTA7mh}`#TRKKWZ(H5feKM&PaUXk2+fWT@ zVEOIPD@cq;a9~#Z%I(!w8JMBUd|y+d>N&>1hMTPq(hB<%GDQDXMBhC+irj zOBBQdw@I$DnhPH0%negUb$!*!W8RREqMgpKaROx_F`FYD{=vOA>RL2~)#mlFLSbL# zG!$_J&nInIdI^AUDW=kw1arDD$)WD`EpE&KrXi#%Nf;C(u-zkxwwOo9R0cy=U8I6p zW72L*L7LGzHb{ZL?2Mi|vdiL<%mnrNsXCeAcl@qj(oSIL?nS00vD2EqdHNcB+AA@bQT&MY;xN~@%F>_*2Pt#Fz8O}T?>v;tPK^`03kmdgg+CM}dW3#3>8YkI+v5Mx z)4i#e5E^(SEYV<09V}%S8p~Zp{$pMk*)qN41A$mKBOM#iHe0U@|kRGU+;Ut z@`lMO$tYEtwYvB0t(=Cj^&J`8w!dmf?xSaEh-ZW{9f3a{Ki4o$Aeh;G5~NJXkS(+8 zl`&OD6L~#(*bL@#{$_WY*m-2#SwwEzacJsybxM7R7Lam?I}p1_>FiH)8uejC7}_N; zULFLhJkKZp9jez9DiW&1ZTW>#O9-eONK+YX1uR#4(FG);&=Pr5jTysMyV)IYDg|E4 zj#KV^#$BzBhekC(Dd2A#!iWa3UlgXXx`8}xXi~a3Azye?pCQ3`(iE>W{Mk7=T--EV zBY4j^sY9|{#ThNPyP&4>G3(_Ax%%!Du&jOyu0@&Xw=m44p(&o#g$kZ&Ie=@~388MX zpfIORWyhM4bPERT#|{MvOCE{eWLw>6<>2I*;&S}N9BW3s!8aap~k9YV=yP|{WMn9uhs+;RG!%FE&oDic_iW+{0 z;x)@*9WL!(Eo_=3@W)!_yXgv+Ar3sMC^-y&Z(`OB$$u_nYoPZrYR)4!Yg~<8CyyYY zfWg#UjtPTt5&!e+Ap#*v=g72?{0g0rim1DeaZ%fjp56C_o)(>O_h%J?565OBx=$Ij z%*~s$gxHv(388_a)KX!Fhiz}eBvw6raUDvNQ~dPkzL9Dr;(n26VSp%Nx5<*(#*M}F zyq;QP&2hetM&5a1 z>M6dDx^W?%?vtoF!}@@#iV}sBE#+**bni?QT#iW!^iLV~1~Xx6rO9+?dzz)gcXlgN zETmbJ)k}ox`uZhqFWmKpDI=Q9+qIHgaNd0f@y5?orGJ^L#p8;`$vL9q?`Pqy5{s$U&l_0D}!8|}ZKWJ0ku&~|w7DB6-2=C6y!P&vmzN>GbvCC%>@GJlew2nu*!^?{Wm z{F*m>`pTRSwq1WCQxH4gOCh?_-&qWbX zFcDXB{F=SA1Sw3k#D6A_NRA>=08as40^LO?3Z)CBQ?=7}M^}0t9Lz-^ss^yC}ygG@EPC zYo&{VNG=HQPA9$9zPsI9G|GunrXiuJ3sfpbqmtNxskPf$PCTJMm4o2nh%5^NcA-&z z`DL5@fzzsr557OMO&N_Xww?~O$`AYgPRK*u1d6M<5(u|bVmp+zAA`{0a*~AOJB`OUyql(7G%4^xFkX#+kPQD%2R%A$U0P%crdUvzuyDpSI9pxa<#SP;+$Vx z46gd-tDN){2#eUY9GFlf@*EUm)e8G4w%iZT`7P%W8jCIur@i`a(AIeBJLD|ZOQV|E zW^fyb$((anOn0InZD~(nz6hQVL2~Z~nCLxGOO=HoM{SErwz6@IAq3q<>6xt5u93tr zZw!q45NzHb!=+h?0j^Inn^$-6ogccyg4;o*MX{e7w|3P+NUkO9W{TCT?1wQj!G&$d zgT)=J)x@(E;4|^pwY1K2n%{IBr~9y|Rk3*45V5skTRQp$II)d3#ChN9fQ`Fii*#lS z%yleoA02C$E0Q2HpHKC4W+oPZBX>@VO01*|sg;unE@7^p(kt){cI{L1ddahJ>6r{p z)vNOJp6K#>wtkQ(ZWsU(4z1cGvlkBz%vD-BM`>&AZ7|jhR|XyS8TJ+^{fg5BQ;@qq zv4O4`x`_qB(@$QejydQYACP)2?zK^cV>#e^PP!)WX-zIgABgs`LIGbP`@|MS80En* zZ51Vgb{iz0%wTPZol^8GX*zk;-mMBy1@kroygmvr0jBF*_Y3O-)EfT# z1OVnX&4S2dnot-(%SypuI_EpCMT-RCCM8=eWX~?@ub)J(^D(znf9OQv=`;|nd%+Ey z-9F0NcCKeqh4Thb9GEt&`HD@@fNw4!A9pCt%Y|4Vv2*=IK)D zPccn9L2o5y11w4{C(}I!uRAZ1tWq3u0ZvErRc0)6qw6$XW;IzGjvjs(*+)5}i`640 z1w2A@M9NRRRiB)(zRD>A%}rPl6q&kS`9bO9z_@5q=Ui>Gdusabn8G-kd67vC!x6Jr zBo093lcR-no02-$35{sfMe)W3o0)Rd>wxGeGVi(23Hr7Z*%3o%;#bo znYFIvy?Y;y2Ku`P~Ip?I5iVrWXI-yDG@-(^hFh@*gw$?N5zS=>ln47+FKa}PMu z2&2Nem~G9|r?}|MyXJ;}+Nk^F1TV?YU9_0bHEaO)^(M;D%tOtK%xlaolU&Y=5{B5U zbNBi*Epu)94E+f}TPjQ|)?~n5w;#Qsr0(%|mE45PvYThlrYbFAI?bg9+R`xSj`j@R zwUNrZ8H(Joph^smD}S4s;B4b5Yq(XTSqyhLL1ld?c7|;$ha6lxZ6_6kiOj9Qk|h@h z@%VIG3=*CybU&sMJ9fV7S*fo=Ju)IcTp*#kUhu0tNtLK-G9`Nh^0PLkYS&ClkAPS; zOyXE4jrdplC`=I}J16CcM|5sm&eMo8>FnXXRE(mDjr?>5%kSGp zcuvF@VmQNdeM<1O7x`qBd!by{Z7C+#s0U*N+$`;Mwi?^b%U)tntuy>7*1htlUt_!j zsU2LToZEox$O@9pHN$2n2fF0;(#r^A>)fb`uB#=LPGr!SHv?$0P9uZoizM54UyPQZ z6K`nwE!em1_ju=@yk_Sy9|=P+MjmeG;u!Vb zonx@DJ6@)(CKXK=lz-^c(OW2Zn=%(%QyE^S0H`f6c%%hXNR(Uu z+K%Nwj>HOm9V2|M`HZg$HU~N%IN&`~`}K%;iRGz?rl1GYV>my?V}rldX&s^h$o7eR z#(@_oay1?4cZ{8Qm#4n-PNZZ=XSB+E^E~kqV8m@q>WS%zG3-ORF{{eJcd_sLvk}%c zQY?KOps#`+z*CSmhIKJRo z>0sFRIgp{MO(mKoqRYbi6AR2{C+xv3yigwN^DWX|V+LrwL;-uUW6J6@oS(0;3c)}k}9@A$v_TDZ6T}Aae6I|@EFz=O(1}Pcs-oRQ*}8}`iP0B$?V$- zKy1qK(XRYvGol$?2u$i|M*X4Jidd4_%uQ^o1|Syvu^}0a&d&yIaiBpZM_@nS) zjj^R%Lpe)E(B*;t>1jDUORrb@MdhXA^I7lsQ!*T5v-1ML`b}9GOR=AmZm+S4FrRt= zqwH*4M|i}et(_}Bj!X4B5ORG`7(vVk%avy&XGj)lIpC=W0;-V<1E?2_xK8d=a z@0LSct?n}u-r`c;lrcUrww(EP2?VjXOVF`B&5{J1rnJ6t=XzpX7bkkxrwCQErR;)n z0AA}te8H(bL&~m#1x=^5e4WBaZ#!Hhz^DUoF946`Zyp%r{H!c#BBnAy3H%?b7*A9< zGG<{ll{joj=)f5|E7uwe>Gy6A`@U4~rA|IP+cez{B)uxIOLp`Uaayfj+vQv#eZ|u6 z(>;{)wDPg%81}HEH6QFWR@LcQ?{-evV_?QM9=nj;d8Z@cgJj=1WD6ovR~Aavyq#8G zR96q+FjNGcoZk*59%o1G6IJNK^eU3JUtB>S7aRX>E4Z^U{6uQTbA-W##-8DdpHmu zVzA58{tu5GbR$q)A^@&!Zxg-5f(b3VX%R&U4BY)aCHxX|j@Y`8DNH{-h>&8@H8Xq0 zbjY_$bdDA$!_&9FIkpB{A0r&QvFtecrUOgv59N>wTa>Ls6gSyz3u2$3zw_2TVvnTz z*3!tC2YofW^~*SY=mS{Kh%{rzy0R}jab$L5-!Ud&C2Q6%2(${-cKeXe;Vwl&m7=`v z;I}76O1vyWHVpjU=C6Hhn>d{LgpCh%XQ4foZMnO5);J@<(4jC6%IZcy*2B3t8i@>? zluJu+j6La#yugD9vWp42Y}*F_q00*Dmh-l?>n)5JLLaBLw1+cqBcH=$>UwM@s8+8; z)kGOkM4Ar_MlhS$IXAkg=u(ouIT_|{+_`Lx=I8OcJUwBP**#QquEPo*bAD}DuETiL z<_Y}T3H;tV--$dA40u{B%8geGpTkeAVD@QYd-y<0ilx29zI z-MCSpB)L-p_qmhtF@Dt&Z0su#J#wrrs-B!09KPx-z*>|qu*u!n-SMKP~ z=5W+1K!uZ>>A#bN$|Kow)L@dccpH?z<>$w8jCNM9=el?b#OcXd^W~w>Hp9DpjMpVG$D2cEYPmPTFc$#rIYt-Ey>YiP3E;pBm%mM`;BY7J-alvl71J z>B9-sIvAGADRV`Yioo=_>-nn}OJ_A1C6jpRTIbLCcKC1n!%-l%m@8AR3wQbH6&w?o z?ddlz1!bd#BM);^bA29TGJ3Cfch&2M!)+&rF=4bJ*1Z9jFR6^=wLb4k&nA9y%-hb5 zUz2u?+6S8oS8-GmDx$qrNk^#U@OnTzIE-w=`F$->o9p@dVJdXa4;|zigkY|JODHV( zhCM5lptjJ!xWd+0jb{Zizx&w?y)vX*;VZ~f?ts3~>$1*mnynlYgG*;`^K!(RbO3Zc zH)YptmBBBxpB*^G|M=p%M?X6oG4Mp0cn7yI)#%ees>YiE{ImhMx^u&B>+(YD?&iC@ zpO|b6{Gt!++*}Zg?GbcUY4c~^6VAJ!hxv>T3L2carmMWsk)h3w z&}K*iI-1?>+{=#^ZcnJUf39qZcRnYe*8e1Bn)=YR7@OZ;pZ&$>qPqwo=F1F`K9<) z=$Rhk>^P($$9A+8TEsKH=P>00Xd){kjUb6(vN7)}$RhD4@sn`yAa?`Odv79 zTchN*iB|AFv2k+5Hpb#*MC9YU8Qv2Nur}b3Dbka=%YNTbQ0aXBC6^$TkmsAO7{6k+J6CD#87Xb4~I&%6gJ6F@+cGR*xVq5x<`IB4W zyt`6MJb6Y4(;LQIT0fB$-HT6T;LeYdt@pj|+LTvHLh30z!g1uH&|)t|@#yH8VZK@Z zOn&aet}lMpQNDPe;@K2t7K3v>El>gN46S()a~Qx{MYE1WC*52s?lP3^Xve6V1$n`1 zz8=`a3a>+)avXt(xgY`E>l%EiYbL?E(02-T*yT2J#A4=Y534@wCZZ89A8xsROk8Qp zQ&_+(3gibHmf7jH3kVZN7ZWs0E^POl$J38i$dF|9JcU#kOzUe&YM)1dLtWMzk{2D9 zPFrou<(O^s8Ngq~SMx@!@VEtz=5E%euVpbUs<{1dkohnwba@`@rN7dQBp;T^Ea9a? zRM~JMn%>l(!82|DIcAzZ1&K;XU)1<9S}w}5HdbZSiRrh9yNww(LBdH$ucs=WVY zUV(8|>#M~==P^SDcz%JlzKz^%#p?Ml8UkENkLSAo-~XC!h_CrT=%vui4J8gV1^gsQ z&z8wfz=-wk4T~ECmInZdMQI2gZI@ya`S4J#^pNYgrJU5F)&A0`Y9f1Cep24Re4@kn zufV=bJEXBkS*4I;ZYOID3h4gw^m7sRv@5xTUl=mA5+igTXY3!L zGA-`oACzF;F`^f)2_ba(4#(ZCv6xbOD>zWtv%;LP&)<*CEW#Z|<6XDsN2iH>4n%wWA znHq;m{0;oxBV9$9AU-80suEUPmT3n=kuEwmv%u^&yG(_nvx4(lGGoo zNh^{Xu1t3{d#++hLMTGiL8$!BtHh%iNMX{;nZnbW7#W;bmjff{;7*_KU4&HA8t^BI zx2W@0st4~Ss@ADC-=Z*K=&kVY0RJFM?27fXzec{vqlLR4X}2%D9Z(jiq&UxEQGi__ z^LDdX6eeU6cQvM(Bb1An0}kFC$~>jYV%gCDc{IIC3vBGF&hdh@g~x|eKaxHySPo;# zHq#F3tWy~d#0PEYJZU)7zx+6_7fiSd_fkGogkvl2|BkI2GDU2agk0bWd6^miaSInw zojWNKzvX_@ZI6a}sesPj72p@y!e6uCl3VB9$9};dB2~`XH=VI3If9|5uTZ;<;7-M( z0Vk*Ll9CN3|I-kjA!B(3#h)|C)lMqL9eWFtL3OBXZdUxP(Ryrc*5FU}Fa*zRie-iG zPP~rAB0y27ph*~&>vJTF^Og5^Ec6N+avYB=$^RLV80TdVl@8$sYcMcaQ)ni%*IxKc z0B6)|1nig1_NVfUAU5LFS`aiMQ)s5+86B<%@jfeU3dbhJ(4IoZneuPfq+etyvP=xv zbxtqxvFSlW5|$O&sM&At%U6X;k>37xLjDbHB$oWf+686~XPCxgH_fDy2Vh!q*HJQi zw^(ojXib4v@}JD{&FNUfj_(@UI?lc?p0qxN)>Ir>@H*{9jbh4VaajMYAq97nr(-w+ zH7dq$dv93Cf8K4oLYa=LA+0=pRZ$^#{5Rpb5!G_qqi7 zY;K~tB?BBd-;?P4fJ?x#iy&XK@n7tqOkSBQX;pTZ7Im( zf)!$N{^=#Z?m=|XxZ3ZCm*1!@!}`n-?bMw)-tlxIgC~7SDz7Bz1pB8fZP@!$M{C#Q zw1)DN4Z6;+CHHVb@sc`RZA~u#0jpuF?*vsc^pX_t+6!WsksVJNJNT@${-Sq;rsVhT z{vz0B0+c9ft2XKg*oZ{*8`djcn+C)c%&J=YhuS(4B2KC3-ul3kqR+XQSPSQ$bbfFQ zc#6C`9^P~{c&sH{utkF6#Ni}3v2ogqg8(8wOp`MXpXy6!%#ivg3i=mMRf{ff3{)ui z^x%$c#BaIZ`g=KR0x4wV<~XTq@~lSICjEvtY@I&+Wxp_Nc_kWV?5%9288V@-;j0sz z&REAcdPWq5f1nnjO0iGtL-W+u#a(-ZG;6%-jCN~EP^XZ0#wh*U#r!{vJcNB{5fHfh zc*538f@-}0ARW;qSzEm}?}mO-!}7-TUdsTFKp9+(O#VGSN2OL?R?n*`MwXtn^Jh)j zG;Zh|w)Btx5DWqmh&tth{!O6MvTj|#O8saQRV*?rSYzO!WViM9i`#)=xpV#>_v#f8 zV>`GyTkb{w7)L5=hQbXee8R9{$%~b&m=>hJh;?D~tVUhf*NQ8T?~k*~^>sx;8bxy= zJ0wysxIRieu@FLg`Nr-dh=;`QN*bWOKC}yej7$bgW?^0s@PL+T#7g<@;Xxwh#n3st=3|Eo1JW7t ztLw3&OyMmR@xP)B?r|@OhzDW|sI4Ko%GG~?yaq6f&>V!c7{-#??rPV=HY(#koO=IPwbW9FwJl%_#(>jg8|C139P618c&14Ihc&U}b zEW;ahX>MVqP)8wlM8}_5^%Zsimfi^_zT`@v2i(K+4=KQ7u|7hX?dSlS^JltTOr+5s zTl*?^z|Z~RkU8~Z%|<-_-K8<28Qi}>B7#1mW8{f<6f|)|b7oJ+ulEjeau0^VxBHU~ z%utLjR+&{-qkqx^!TIeRbXk}x11 zAGTz@p3w2j`1W5UEUTdg7eAj|7!V|d@wgB=SZWm=g!QLg+dF|OaIG}|v_pV1fF*0~ zn^#|6-#tXqT71+{+z|z@)Hb4H`4>lUKaV_3Og4+JY?hZ@u}vaH$>#XGN#8G8X$Rwl z+|MJ!*8VOq7IhkZh~G8;_|UBP5}&v@*J>fG&jPpN-i$^R%cCy=X^+LVFUYZYp7j)h zkD+V%Wj1`jA|(C*fP~mI)v=x?w7x;Q8fEU!#Dg+FXq9UE;V)SXzHO$_%@ZdLaEryn zq!=2SS>rYNi+)cy&Ah}b3>CR^^JKB``_#B?P62mC`!jwbVD%Y3Y0V`Js%gOwCQ1bh z4*b)7TH}L10s=bh?dw;Hzo-u$BX-Nx5%AT;!XIR5 zLjH1-8tN8j@%;X=?eXb)dq<^^Q z$&LQ4z?b;Lqx&tNo6BtmDYww8-cBrNnh?gs}vr+*y76yyqmIiRKSjzb*&gJeGo! zdrxRHojS>>h>^9qk8f~E!m|G02LZ_x$%54_wv#~VVxY^cDps;Q*9xZUC8%>jUw^L^ zh!RWgp<+jm>D!Ocx%@$Bh{llc#P;X^AAB)K@ z{X=I@;;#}3UlIW#-h7_8RB9iHZFTU}jtmXc?5ea;g7WXW;ZZ3$FQ;BQmIgJo1It9K zFzI02CtsGDBeDLrfe#eufns9{;@HZPR44#hm_u(AM2zn(2p%Ya?O(_DAw{EzDN#BF zG9_A*zM;?%(0LCm9r0#P$Av*lc7uJtAi=x5!Poan%5C67k zw#UMH`{hCpB=Q47@u+p`K;*6LzraiOFqs?^?55eT51Rn5{EoEvBTRLqo4-g3`-w=# zxLYn!ko;@Z;RK*8`Eb(%623o4rhoCv1i(f4>a2%sLpMz6#>8g*uXKQGOH;UqtTMnF zkvk_defSH>A6jd+#~GGn20|bKn0snV?4f@TW)YFi5zy1tGSaEYMlIrJf4hiaIVh7{ z7j%ht{WmGkFAc`Q4rwS?f9dw-qV-=DI0Z>P!S!7m0~0k3B&8qsu$B>!g$TBgPai-L z!H8=J@ZaWQQsniE2XeeVEOPdD2TgbDcVs$0eILL)PdqI>{`|rVPx}NK*y&*>@B*}mBAO1q36v|C<-CMVz1a#b_w`zk5Lo^q60UJEAlZX@ZUe#qO@X`u|Ef+F1lZ?G4dHLPRF=QD>j<88 z*wxpHu!YcI^m_y%vYe0Va?yWo556-Y{Wo6>;atLt>@~8U4B_ZO?kyMVQW`4^utqPWz=Y!;p995t5W-s^%XwG5%=zo-y2qx;x(uZJ71XL00K({|1@{(ayC z89axUV!m~{k6$Fi?(O+yGyq8PQ@D8Fod(a>gf3g`ZFxb91MWWQKM=OX%h`GgBdE+pwB9je#=Dtovuj$rG? zItur>y$ARdYRXVC2j$<(F??q(C_R8p_{Nq0!X^YT44xvuzF!R2(-~U?v=qcS_g8C% zPi>2|bpi?YzsFu4%?&6iLkHqN_U=guk!60w(<@tv*Z=ya4WH$La=`8$faG73-~y4K zit=CISmCochZ_pq0|l#E7Qxjm2^;%g-|*qHG`uMzxlgQ`bZ`I%{Za0Jn-7A|qMb2= zaUZ!wxFR4Oqt}4@-{5F;{a-Zx+Km52<1ZTwoL%A0+5bz8zuEKu1ExM~y{A>e3E+|$ oaC-w+^p5U}OZaJzfpmxLb0FQxnrrh60siw=T=sR5=*MsW1C)pe4gdfE literal 0 HcmV?d00001 diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index a3a493347..8eccf4f2c 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -35,8 +35,7 @@ export const ROOM = createRequestTypes('ROOM', [ 'LEAVE', 'ERASE', 'USER_TYPING', - 'MESSAGE_RECEIVED', - 'SET_LAST_OPEN' + 'MESSAGE_RECEIVED' ]); export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT']); export const MESSAGES = createRequestTypes('MESSAGES', [ diff --git a/app/actions/room.js b/app/actions/room.js index ab8b1b1e6..00fcb4c7e 100644 --- a/app/actions/room.js +++ b/app/actions/room.js @@ -64,10 +64,3 @@ export function roomMessageReceived(message) { message }; } - -export function setLastOpen(date = new Date()) { - return { - type: types.ROOM.SET_LAST_OPEN, - date - }; -} diff --git a/app/constants/colors.js b/app/constants/colors.js index 658bf9f47..e666dee59 100644 --- a/app/constants/colors.js +++ b/app/constants/colors.js @@ -3,7 +3,7 @@ import { isIOS } from '../utils/deviceInfo'; export const COLOR_DANGER = '#f5455c'; export const COLOR_BUTTON_PRIMARY = '#1d74f5'; export const COLOR_TEXT = '#292E35'; -export const COLOR_SEPARATOR = '#CBCED1'; +export const COLOR_SEPARATOR = '#A7A7AA'; export const STATUS_COLORS = { online: '#2de0a5', busy: COLOR_DANGER, @@ -11,6 +11,6 @@ export const STATUS_COLORS = { offline: '#cbced1' }; -export const HEADER_BACKGROUND = isIOS ? '#FFF' : '#2F343D'; +export const HEADER_BACKGROUND = isIOS ? '#f8f8f8' : '#2F343D'; export const HEADER_TITLE = isIOS ? '#0C0D0F' : '#FFF'; export const HEADER_BACK = isIOS ? '#1d74f5' : '#FFF'; diff --git a/app/containers/message/Message.js b/app/containers/message/Message.js index 940def60d..6ec7efff8 100644 --- a/app/containers/message/Message.js +++ b/app/containers/message/Message.js @@ -365,7 +365,7 @@ export default class Message extends PureComponent { onPress={this.onPress} > diff --git a/app/containers/message/index.js b/app/containers/message/index.js index 4da77d2ae..25f283e17 100644 --- a/app/containers/message/index.js +++ b/app/containers/message/index.js @@ -101,7 +101,6 @@ export default class MessageContainer extends React.Component { return _updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString(); } - onLongPress = () => { const { onLongPress } = this.props; onLongPress(this.parseMessage()); diff --git a/app/containers/message/styles.js b/app/containers/message/styles.js index a3ae5f7b9..64e506797 100644 --- a/app/containers/message/styles.js +++ b/app/containers/message/styles.js @@ -5,11 +5,10 @@ export default StyleSheet.create({ flexDirection: 'row' }, container: { - paddingVertical: 5, + paddingVertical: 4, width: '100%', - paddingHorizontal: 15, + paddingHorizontal: 14, flexDirection: 'column', - transform: [{ scaleY: -1 }], flex: 1 }, messageContent: { @@ -39,8 +38,8 @@ export default StyleSheet.create({ height: 20 }, temp: { opacity: 0.3 }, - marginBottom: { - marginBottom: 10 + marginTop: { + marginTop: 10 }, reactionsContainer: { flexDirection: 'row', @@ -82,7 +81,7 @@ export default StyleSheet.create({ marginLeft: 7 }, avatar: { - marginTop: 5 + marginTop: 4 }, addReaction: { color: '#1D74F5' diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 4bf67f4c5..27fdf2629 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -1,4 +1,4 @@ -import { AsyncStorage } from 'react-native'; +import { AsyncStorage, InteractionManager } from 'react-native'; import foreach from 'lodash/forEach'; import semver from 'semver'; import { Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; @@ -118,17 +118,17 @@ const RocketChat = { reduxStore.dispatch(setUser({ status: 'offline' })); } - if (this._setUserTimer) { - clearTimeout(this._setUserTimer); - this._setUserTimer = null; + if (!this._setUserTimer) { + this._setUserTimer = setTimeout(() => { + const batchUsers = this.activeUsers; + InteractionManager.runAfterInteractions(() => { + reduxStore.dispatch(setActiveUser(batchUsers)); + }); + this._setUserTimer = null; + return this.activeUsers = {}; + }, 10000); } - this._setUserTimer = setTimeout(() => { - reduxStore.dispatch(setActiveUser(this.activeUsers)); - this._setUserTimer = null; - return this.activeUsers = {}; - }, 2000); - const activeUser = reduxStore.getState().activeUsers[ddpMessage.id]; if (!ddpMessage.fields) { this.activeUsers[ddpMessage.id] = {}; @@ -145,11 +145,18 @@ const RocketChat = { } this.roomsSub = await this.subscribeRooms(); - this.sdk.subscribe('activeUsers'); this.sdk.subscribe('roles'); this.getPermissions(); this.getCustomEmoji(); this.registerPushToken().catch(e => console.log(e)); + + if (this.activeUsersSubTimeout) { + clearTimeout(this.activeUsersSubTimeout); + this.activeUsersSubTimeout = false; + } + this.activeUsersSubTimeout = setTimeout(() => { + this.sdk.subscribe('activeUsers'); + }, 5000); }, connect({ server, user }) { database.setActiveDB(server); @@ -330,6 +337,11 @@ const RocketChat = { this.roomsSub.stop(); } + if (this.activeUsersSubTimeout) { + clearTimeout(this.activeUsersSubTimeout); + this.activeUsersSubTimeout = false; + } + try { await this.removePushToken(); } catch (error) { diff --git a/app/presentation/RoomItem.js b/app/presentation/RoomItem.js index 8b5cdab94..471732860 100644 --- a/app/presentation/RoomItem.js +++ b/app/presentation/RoomItem.js @@ -214,7 +214,7 @@ export default class RoomItem extends React.Component { formatDate = date => moment(date).calendar(null, { lastDay: `[${ I18n.t('Yesterday') }]`, - sameDay: 'h:mm A', + sameDay: 'HH:mm', lastWeek: 'dddd', sameElse: 'MMM D' }) diff --git a/app/reducers/room.js b/app/reducers/room.js index 03b1fa898..5d20b1483 100644 --- a/app/reducers/room.js +++ b/app/reducers/room.js @@ -9,18 +9,12 @@ export default function room(state = initialState, action) { case types.ROOM.OPEN: return { ...initialState, - ...action.room, - lastOpen: new Date() + ...action.room }; case types.ROOM.CLOSE: return { ...initialState }; - case types.ROOM.SET_LAST_OPEN: - return { - ...state, - lastOpen: action.date - }; case types.ROOM.ADD_USER_TYPING: return { ...state, diff --git a/app/views/LegalView.js b/app/views/LegalView.js index 313e90cb8..28ae450eb 100644 --- a/app/views/LegalView.js +++ b/app/views/LegalView.js @@ -13,6 +13,7 @@ import I18n from '../i18n'; import DisclosureIndicator from '../containers/DisclosureIndicator'; import { CloseModalButton } from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; +import { COLOR_SEPARATOR } from '../constants/colors'; const styles = StyleSheet.create({ container: { @@ -22,12 +23,12 @@ const styles = StyleSheet.create({ scroll: { marginTop: 35, backgroundColor: '#fff', - borderColor: '#cbced1', + borderColor: COLOR_SEPARATOR, borderTopWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth }, separator: { - backgroundColor: '#cbced1', + backgroundColor: COLOR_SEPARATOR, height: StyleSheet.hairlineWidth, width: '100%', marginLeft: 20 diff --git a/app/views/LoginSignupView.js b/app/views/LoginSignupView.js index 5b6a36156..b40bda72f 100644 --- a/app/views/LoginSignupView.js +++ b/app/views/LoginSignupView.js @@ -17,6 +17,7 @@ import Button from '../containers/Button'; import I18n from '../i18n'; import { LegalButton } from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; +import { COLOR_SEPARATOR } from '../constants/colors'; const styles = StyleSheet.create({ container: { @@ -72,7 +73,7 @@ const styles = StyleSheet.create({ separatorLine: { flex: 1, height: 1, - backgroundColor: '#e1e5e8' + backgroundColor: COLOR_SEPARATOR }, separatorLineLeft: { marginRight: 15 diff --git a/app/views/LoginView.js b/app/views/LoginView.js index beb2051ea..134eb99ef 100644 --- a/app/views/LoginView.js +++ b/app/views/LoginView.js @@ -96,7 +96,7 @@ export default class LoginView extends LoggedView { componentWillReceiveProps(nextProps) { const { Site_Name, error } = this.props; - if (Site_Name && nextProps.Site_Name !== Site_Name) { + if (nextProps.Site_Name && nextProps.Site_Name !== Site_Name) { this.setTitle(nextProps.Site_Name); } else if (nextProps.failure && !equal(error, nextProps.error)) { if (nextProps.error && nextProps.error.error === 'totp-required') { diff --git a/app/views/RoomActionsView/styles.js b/app/views/RoomActionsView/styles.js index eb6c54ad6..6a0d93b1d 100644 --- a/app/views/RoomActionsView/styles.js +++ b/app/views/RoomActionsView/styles.js @@ -1,4 +1,5 @@ import { StyleSheet } from 'react-native'; +import { COLOR_SEPARATOR } from '../../constants/colors'; export default StyleSheet.create({ container: { @@ -26,7 +27,7 @@ export default StyleSheet.create({ }, separator: { height: StyleSheet.hairlineWidth, - backgroundColor: '#ddd' + backgroundColor: COLOR_SEPARATOR }, sectionSeparator: { height: 10, diff --git a/app/views/RoomInfoEditView/styles.js b/app/views/RoomInfoEditView/styles.js index 3c61eeceb..c3b86da48 100644 --- a/app/views/RoomInfoEditView/styles.js +++ b/app/views/RoomInfoEditView/styles.js @@ -1,6 +1,6 @@ import { StyleSheet } from 'react-native'; -import { COLOR_DANGER } from '../../constants/colors'; +import { COLOR_DANGER, COLOR_SEPARATOR } from '../../constants/colors'; export default StyleSheet.create({ buttonInverted: { @@ -39,7 +39,7 @@ export default StyleSheet.create({ }, divider: { height: StyleSheet.hairlineWidth, - borderColor: '#ddd', + borderColor: COLOR_SEPARATOR, borderBottomWidth: StyleSheet.hairlineWidth, marginVertical: 20 }, diff --git a/app/views/RoomMembersView/styles.js b/app/views/RoomMembersView/styles.js index 444bbfa0c..0ca648e5b 100644 --- a/app/views/RoomMembersView/styles.js +++ b/app/views/RoomMembersView/styles.js @@ -1,4 +1,5 @@ import { StyleSheet } from 'react-native'; +import { COLOR_SEPARATOR } from '../../constants/colors'; export default StyleSheet.create({ list: { @@ -24,7 +25,7 @@ export default StyleSheet.create({ }, separator: { height: StyleSheet.hairlineWidth, - backgroundColor: '#E1E5E8', + backgroundColor: COLOR_SEPARATOR, marginLeft: 60 }, username: { diff --git a/app/views/RoomView/EmptyRoom.js b/app/views/RoomView/EmptyRoom.js new file mode 100644 index 000000000..9f65079db --- /dev/null +++ b/app/views/RoomView/EmptyRoom.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { ImageBackground, StyleSheet } from 'react-native'; +import PropTypes from 'prop-types'; + +const styles = StyleSheet.create({ + image: { + width: '100%', + height: '100%', + position: 'absolute' + } +}); + +const EmptyRoom = React.memo(({ length }) => { + if (length === 0) { + return ; + } + return null; +}); + +EmptyRoom.propTypes = { + length: PropTypes.number.isRequired +}; +export default EmptyRoom; diff --git a/app/views/RoomView/Header/Icon.js b/app/views/RoomView/Header/Icon.js new file mode 100644 index 000000000..5c0682859 --- /dev/null +++ b/app/views/RoomView/Header/Icon.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import PropTypes from 'prop-types'; + +import { STATUS_COLORS } from '../../../constants/colors'; +import { CustomIcon } from '../../../lib/Icons'; +import Status from '../../../containers/Status/Status'; +import { isIOS } from '../../../utils/deviceInfo'; + +const ICON_SIZE = 18; + +const styles = StyleSheet.create({ + type: { + width: ICON_SIZE, + height: ICON_SIZE, + marginRight: 8, + color: isIOS ? '#9EA2A8' : '#fff' + }, + status: { + marginRight: 8 + } +}); + +const Icon = React.memo(({ type, status }) => { + if (type === 'd') { + return ; + } + + const icon = type === 'c' ? 'hashtag' : 'lock'; + return ( + + ); +}); + +Icon.propTypes = { + type: PropTypes.string, + status: PropTypes.string +}; +export default Icon; diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js index c6a3f1589..abb934faa 100644 --- a/app/views/RoomView/Header/index.js +++ b/app/views/RoomView/Header/index.js @@ -1,52 +1,45 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { - View, Text, StyleSheet, LayoutAnimation, ScrollView + View, Text, StyleSheet, ScrollView } from 'react-native'; import { connect } from 'react-redux'; import { responsive } from 'react-native-responsive-ui'; import equal from 'deep-equal'; import I18n from '../../../i18n'; -import { STATUS_COLORS } from '../../../constants/colors'; import sharedStyles from '../../Styles'; import { isIOS } from '../../../utils/deviceInfo'; -import { CustomIcon } from '../../../lib/Icons'; -import Status from '../../../containers/Status/Status'; import { headerIconSize } from '../../../containers/HeaderButton'; +import Icon from './Icon'; const TITLE_SIZE = 18; -const ICON_SIZE = 18; const styles = StyleSheet.create({ container: { - flex: 1 + flex: 1, + height: '100%' }, titleContainer: { - flexDirection: 'row', - alignItems: 'center' + flex: 1, + flexDirection: 'row' }, title: { ...sharedStyles.textSemibold, color: isIOS ? '#0C0D0F' : '#fff', fontSize: TITLE_SIZE }, - type: { - width: ICON_SIZE, - height: ICON_SIZE, - marginRight: 8, - color: isIOS ? '#9EA2A8' : '#fff' + scroll: { + alignItems: 'center' }, typing: { ...sharedStyles.textRegular, color: isIOS ? '#9EA2A8' : '#fff', - fontSize: 12 + fontSize: 12, + marginBottom: 2 }, typingUsers: { ...sharedStyles.textSemibold, fontWeight: '600' - }, - status: { - marginRight: 8 } }); @@ -119,14 +112,14 @@ export default class RoomHeaderView extends Component { return false; } - componentDidUpdate(prevProps) { - if (isIOS) { - const { usersTyping } = this.props; - if (!equal(prevProps.usersTyping, usersTyping)) { - LayoutAnimation.easeInEaseOut(); - } - } - } + // componentDidUpdate(prevProps) { + // if (isIOS) { + // const { usersTyping } = this.props; + // if (!equal(prevProps.usersTyping, usersTyping)) { + // LayoutAnimation.easeInEaseOut(); + // } + // } + // } get typing() { const { usersTyping } = this.props; @@ -146,32 +139,9 @@ export default class RoomHeaderView extends Component { ); } - renderIcon = () => { - const { type, status } = this.props; - if (type === 'd') { - return ; - } - - const icon = type === 'c' ? 'hashtag' : 'lock'; - return ( - - ); - } - render() { const { - window, title, usersTyping + window, title, usersTyping, type, status } = this.props; const portrait = window.height > window.width; const widthScrollView = window.width - 6.5 * headerIconSize; @@ -190,8 +160,9 @@ export default class RoomHeaderView extends Component { showsHorizontalScrollIndicator={false} horizontal bounces={false} + contentContainerStyle={styles.scroll} > - {this.renderIcon()} + {title} diff --git a/app/views/RoomView/List.js b/app/views/RoomView/List.js new file mode 100644 index 000000000..4f5be429b --- /dev/null +++ b/app/views/RoomView/List.js @@ -0,0 +1,138 @@ +import React from 'react'; +import { ActivityIndicator, FlatList } from 'react-native'; +import PropTypes from 'prop-types'; +import { responsive } from 'react-native-responsive-ui'; + +import styles from './styles'; +import database from '../../lib/realm'; +import scrollPersistTaps from '../../utils/scrollPersistTaps'; +import debounce from '../../utils/debounce'; +import RocketChat from '../../lib/rocketchat'; +import log from '../../utils/log'; +import EmptyRoom from './EmptyRoom'; +import ScrollBottomButton from './ScrollBottomButton'; + +@responsive +export class List extends React.Component { + static propTypes = { + onEndReached: PropTypes.func, + renderFooter: PropTypes.func, + renderRow: PropTypes.func, + room: PropTypes.object, + window: PropTypes.object + }; + + constructor(props) { + super(props); + this.data = database + .objects('messages') + .filtered('rid = $0', props.room.rid) + .sorted('ts', true); + this.state = { + loading: true, + loadingMore: false, + end: false, + messages: this.data.slice(), + showScollToBottomButton: false + }; + this.data.addListener(this.updateState); + } + + // shouldComponentUpdate(nextProps, nextState) { + // const { + // loadingMore, loading, end, showScollToBottomButton, messages + // } = this.state; + // const { window } = this.props; + // return end !== nextState.end + // || loadingMore !== nextState.loadingMore + // || loading !== nextState.loading + // || showScollToBottomButton !== nextState.showScollToBottomButton + // // || messages.length !== nextState.messages.length + // || !equal(messages, nextState.messages) + // || window.width !== nextProps.window.width; + // } + + componentWillUnmount() { + this.data.removeAllListeners(); + this.updateState.stop(); + } + + // eslint-disable-next-line react/sort-comp + updateState = debounce(() => { + this.setState({ messages: this.data.slice(), loading: false, loadingMore: false }); + }, 300); + + onEndReached = async() => { + const { + loadingMore, loading, end, messages + } = this.state; + if (loadingMore || loading || end || messages.length < 50) { + return; + } + + this.setState({ loadingMore: true }); + const { room } = this.props; + try { + const result = await RocketChat.loadMessagesForRoom({ rid: room.rid, t: room.t, latest: this.data[this.data.length - 1].ts }); + this.setState({ end: result.length < 50 }); + } catch (e) { + this.setState({ loadingMore: false }); + log('ListView.onEndReached', e); + } + } + + scrollToBottom = () => { + requestAnimationFrame(() => { + this.list.scrollToOffset({ offset: -100 }); + }); + } + + handleScroll = (event) => { + if (event.nativeEvent.contentOffset.y > 0) { + this.setState({ showScollToBottomButton: true }); + } else { + this.setState({ showScollToBottomButton: false }); + } + } + + renderFooter = () => { + const { loadingMore, loading } = this.state; + if (loadingMore || loading) { + return ; + } + return null; + } + + render() { + const { renderRow, window } = this.props; + const { showScollToBottomButton, messages } = this.state; + return ( + + + this.list = ref} + keyExtractor={item => item._id} + data={messages} + extraData={this.state} + renderItem={({ item, index }) => renderRow(item, messages[index + 1])} + style={styles.list} + onScroll={this.handleScroll} + inverted + removeClippedSubviews + initialNumToRender={10} + onEndReached={this.onEndReached} + onEndReachedThreshold={0.5} + maxToRenderPerBatch={20} + ListFooterComponent={this.renderFooter} + {...scrollPersistTaps} + /> + window.height} + /> + + ); + } +} diff --git a/app/views/RoomView/ListView.js b/app/views/RoomView/ListView.js deleted file mode 100644 index 962cc07f6..000000000 --- a/app/views/RoomView/ListView.js +++ /dev/null @@ -1,277 +0,0 @@ -import { ListView as OldList } from 'realm/react-native'; -import React from 'react'; -import { - TouchableOpacity, ScrollView, ListView as OldList2, ImageBackground, ActivityIndicator -} from 'react-native'; -import moment from 'moment'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; - -import Separator from './Separator'; -import styles from './styles'; -import database from '../../lib/realm'; -import scrollPersistTaps from '../../utils/scrollPersistTaps'; -import debounce from '../../utils/debounce'; -import RocketChat from '../../lib/rocketchat'; -import log from '../../utils/log'; -import { CustomIcon } from '../../lib/Icons'; -import { isIOS, isNotch } from '../../utils/deviceInfo'; - -const DEFAULT_SCROLL_CALLBACK_THROTTLE = 100; - -export class DataSource extends OldList.DataSource { - getRowData(sectionIndex: number, rowIndex: number): any { - const sectionID = this.sectionIdentities[sectionIndex]; - const rowID = this.rowIdentities[sectionIndex][rowIndex]; - return this._getRowData(this._dataBlob, sectionID, rowID); - } - _calculateDirtyArrays() { // eslint-disable-line - return false; - } -} - -const ds = new DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id }); - -export class List extends React.Component { - static propTypes = { - onEndReached: PropTypes.func, - renderFooter: PropTypes.func, - renderRow: PropTypes.func, - room: PropTypes.object - }; - - constructor(props) { - super(props); - this.data = database - .objects('messages') - .filtered('rid = $0', props.room.rid) - .sorted('ts', true); - this.state = { - loading: true, - loadingMore: false, - end: false, - showScollToBottomButton: false - }; - this.dataSource = ds.cloneWithRows(this.data); - } - - componentDidMount() { - this.updateState(); - this.data.addListener(this.updateState); - } - - shouldComponentUpdate(nextProps, nextState) { - const { loadingMore, loading, end, showScollToBottomButton } = this.state; - return end !== nextState.end || loadingMore !== nextState.loadingMore || loading !== nextState.loading || showScollToBottomButton !== nextState.showScollToBottomButton; - } - - componentWillUnmount() { - this.data.removeAllListeners(); - this.updateState.stop(); - } - - // eslint-disable-next-line react/sort-comp - updateState = debounce(() => { - this.setState({ loading: true }); - this.dataSource = this.dataSource.cloneWithRows(this.data); - this.setState({ loading: false }); - }, 300); - - onEndReached = async() => { - const { loadingMore, end } = this.state; - if (loadingMore || end || this.data.length < 50) { - return; - } - - this.setState({ loadingMore: true }); - const { room } = this.props; - try { - const result = await RocketChat.loadMessagesForRoom({ rid: room.rid, t: room.t, latest: this.data[this.data.length - 1].ts }); - this.setState({ end: result.length < 50, loadingMore: false }); - } catch (e) { - this.setState({ loadingMore: false }); - log('ListView.onEndReached', e); - } - } - - scrollToBottom = () => { - this.listView.scrollTo({ x: 0, y: 0, animated: true }); - } - - handleScroll= (event) => { - if (event.nativeEvent.contentOffset.y > 0) { - this.setState({ showScollToBottomButton: true }); - } else { - this.setState({ showScollToBottomButton: false }); - } - } - - renderFooter = () => { - const { loadingMore, loading } = this.state; - if (loadingMore || loading) { - return ; - } - return null; - } - - getScrollButtonStyle = () => { - let right = 30; - if (isIOS) { - right = isNotch ? 45 : 30; - } - return ({ - position: 'absolute', - width: 42, - height: 42, - alignItems: 'center', - justifyContent: 'center', - right, - bottom: 70, - backgroundColor: '#EAF2FE', - borderRadius: 20 - }) - } - - render() { - const { renderRow } = this.props; - const { showScollToBottomButton } = this.state; - const scrollButtonStyle = this.getScrollButtonStyle(); - return ( - - this.listView = ref} - style={styles.list} - data={this.data} - keyExtractor={item => item._id} - onEndReachedThreshold={100} - renderFooter={this.renderFooter} - onEndReached={this.onEndReached} - dataSource={this.dataSource} - renderRow={(item, previousItem) => renderRow(item, previousItem)} - initialListSize={1} - pageSize={20} - onScroll={this.handleScroll} - testID='room-view-messages' - {...scrollPersistTaps} - /> - {showScollToBottomButton ? ( - - - - ) : null} - - ); - } -} - -@connect(state => ({ - lastOpen: state.room.lastOpen -}), null, null, { forwardRef: true }) -export class ListView extends OldList2 { - constructor(props) { - super(props); - this.state = { - curRenderedRowsCount: 10 - // highlightedRow: ({}: Object) - }; - } - - getInnerViewNode() { - return this.refs.listView.getInnerViewNode(); - } - - scrollTo(...args) { - this.refs.listView.scrollTo(...args); - } - - setNativeProps(props) { - this.refs.listView.setNativeProps(props); - } - - static DataSource = DataSource; - - render() { - const bodyComponents = []; - - // const stickySectionHeaderIndices = []; - - // const { renderSectionHeader } = this.props; - - const header = this.props.renderHeader ? this.props.renderHeader() : null; - const footer = this.props.renderFooter ? this.props.renderFooter() : null; - // let totalIndex = header ? 1 : 0; - - const { data } = this.props; - let count = 0; - - for (let i = 0; i < this.state.curRenderedRowsCount && i < data.length; i += 1, count += 1) { - const message = data[i]; - const previousMessage = data[i + 1]; - bodyComponents.push(this.props.renderRow(message, previousMessage)); - - - if (!previousMessage) { - bodyComponents.push(); - continue; // eslint-disable-line - } - - const showUnreadSeparator = this.props.lastOpen - && moment(message.ts).isAfter(this.props.lastOpen) - && moment(previousMessage.ts).isBefore(this.props.lastOpen); - const showDateSeparator = !moment(message.ts).isSame(previousMessage.ts, 'day'); - - if (showUnreadSeparator || showDateSeparator) { - bodyComponents.push(); - } - } - - const { ...props } = this.props; - if (!props.scrollEventThrottle) { - props.scrollEventThrottle = DEFAULT_SCROLL_CALLBACK_THROTTLE; - } - if (props.removeClippedSubviews === undefined) { - props.removeClippedSubviews = true; - } - /* $FlowFixMe(>=0.54.0 site=react_native_fb,react_native_oss) This comment - * suppresses an error found when Flow v0.54 was deployed. To see the error - * delete this comment and run Flow. */ - Object.assign(props, { - onScroll: this._onScroll, - /* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This - * comment suppresses an error when upgrading Flow's support for React. - * To see the error delete this comment and run Flow. */ - // stickyHeaderIndices: this.props.stickyHeaderIndices.concat(stickySectionHeaderIndices,), - - // Do not pass these events downstream to ScrollView since they will be - // registered in ListView's own ScrollResponder.Mixin - onKeyboardWillShow: undefined, - onKeyboardWillHide: undefined, - onKeyboardDidShow: undefined, - onKeyboardDidHide: undefined - }); - - const image = data.length === 0 ? { uri: 'message_empty' } : null; - return ( - [ - , - - {header} - {bodyComponents} - {footer} - - ] - ); - } -} -ListView.DataSource = DataSource; diff --git a/app/views/RoomView/ScrollBottomButton.js b/app/views/RoomView/ScrollBottomButton.js new file mode 100644 index 000000000..ad9429d0d --- /dev/null +++ b/app/views/RoomView/ScrollBottomButton.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { TouchableOpacity, StyleSheet } from 'react-native'; +import PropTypes from 'prop-types'; + +import { isNotch } from '../../utils/deviceInfo'; +import { CustomIcon } from '../../lib/Icons'; +import { COLOR_BUTTON_PRIMARY } from '../../constants/colors'; + +const styles = StyleSheet.create({ + button: { + position: 'absolute', + width: 42, + height: 42, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#EAF2FE', + borderRadius: 21, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 1 + }, + shadowOpacity: 0.20, + shadowRadius: 1.41, + elevation: 2 + } +}); + +let right; +let bottom = 80; +if (isNotch) { + bottom = 120; +} + +const ScrollBottomButton = React.memo(({ show, onPress, landscape }) => { + if (show) { + if (landscape) { + right = 45; + } else { + right = 30; + } + return ( + + + + ); + } + return null; +}); + +ScrollBottomButton.propTypes = { + show: PropTypes.bool.isRequired, + onPress: PropTypes.func.isRequired, + landscape: PropTypes.bool +}; +export default ScrollBottomButton; diff --git a/app/views/RoomView/Separator.js b/app/views/RoomView/Separator.js index 181d87f19..17ac04713 100644 --- a/app/views/RoomView/Separator.js +++ b/app/views/RoomView/Separator.js @@ -8,10 +8,9 @@ const styles = StyleSheet.create({ container: { flexDirection: 'row', alignItems: 'center', - marginBottom: 25, - marginTop: 15, - marginHorizontal: 15, - transform: [{ scaleY: -1 }] + marginTop: 16, + marginBottom: 4, + marginHorizontal: 14 }, line: { backgroundColor: '#9ea2a8', @@ -40,7 +39,7 @@ const styles = StyleSheet.create({ } }); -const DateSeparator = ({ ts, unread }) => { +const DateSeparator = React.memo(({ ts, unread }) => { const date = ts ? moment(ts).format('MMM DD, YYYY') : null; if (ts && unread) { return ( @@ -65,7 +64,7 @@ const DateSeparator = ({ ts, unread }) => { ); -}; +}); DateSeparator.propTypes = { ts: PropTypes.instanceOf(Date), diff --git a/app/views/RoomView/UploadProgress.js b/app/views/RoomView/UploadProgress.js index f3c969c40..f4560b3bf 100644 --- a/app/views/RoomView/UploadProgress.js +++ b/app/views/RoomView/UploadProgress.js @@ -11,6 +11,7 @@ import RocketChat from '../../lib/rocketchat'; import log from '../../utils/log'; import I18n from '../../i18n'; import { CustomIcon } from '../../lib/Icons'; +import { COLOR_SEPARATOR } from '../../constants/colors'; const styles = StyleSheet.create({ container: { @@ -23,7 +24,7 @@ const styles = StyleSheet.create({ backgroundColor: '#F1F2F4', height: 54, borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: '#CACED1', + borderColor: COLOR_SEPARATOR, justifyContent: 'center', paddingHorizontal: 20 }, diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index c7da383b5..37aaededc 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -7,11 +7,12 @@ import { connect } from 'react-redux'; import { RectButton } from 'react-native-gesture-handler'; import { SafeAreaView } from 'react-navigation'; import equal from 'deep-equal'; +import moment from 'moment'; -import { openRoom as openRoomAction, closeRoom as closeRoomAction, setLastOpen as setLastOpenAction } from '../../actions/room'; +import { openRoom as openRoomAction, closeRoom as closeRoomAction } from '../../actions/room'; import { toggleReactionPicker as toggleReactionPickerAction, actionsShow as actionsShowAction } from '../../actions/messages'; import LoggedView from '../View'; -import { List } from './ListView'; +import { List } from './List'; import database from '../../lib/realm'; import RocketChat from '../../lib/rocketchat'; import Message from '../../containers/message'; @@ -28,6 +29,7 @@ import ConnectionBadge from '../../containers/ConnectionBadge'; import { CustomHeaderButtons, Item } from '../../containers/HeaderButton'; import RoomHeaderView from './Header'; import StatusBar from '../../containers/StatusBar'; +import Separator from './Separator'; @connect(state => ({ user: { @@ -41,7 +43,6 @@ import StatusBar from '../../containers/StatusBar'; appState: state.app.ready && state.app.foreground ? 'foreground' : 'background' }), dispatch => ({ openRoom: room => dispatch(openRoomAction(room)), - setLastOpen: date => dispatch(setLastOpenAction(date)), toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message)), actionsShow: actionMessage => dispatch(actionsShowAction(actionMessage)), closeRoom: () => dispatch(closeRoomAction()) @@ -70,7 +71,6 @@ export default class RoomView extends LoggedView { static propTypes = { navigation: PropTypes.object, openRoom: PropTypes.func.isRequired, - setLastOpen: PropTypes.func.isRequired, user: PropTypes.shape({ id: PropTypes.string.isRequired, username: PropTypes.string.isRequired, @@ -92,9 +92,14 @@ export default class RoomView extends LoggedView { this.state = { loaded: false, joined: this.rooms.length > 0, - room: {} + room: {}, + lastOpen: null }; + this.beginAnimating = false; this.onReactionPress = this.onReactionPress.bind(this); + setTimeout(() => { + this.beginAnimating = true; + }, 300); } componentDidMount() { @@ -181,7 +186,7 @@ export default class RoomView extends LoggedView { }; internalSetState = (...args) => { - if (isIOS) { + if (isIOS && this.beginAnimating) { LayoutAnimation.easeInEaseOut(); } this.setState(...args); @@ -189,7 +194,7 @@ export default class RoomView extends LoggedView { // eslint-disable-next-line react/sort-comp updateRoom = () => { - const { openRoom, setLastOpen } = this.props; + const { openRoom } = this.props; if (this.rooms.length > 0) { const { room: prevRoom } = this.state; @@ -201,9 +206,9 @@ export default class RoomView extends LoggedView { ...room }); if (room.alert || room.unread || room.userMentions) { - setLastOpen(room.ls); + this.setLastOpen(room.ls); } else { - setLastOpen(null); + this.setLastOpen(null); } } } else { @@ -226,13 +231,14 @@ export default class RoomView extends LoggedView { } sendMessage = (message) => { - const { setLastOpen } = this.props; LayoutAnimation.easeInEaseOut(); RocketChat.sendMessage(this.rid, message).then(() => { - setLastOpen(null); + this.setLastOpen(null); }); }; + setLastOpen = lastOpen => this.setState({ lastOpen }); + joinRoom = async() => { try { const result = await RocketChat.joinRoom(this.rid); @@ -275,8 +281,46 @@ export default class RoomView extends LoggedView { } renderItem = (item, previousItem) => { - const { room } = this.state; + const { room, lastOpen } = this.state; const { user } = this.props; + let dateSeparator = null; + let showUnreadSeparator = false; + + if (!previousItem) { + dateSeparator = item.ts; + showUnreadSeparator = moment(item.ts).isAfter(lastOpen); + } else { + showUnreadSeparator = lastOpen + && moment(item.ts).isAfter(lastOpen) + && moment(previousItem.ts).isBefore(lastOpen); + if (!moment(item.ts).isSame(previousItem.ts, 'day')) { + dateSeparator = previousItem.ts; + } + } + + if (showUnreadSeparator || dateSeparator) { + return ( + + + + + ); + } return ( { - if (isIOS) { + const { navigation } = this.props; + if (isIOS && navigation.isFocused()) { LayoutAnimation.easeInEaseOut(); } this.setState(...args); diff --git a/app/views/RoomsListView/styles.js b/app/views/RoomsListView/styles.js index 5eea227a2..862aac978 100644 --- a/app/views/RoomsListView/styles.js +++ b/app/views/RoomsListView/styles.js @@ -1,5 +1,6 @@ import { StyleSheet } from 'react-native'; import { isIOS } from '../../utils/deviceInfo'; +import { COLOR_SEPARATOR } from '../../constants/colors'; export default StyleSheet.create({ container: { @@ -8,8 +9,8 @@ export default StyleSheet.create({ }, separator: { height: StyleSheet.hairlineWidth, - backgroundColor: '#E1E5E8', - marginLeft: 78 + backgroundColor: COLOR_SEPARATOR, + marginLeft: 73 }, list: { width: '100%', @@ -36,7 +37,7 @@ export default StyleSheet.create({ dropdownContainerHeader: { height: 41, borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: '#E1E5E8', + borderColor: COLOR_SEPARATOR, alignItems: 'center', backgroundColor: isIOS ? '#fff' : '#54585E', flexDirection: 'row' @@ -79,7 +80,7 @@ export default StyleSheet.create({ }, sortSeparator: { height: StyleSheet.hairlineWidth, - backgroundColor: '#CBCED1', + backgroundColor: COLOR_SEPARATOR, marginHorizontal: 15, flex: 1 }, @@ -154,7 +155,7 @@ export default StyleSheet.create({ }, serverSeparator: { height: StyleSheet.hairlineWidth, - backgroundColor: '#E1E5E8', + backgroundColor: COLOR_SEPARATOR, marginLeft: 72 } }); diff --git a/app/views/SearchMessagesView/styles.js b/app/views/SearchMessagesView/styles.js index 976453264..4b524b73f 100644 --- a/app/views/SearchMessagesView/styles.js +++ b/app/views/SearchMessagesView/styles.js @@ -1,4 +1,5 @@ import { StyleSheet } from 'react-native'; +import { COLOR_SEPARATOR } from '../../constants/colors'; export default StyleSheet.create({ container: { @@ -19,7 +20,7 @@ export default StyleSheet.create({ divider: { width: '100%', height: StyleSheet.hairlineWidth, - backgroundColor: '#E7EBF2', + backgroundColor: COLOR_SEPARATOR, marginVertical: 20 }, listEmptyContainer: { diff --git a/app/views/SidebarView/styles.js b/app/views/SidebarView/styles.js index 87c1e75a6..420c66c27 100644 --- a/app/views/SidebarView/styles.js +++ b/app/views/SidebarView/styles.js @@ -1,4 +1,5 @@ import { StyleSheet } from 'react-native'; +import { COLOR_SEPARATOR } from '../../constants/colors'; export default StyleSheet.create({ container: { @@ -27,7 +28,7 @@ export default StyleSheet.create({ }, separator: { borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: '#E1E5E8', + borderColor: COLOR_SEPARATOR, marginVertical: 4 }, header: { diff --git a/e2e/07-createroom.spec.js b/e2e/07-createroom.spec.js index 98ab91e54..90767ba3b 100644 --- a/e2e/07-createroom.spec.js +++ b/e2e/07-createroom.spec.js @@ -117,8 +117,8 @@ describe('Create room screen', () => { await element(by.id('create-channel-submit')).tap(); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000); await expect(element(by.id('room-view'))).toBeVisible(); - await waitFor(element(by.text(room))).toBeVisible().withTimeout(60000); - await expect(element(by.text(room))).toBeVisible(); + await waitFor(element(by.text(room))).toExist().withTimeout(60000); + await expect(element(by.text(room))).toExist(); await tapBack(); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeVisible().withTimeout(60000); @@ -141,8 +141,8 @@ describe('Create room screen', () => { await element(by.id('create-channel-submit')).tap(); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000); await expect(element(by.id('room-view'))).toBeVisible(); - await waitFor(element(by.text(room))).toBeVisible().withTimeout(60000); - await expect(element(by.text(room))).toBeVisible(); + await waitFor(element(by.text(room))).toExist().withTimeout(60000); + await expect(element(by.text(room))).toExist(); await tapBack(); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); await element(by.id('rooms-list-view-search')).replaceText(room); @@ -165,8 +165,8 @@ describe('Create room screen', () => { await element(by.id('create-channel-submit')).tap(); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000); await expect(element(by.id('room-view'))).toBeVisible(); - await waitFor(element(by.text(room))).toBeVisible().withTimeout(60000); - await expect(element(by.text(room))).toBeVisible(); + await waitFor(element(by.text(room))).toExist().withTimeout(60000); + await expect(element(by.text(room))).toExist(); await tapBack(); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); await element(by.id('rooms-list-view-search')).replaceText(room); diff --git a/e2e/12-broadcast.spec.js b/e2e/12-broadcast.spec.js index 018ba3c92..032001c00 100644 --- a/e2e/12-broadcast.spec.js +++ b/e2e/12-broadcast.spec.js @@ -26,8 +26,8 @@ describe('Broadcast room', () => { await element(by.id('create-channel-submit')).tap(); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000); await expect(element(by.id('room-view'))).toBeVisible(); - await waitFor(element(by.text(`broadcast${ data.random }`))).toBeVisible().withTimeout(60000); - await expect(element(by.text(`broadcast${ data.random }`))).toBeVisible(); + await waitFor(element(by.text(`broadcast${ data.random }`))).toExist().withTimeout(60000); + await expect(element(by.text(`broadcast${ data.random }`))).toExist(); await element(by.id('room-view-header-actions')).tap(); await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000); await element(by.id('room-actions-info')).tap(); @@ -76,8 +76,8 @@ describe('Broadcast room', () => { await expect(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toExist(); await element(by.id(`rooms-list-view-item-broadcast${ data.random }`)).tap(); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000); - await waitFor(element(by.text(`broadcast${ data.random }`))).toBeVisible().withTimeout(60000); - await expect(element(by.text(`broadcast${ data.random }`))).toBeVisible(); + await waitFor(element(by.text(`broadcast${ data.random }`))).toExist().withTimeout(60000); + await expect(element(by.text(`broadcast${ data.random }`))).toExist(); }); it('should not have messagebox', async() => { diff --git a/e2e/data.js b/e2e/data.js index 850d9df59..512512b58 100644 --- a/e2e/data.js +++ b/e2e/data.js @@ -7,7 +7,7 @@ const data = { password: `password${ value }`, alternateUser: 'detoxrn', alternateUserPassword: '123', - alternateUserTOTPSecret: 'KESVIUCQMZWEYNBMJJAUW4LYKRBVWYZ7HBWTIWDPIAZUOURTF4WA', + alternateUserTOTPSecret: 'I5SGETK3GBXXA7LNLMZTEJJRIN3G6LTEEE4G4PS3EQRXU4LNPU7A', email: `diego.mello+e2e${ value }@rocket.chat`, random: value } diff --git a/storybook/stories/Message.js b/storybook/stories/Message.js index 6bd0b1e84..9081b4dc9 100644 --- a/storybook/stories/Message.js +++ b/storybook/stories/Message.js @@ -8,9 +8,8 @@ import MessageSeparator from '../../app/views/RoomView/Separator'; const styles = StyleSheet.create({ separator: { - transform: [{ scaleY: -1 }], - marginBottom: 30, - marginTop: 0 + marginTop: 30, + marginBottom: 0 } }); @@ -44,14 +43,15 @@ const Message = props => ( const Separator = ({ title }) => ; export default ( - + - + - + + - - + - + - + + - + - - + - + - + - + + {}} /> - + {}} /> - + - + - + - + - + - + - + - + - + - - alert('broadcast!')} /> + alert('broadcast!')} /> - + + alert('Error pressed')} header={false} /> alert('Error pressed')} /> - - + - + - + - + - + + - - + - + - + - + - + + - + - - + - + - + - + - + - + + - - + - + + - - + + - );