From d4d062bd6e91306b40707b9be435c7f1fa66f894 Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Sat, 8 Aug 2026 15:44:13 +0200 Subject: [PATCH 1/8] feat(support): give the customer chat DFX colours and drop the dead reply UI Replies and emoji reactions were fully built but switched off by a commented-out line, so the menu could never open. Nothing stored them either: the SDK never sends replyTo and the API knows neither field. The unreachable code also made the 100 % coverage this repository requires impossible to meet, so it goes. - customer bubble in dfxBlue-800 instead of Telegram's #24A1DE, timestamps and the author name in palette colours - date separator on the calendar day, above the first message, with Today and Yesterday - delivery status only on the customer's own messages - author name also above an attachment - remove the reply preview, reaction chips, bubble menu, the unreachable document lightbox and a dead early return --- ...support-chat-01-thread-chromium-darwin.png | Bin 0 -> 33707 bytes ...ort-chat-02-attachment-chromium-darwin.png | Bin 0 -> 27933 bytes ...support-chat-03-status-chromium-darwin.png | Bin 0 -> 30009 bytes e2e/support-chat.spec.ts | 206 ++++ scripts/handbook/metadata.json | 4 + src/__tests__/chat.screen.test.tsx | 1025 +++++++++++++++++ src/__tests__/support-helpers.test.ts | 89 +- src/screens/chat.screen.tsx | 289 +---- src/translations/languages/de.json | 2 + src/translations/languages/fr.json | 2 + src/translations/languages/it.json | 2 + src/util/support-helpers.ts | 30 + 12 files changed, 1409 insertions(+), 240 deletions(-) create mode 100644 e2e/screenshots/baseline/support-chat.spec.ts-support-chat-01-thread-chromium-darwin.png create mode 100644 e2e/screenshots/baseline/support-chat.spec.ts-support-chat-02-attachment-chromium-darwin.png create mode 100644 e2e/screenshots/baseline/support-chat.spec.ts-support-chat-03-status-chromium-darwin.png create mode 100644 e2e/support-chat.spec.ts create mode 100644 src/__tests__/chat.screen.test.tsx diff --git a/e2e/screenshots/baseline/support-chat.spec.ts-support-chat-01-thread-chromium-darwin.png b/e2e/screenshots/baseline/support-chat.spec.ts-support-chat-01-thread-chromium-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..eac3b208570a16602d0c75005038096bde1f0530 GIT binary patch literal 33707 zcmeFZWn9z!-#0u&;7~-4!$Ml6TSAal5n-fsgG#q_ZlEZLNSAcy7)Uomk?w9rBco%) zVB`LB{IB!6@8{=zoe%EUy(imi`@Meo{nR@a{8~kh;yTTB2n0d_m4B%YfslZYR|fvN z0^Xi6DiA>+e?y=zpKE$1Z_bkV-uukhzGvXUmQ`Q>yzb?Vmw&&sz8a~X9UDVRLUb+X zpV)2iqI*2?hL(H`f3YR%MR1|G@o9DqpLnpHx=|lAdkm*)v-phR1UrX7%skpIj|+j& zJcE#4zFS=(x_|kee(ma;%Xfd)zx}}pKp=_#gByH|EH#xPfw+GA_Kh>1$HCrwkNL5# zm#FYqoGke=^o#0W;a@b3^)q~HC1#RgX;*Vc0}#H{@wxkU+Tu1V|_0A3U_%P z+`fL}rkZhLNEYm8gDBIZtjsLan4Gc!9MXAAPEsw1j#gQXIm zKUnXEpcjP=HaDlcmVVT}w_4_=K5H6%lx}5k4?eD*Q*1*nSUw*i;;Oaw-mNQbnu%FR zLReVEOLRXk!(?2zz0WwTE7Q;3xCb$p3z@rGJ7a`%Cmc5KxfH;~id5zL&*oHRT4Jco z^CAA3-xCwhh2IWjY0%y*I>(+@wWbE?V9&}z_ix$g@OU^cHQYqbX_Wn9MBR{Xuuu_` z)GX1(Y#i|lN%Wu;8v-ueVa zk~#zD;?y4HPi|lty5;z8|Ks>|^P15c6cpwK22T(r4R51*b>lXT_@Jwn^8M}>xw+18 zg{nTjtR}6YiOb7&Du9n0t0z&?!lx@Mr;3U(X@OXEf-Q87lVhFTg`LC5%Q_7<@k6Iw zwd+p*N43UpI;F})4c1L|XA8%T;yp=1PgmrI)TA&oIM@oy`)fjw)?YJry^c#5t&$&h zvtdY|@J_W^Dn=1%kstNt0kZ>8m)Po`IC?Tg;geqnAHw(DZPfEH{@eDdsVLlhWZJ$- zl91(OQ)xk>~*Q5$E3CLgoP zJ1W>&9-ba^mKpK7)tNC_^)f}#hkC|P)X)!?$qE*YpaOa_zuE1Msandgk~0@v$Mu}A z7@xS-Nz}fMUXDu}#bd!IgC5nDRX8utv?Q`P#Yy*_ed;G%v2kI_Had}RPe4q-9LqRz z81W4`m?5mVPgruCesN~Krj|m@S~btjx(fWSBbF#k zte>Mfb>~(9S34!HYh;9Zlv=UQqtZ&>@*tBVT*Yyw?1WA4J-!QSDVbd^(^VA#ElChp zv8O+^r=kwWnU|JTM1)uIoTJBJd-*r;I3rwKf7u&Tm``dM2JJG5x_@W|Ls+9r`r~ zI~#Ol!^gF-m!M30Pl;2T-4Tai??%}cQcZm1JoQj=&J~hRGpRn)MK{WH=UP^A$DBgF z7nA}KVkxsvM@cp(7j7qP$EU&Ad-t~@+~D2^S42$`#the2x)!1^V@g;PH(nQEQgiK4Ilkz9=R9*w4FVPFQ^YS@~fB#1rBB#C6|f!vi{F#J?eT z>fYU-SZkW2CDtX^)%_4c{JJoPix+jFkJ3Q1JQA?M_sI;su2tec-P*KbD|mKMfG7_L zi4G9@o{*s6A-WqIkOYFgjqNuZ?SYPo5T7wSy3Hs^@m?QlGyWgvm-&Aj7WJhaGqV~) zNA8BFHO}N|AQoWa>+NIprrrBFUyV^NCSk~nR8jpXo1uTI2*Qgi%>_9tW#rP54<$p&Q$MY+_@m&4b7;+}9he3veLGnYKn_-;KjO5`h5S5l2a$nr(mRuC>yo?&}W_Fy(hrBgpP z)y;Oo9bM|;d@@nxWm=imAUeD9Yu`1^UjVtWNCYmJ%e9L3f|f#S@I6?`Li6M z(W!c>!FS2w!aqHxUZapa=a0Mm91t1i;XkPR6N#P z;vE&rL!x0rJ8uHJ;hBpG(+fU%f-k*@pOscs+BtQuElHEkaIRep(ROHpp$!UW zoZmf~^sgF=6UdlcBE3oyME4Y4Y~5&6rm&*)scy=gt0S?&`(Qg%+<4tr?ofR9*WIL$ zdmZK03(Rh0aoc6nGuVB-AHLB&8OeKU0k8G{jU&D)60Go!R3@6gc%}g%OvRUM5*US7 zZW1U{pdBxa>eNaZb4VV$(syQ?b>7YGQ<)O#hVDZyrwu2BXa@z#W{c@S@e)O4!v2Yp`pNdia^M`OauSd(o3^t>_`;e0**@SvorX;9oEZ+|`;u8ID; zIjQKt-^s5Sefqp5kqeG@S}EdJ#%LPVS=75~8q?^g6TKcWOx)~PFD%vyBdKP58cCS1 z-j=c&Do$h0iDC4TJ}Q~6i}z(FTlDL_KbfI5TpsV}>2+51M@qBn8ztjuCr4v?$aD^& zi1FpV|43TS+r55i`S(#B)f)pGSf&Cd==w8-VSAtOFCmHt3A`(A*r8Ivd-PInYU+)2 zUTFMrE8-SBJMLs-aTFbzz=3q0*UGWmGh5G;s}DCyp_9UUPXwj}WfxcawU(&EsgF0* zZ<{i8GJ}*I$Q;#l$+n1^44cyT_om@>aU&zBGls*HW=98!kEyg&WTm zRQ;sHAB{vay$}tI7MPvCR!*7vluxc2?Ru)It%(D1&htm$dRrnPBMmD*Fs3UXC z{yL*=HP?EgwAYt&`2Z!@?k7>q({f*gBn3tEO6mOO`*~r;g%Xev%mBt~b`Q7IB*U5FD-y~d4>s-+dYhN6n&Ex2U%t)7ATf(Gs z$B^0}yrZGjWrtO7x&|gDYAP(0SjcdZYJ(KhN3qxkAJ%?_u{cjOI-GrQ9g_(ZU#^0+ zXZIJbD0T>Q8WlCecG^KfRJ*lvgAV~Ol#u2kKfo}$`RsP^dE6X7n5OD3$gk}lJGQ$B z=5{>IO(~wMjuqCWqeZb#B_mk($|^w#nJiq@46(-)2t64W^6QHTg}bI42GPyjN0%1j`tGI zV<5WkpDC3$-uZL8)X%>uD_Z7VL9YljzM^CBec`&!=F|5t@a(1ORE#E7o)4{W-nfFTYB5!wRWw^= z()E|Yy^D&C`Oe1nJ_V9jU93=_d`+>^A|7+DO06)|XrMC-`Nmm+Zk?{7;X4N%*Ndx) zAEU&)a)yHQh|O+pjOV|9dln@rr+U!{kN4I{rnBzcDNchu2ts|`4ldX?xHmBQQw7@d zxIGOSWJ*n#Hp-}gne4Dnh`626=%q8|evGVjqz19GoxQpB+~Ta*E%2$RP08f2 zusYFrXqE%{`IhrbpBFl!@wNM4RLWyNGmy7B`Qj)hQm7@1F;a)e46w{L`uZNM^gZm% zdrvUba-|9>T!Plc%B~TplXc=A*%v$_2}@}#VrY`yCx7Zi&~L=7Ah~&l^eCyfBg!T*V&3;eIu`RrQrEu^YR1HtX= z-cZi?kF?kxsO`UNQca*=e67Ht>qcG;b_cMhC7i5n@BRx5&_Gz6Y4;@a*QLRe2_Jr{UP7^kyb2bH!18u z`cOu_GUVVu`<$G#yxQsbSxuJ;Dx(Oem>8Zqf@U3qJc}!uJ94VdaBi5ZH&yK9De*?+ z&}oU0vn)oN(c)jOe6R5gsES_YE-x*+KeBYtT~Dw&Q)60}Dl~~t>CT1CWV^q4-!U{g zeMrnY-9;URbu=^k?%E&x^=n*7z~@@)vIJGJz8fAL(Q74LPJYiXT z`m5!^$Ghcn;kd<0S8cWbX6bg)7BA zaSSED@K2=r+hXmePa#>2n&B(tZtBNmH=7OQ!X#H!X&&V%uPn!(%eS0&%0x-oEq+!b zB2A|gb5jdbkUQFVv_gWt>O1PpG6kDqe(+~~g*J7F=I=X?WE&Pg?;AC`jgNNz9PK1D zRXbX=75SYgSMIZ{+`zZmdSutmvh8&7TVYpcbN*r?Nf^Y}u_r8PF9eLbv{%_^t6ryp z?&zwp#@C0`hnQCXyq#04(;nw6F}oTN2xYwQso>GWLnL$k`Ot%f*HH_bIkwSZFL4}T z*mIS3dsw>W;fhTbq$=sYp*ZMvMp$x*M|xCLOICQh2dU1{KiwW5j=$A^N1nvAcKb+8 z#8U2*O&_BO`Y|>YMGsj-9fNu!pIQ!~y!h1>JaJmr9X>OAb0>Wd*2Q0y1_ExAVsfsP zxitMa4)q7&hp6`}aSU~AGR!bcY-WAE&+fR1>qsNF4EE@y_}!Z~NpeX6EGX)6bfUX5N=c z>27u(I$zt|#iiURNCkHyRqK3E>`P?MIeo;aV=QkyyP=-bz#6+O(0Y9iwkyxWs`w-M zv28&j^!oDNF6`EK8*}5a2K_3nV_$4il9E39pC`r4anIJcHC&Iqj;xMLH$5#~(UBxv z!pGG7T7#`WW*8jGjE3}OZ(HCaSJ0JI(;xXB93aGUD(<~b%*ocP)BWir7=O6A+qvn4 z-+)hX3F;8%);$@<=}02^BBQQZ_j>7H*md2=mPUq*J8)Rr4Z9L#aW1ocd{PcW_>fgF zwnXr6)OVxP|{0qHLizy$`X&xPiDo`y$E%%5TzyyVNS7EH;u)Q89i(c&P`q& zN&4>p0u4>Uk*LM9YGFp<7}J-~;7SCuaj?hMQDe5`rkJb3O|Kx|S(U_qk20_Pj&r#C zvPCbvhx-|AtGXYK{Nj*dxf#ozd=U`%ge7yv=R`hi^|<6a$En%!2+2)%VNt^rGvP;% zvY4yoOAqrHy$uqRq_16P0Q0%%GSA?=%Qc2ulxDC#29f#2%4rkWoS_Ly8Pk(PyI*fG zO_|fcR91yTy0qH&a)t@6(|wa+ZPR9OJ32W{e}&7c6%?hTy^eq z)^Uxp)W-7Z>6SSWBp4(Kc;*T74R?76p$W8{hdP3&Y;?K7Jq7wAjGResGIb}zio%_$ z%F;rUtjzMPl%8*QLQPt=L&Cx$pKy>rB5!h^{$SRUi+WvPv*(baXL{D1f3IjFn_&Zy zWNcw9EVneJ_3qufwws&9J0`PTm-8hud3aX#V` z3g43AO$esEY&de^W&yJ?3Cxhtqxe*3>9{X|4d9MKrDq|eZ-U(sYhebJUaANS>q z#ZKtgZSNDlL=j{DjrO!PyuG8TB>a8&86~3@A>Fp?r+JBn>lyUIA+0m`alt_DCL40+ zi*NRJ^Dub2&X{*7zUhuc!&>X`mp`}2R9tPasX`s`nFo2jQZ|MN<&u9ksu<^8Fx*jw&hKjF3KDuYtG!thYu{bNwe89&<}LcTqMjGeN(VG2BYCA3I{gI!tG~ zuym)|eQFe|&{VhUtsrQrZYUlKxbU`KXo{>@G72mC>97AA5>q~hJuPws>BH>@7yccA zuf+*stKy=t*DC?V7~bpi6nVKOr(_BjG}-rNQ6>UdvUT=$X1>!0R=gibBmRuHBF@5+ z-Pf9`=_2JfMLqXlbeDjJW^=VAxcp+G;Y=O?#?luxojZQ^yI$wa(OI$F56vUeg0NL( zLX3|c;=F~VVL!XT^d%o@3Gl3N{1xU{js(^qWpr2~uPl;8BQRG>luqL9(O6?U^S-_Q z(^{3veNzi=<61ArA%@heq#_#=Chtx=9L2iTA~pS_Fk(-+&rMnrvMhNk&n#T+43p{7 z+^i-wZw@ZY8`UZPES#Q)I`D=eR=*{P!~N-oDlw5Ki;oH(G>&InP^gxyXEjA;85i(D zFKqVmw89y~lADx)*@m);S#8I5anhVW>_ zkKIIvih3?KWY))e51dS@{~f2rsb*+`xZzDB=J|_`V|CNy6hY_8tJ@^H-eI0PN|Qn` zeyZoz#@d#f+g4C46U=){U#!xGRrEEduz+9}*R&0|&?u%Ia8k;rLy3n}oLY)wc4y{4 zzvf~K?F}EVC-EXzDA$hbsPrglL==Yzb?A1!3v4?|WOM8|&6TVB+DCdal|PQeDzqVz zOt2MAFZl%5tnIscl(>-giG3(}=OTdK@LEs!ohK7|G3xfh0zyh_SLh&)H&5G@KW;B- z8A$I4D)^;@#F%Bfbzr8{yS52RsH3%HBR5Zp6(XnZX`$;qPM)Yzmb()BhWHi2E7;uS%T1^Eyu-}!>9)5 zrvkW(hQfyUBk>b@q%jQvJwTlRovl7VFO(SG3f!WJi+t zmewkO@e^C(t#6X4q+om5>m?*;>rV?I{US-RC+{>-yhfzoOpc1K6#ZfN5VvsqZ%D(n zea{-$U}k4cf}%QJQ$?3gzjkGzeAs}`A94dSIhnUJ?r2!wwbn~pGEtEygW|s)QJhVr zOEf3EqIfS7^Lm4l=BgwG&u}(AB&C8RqS1-tFNm3CFJ?U7xbzS_NFU0yLJ0L9BtXY>z9*l zXXnGLw1W+ew^#;<+jCVYPVi(OJV7kymD#ho&fqwded26&01E+2KsQN#i-7Q~e+R zbuIL@@rUenkELIo4t-&lr^Nh7pwO-4t(=OHrU5{;l)i;BvlJp*udhynP)Z4n-EVv9 zjT_39l+$l8sDHQqu8Z93lpov)QfJT}zCR4WY3sk;C!clo#tw$ENFZmDr%$frs*96t;m``t?5B#WPEsStnyI>XG;q4U)vueo6la?u|YYG z$XY@C?dE@=_>)fXoqN)0fYB};exMF#viagq{cu90rqvyu(DdhgG^yJ% z)cSSSyGU>Q?E1#g(O`-LtZB6MmZ-0SbG^y4*9?^7;b`RaQb;)crk%PGCdRbJtKGJz zB&1}fhKYQ|%F&?WO{x@_J$&=Z;1M82@(bQ$koH+wHR98_9sTRn*>B%CW!pXpaZOvg zdS|Ry0H!JJ76A~j>UGH%G#D)|X^-;<_pCi((QqP2$*;OCvXMYlv}1%2{wo+nQl=_iW2(Ov8y2|7=|tI!&wUdgWCqNPHFt$nE88Aq$`k^eQ)A2=oX zT*S2ST6w%m!dYI^=y3Pi8v0_Y;%qmUB$}l+*AQc7I^nTtSMT^r_T{Srr0SJ`@Z>sn z=`0*JT8)g4GBP4%HBsQXXnlj%815+Pl{_$- zDnhm4BZ!YBGl!dpv-hE>VwEt#SANxAgQ~Mr!aA;}?>n+@}OuqWuVJv86NE*UKpfnf)Wa>HC+!qq*W-%!_Ukxac!gY_1@vVLbBW^bf zy=T^Vw>_K6Gk{l*9!?iM@6EFHL)UJ`10YOsqgaNGWr8b#wY)GF4L~qFzx{7;)uR=t zi<3zbE)gyWq(Q5!=nA)GS>8t2t&zhdk-VfUkY=uQDN4ONeBV{kYBo{vj$W)=_PS{* zvE6sj6=z>){<0)Ys46=EZo9^1=R?fr95|P(esDVOxfpE9?tQrX(7@@i1q+QYZ>vFP z1cB&zU)Yf)?`*_SsqYA2*Bt9Av~Ll=-TU$-e-OnT(Y4zr!g_8O6R8}kH(Qe3ksX%z6DNc+EvwB2KL2M??v#U@!%QxE95;&{CkL^cED;J5s-OOt+^hKA?T9t)|Bu1k}i z4N)rkfy!+7w$@g}6J1(cMLbWvcHC;b`}x~3xuDb5~J^T`k+aa=$+ojxDFT$G{I8RDd0`)pch} z?)bF-{%rwIC4!vvn@=+Gh?+Cd-{z8igJvUQblq0{2PA{9HR9q06!DI8_&<8HmAz!4 zHkwKeUp+@xB{4auvADSXv!PIN8%h+umUkszqOrji!+S$p&mfV_x=Ma3?~eZ{DOP1F zW(hilv2x7IR`$LbF5h4#<`L$H&+si~=4i!$!dE;Ce;_RE0PvrECVG(e+9);$3-u(e zi`5DatBt0)b(=VzG9NB~w#AS~hlYkW*&QtW*=g!ozMX+B=d|F}kKNQrs2FELU8!|p z2WvE+s(7X8dy#~#c!O$Qe*Sk*g1n`V;%_v#b@tDY-hgyC?`ZOtyPv_WC!FSAUhqi+ zvM+KKKp2^DkjkYhA6nkjIZcsP4QnR^$1C+G3H??Yw^U~dxuYq-c2O=Tspwy1gfuKu zb*T~KN{E|oQ7$VUQGuPOvhyi?9Rjifx+%m`mvVi-VH14$ur*>}w|PEdr%lOGJ?|^d ze`CkT?ay%O=-1wQ)#Q5T!ycB?4em&{BSu@=(PH)bt_!M%n91mhJG00>XkHXo5W>s? zd-_%}k3v82-7kvcj+FN= zHu#vD2K|Iw_H0!77_Hfj*-aS2e?trlT=}s>RyuM)H2q&2j~ij3p*JLFQkg2dJV!|7 ze5P~V5QyIAsxtW0t9N@g_6#Xy;^>U)tbT&}K}ISm>2vxDC<*S&wzgFo!0ate^=j>c z)$nx|PoqHe0Pe@q&g{^rx>xxGf(C zUN9hxU0#1|5z0|6?Zb|bbleSYw>Q<6e(#7No?ZZeQOv!0QFOff=g)U@Tfk7dR~d(X z9gDk{$7?9*L@3OxAJF-lX48+g(TvjMrf3YjINS)wcgL~2?%`5O0+oP9t9O0X_dS*l zKkYxPQ|ijUeqe1iF+$RPuySOjiEw`QzN5KR@Q7aA_N_yeO0ejD_E_3{qkLSc6M^zQ? zay&pyAJf7P4_R3Dyv9|r*O=KU^F0Zl2yNwrgKO1Tcr{Fa6q zkuuXm^HkCOW;iX^&uElIn8WCjPw+>{rnH#DESnWYDIMHhw7Wg5<$wUtcn36WU^{6L zx6K=NGXUj~wlJ^my*0TV4Qll(S-ENgj*N*ZtER>;mENet`?I?2?Sn1lW93%+aGAPr~*k zm{)kyPM3Oe5URnl_a2}2Zk4=i51gX5dPxIGB!h@#QdXwZUw0-i8a%TF>he4utTivI?vKq^g)dxP| z45J;9@mmLM+7`?Gl9`IL1=EY6*6UTL>2J$%PohZPFfp~%E|Efwymul6bsW6AiWClG zu@Y}zQ&vXZfHZe|_EP>7__wJ~AX>6eAdL%raq8ljC>?y`GV=`%@<*YIkUh~&1MEmC z$;JkkmG|We`xR?c4<7CwH3r?R5>^Rw?^0B3h@6mqtjc>F)BmUI+mBs&zO;7Gv0H1E zv)~ov0f?xyX2YEj)H5?cOLuE^gYmnx$IZ^>%jnF|q4ketraq~@7GTg|AouTlIFH)x zGBK96mhZ2rIoW3ki2!{icwJ=Z7!&=g?VMNM?c!>_{0Ffgk~|GI7r4e?R=6Jluc(g#W*{hlsDAK7IN=Q3F=QiKn&F z>p2kdq=A8?ZLb1f?WcYC03^nQ-g*1etecSLjfasUocd*s4g11C^(Z9xL|8~j69Ryx zOC>OYj}uUetSX(nN=n~)l$619O_u?Z;RkBIzAwx)E6%mcsAOb_K}9)5f}gw$sYxKu z7U@xw0(j2;kCJQkM3C2)w-*fT-|Z6MT6H(*vM#p+`}QAeZEHnsg*S9XaKEv0H+cnt z2)qQlwlZ=(G%%zO4GZgaP2)HGivlw6bdLB2WT4{YCH3!L2CaAO`(^GYlkh?uO zW}mr4w$o6U$;DY33yT8jQm@ffnN#O_9Zn5-v~1?)Iyn&19EJu5S5{UP>1X7Z2iW5@ z)J&}Gn$JMSrg?RFM14EEHY(D!?v>&;#m?kcuU@Svtj1dy&~mMyT=sB{)5kE*SYA6z zTd%#TM&fo}5!=@sEg}q^zY-Ij_oyJx>QA`-0;gAV@{RZhvhs6KQuO(KTDB%_9s975 z5QB2;p8z@45%`#cxcfRzQr4q-EBHWU%wD<*-d>8RW?=7Uddkc_>Xf^TXK$Wu&3My9 z*B9n~s{^Q(VZOwIG_c%z_rSpY?fPTLvti?tstNE&gHx{cr*hxDOMjP^mW_g~AH55w z)2(_*`0?e7OT43j{`b}1bzIbvQs?jA>=KXk?QfEYE1c8c%P{-o1JETJIg z(c2J6!gH_#f9~725_JO`J@Lio*=S48mzpolVl-Z<7Aq(M5PflJaYyP9>vPMnKxv^- zj#pRzI|l^v!07V9=ThIl-!Sp{XJBBkj+!jSIO7ieZA4g$Li`KRxobW)3G-_!$tH}f z_)y~ejGFL!^`pJLN?E4wzJ0p^I#hLf%ff~R{Eg6WG^3n+y_B$XAW=yPjrxay=yIum zJoapu`Bu|JXsNJv99Z)r+Y&+|f?ms1+1+v1SXtZp!m32Jz3ZP&LN^A7$H`Y+`1Bu) z4@d0gz5S~#CHK>MCrkHtp$9=LYyborL|~u27|)aV19V4=qmAJS~`| zp`|TAic_Y#L48fsKE#+tNQH)e6A^#SC?_|NI0 z?0nBImeSp+4DXos-2ANr?$@{vF(g9r(njz)Gt>0lI{|q3_H_5}-yJ<8FNq+}rk~!v z0r7ul^Da4*k1|d+ngryW0g1~h6f82Fn3QDEebw$wIx{Z~-?J+af5mhn$UrnII8u$@ z$!Sl){jHgqm4n3#9LP4ZAcE&j-YqR=WtlE5p||hbSy@@x*x2=Glt3WzA3=S4#<_U= z);rUsY#KT~WyemmLct*o4UPPt*m=cKDu{o~f93!AgoyvYS`Ea_2B|ut{mw@|+e$SN z!tul{Opor)hnl^0Sj8;QuP*9lzHbWL(Yz!&vl@|C=2`AD12YAli2$7 zaUFcmh~{2^Oe#5DeUhO{W?;)j(o7Dt!w~;X$AvDQB}?Ag9XvqJodGPmMC#@U#3RJS z#3aowmkZqAuJ~fY#FAG(2Qu05oiK~@xKJY6O}>0i52mA*l8cGixDBdhh{{ro=?zrq zW?DKrUK2%F`@RJYOw|*YL7O4IG6axr_Uf0tBHES6|M#$$fB_KecVyfw<0mE#z#gy( z7i;MeMTlP3xd;l`SF%(E!)?dV^ebkzpv~4fKDU1BkPGR!3W!5+aRc9E5%Q>kk8*IL z1Hx23q=W9Vw|P(>*^=b@L|^J?4Tc)H%5lYq!MN2R(!VDiL=&Ve>GoQQ1uOnuPqapq z%n)x|+pCTe5?J5NPnB0f(%TC|%4Cpf7(LJD&fHfl`Mp$c!G_9R=kp@36Ku zL@4ws6}FD=Bij6yq&r+g>g1N(rzv`w}d2gj9esStqa)@h~-J*a91m7kDu!tFS)V)}`(R2bfXBTV-YCm~r#faI~BC0lX{> zXWp3|ueV%rcu}}DzGc8(Ctv|c-#_)L_h+(z$EC|wtw5=Cj8W*dF@M7EB7XGez|TI? zM>=KttjAuEOGnYOBj?4LyCjam9@E&6@bLIRLs7%ana7@Ry%OtIXjEwk+Uz*T8DxV( zWns*0-OhxEsRd^VwjMXwRC%S zT&13e6>HKay{MaQ=V(7a*p)(kJAY+gtWi0GH?=*~5xe&=YNfzMhD%erR@wa z;?;YGe*hD!oFcT3WHTi^%?`Tin82?`yfa#2{M66E+Ui6l$HjoEGdG6e`bu}Dci%+2 zrEq6gyxXHCO)`mc^S9AcT5&40gx%jbVlGQSo0@F#eunAo8j$I9Bu2JYRF!osV3?OS zr&a@ip(Qp@yZ4reZ6U2Djdm~fFv7IEUQXL@IHxkoTCcL7coOI=fk8|py!_D(`A7Os z&HuEe1SPuByh*@nF))sKmOqGqd$TIFrNY*5v~oS7VrAn1z?VXH-v zEcoeiBLI_j3d3xaF8B=mCf+ZPoSIN1obT1Ub=~8O!Q652scACEOS~cOvWIC(k;Vhr z#-Uu2t2W93{i}Df6g%FshHrek*}R>WQ&WjuK1DD-Db5iF8pqbr&Wr`|V8@~-LPF<1 z2F2UmWEH8yk(HwaQ%fI8)t;Q*`kSd|?P6{blr)W5CU+0mC_2-`<;wO9^uFtG8gPdP z*$y9}Q)+!pSN~iJaL3l`OH;_cUDQCgijmS^LPN!!af@q=PsS*RN=JR_DN_{0qCf&F z0h&6AM`fq+0hDPCR(590Uf3WhdcWkRKS3O*gzxu(DBsY~_uPlR$#&MoGfO}A%)=IO zlRZSR!XpTnCfv^*l|S5>>7dVFr4aj>##e|#5^7+a{rT?tHL z6|a))CIITjN2dB(CO50m2vG6_eUzXzB>*SrvBlr%_QDkcz!H86Z1n$DWn(1^^Mym-|zuZRXQE8lnPo&z;{;r(`Y)0Qzs ze%AJLHQFoAt!vx{Y$4@d3uZnB8|ir&0&IYqJ9U{2%QoHJ-;0I%)QF~`oE7zRp2LqB z1rI=xw4nq%7$5WrrySxbU|^i|JD>S^d(Eu{ zPQ^gYf{=g38)9K*m9Mf=?~s;L_dND@%4_Rk+fO$#CBl1l4*c8x0r}A1od@9j$I>Ey z3}%f|2&tk9_HmC=gAmc>0Vz}Zg;oR~O2(WgslX=FbLJD>U=wYfmLzgrR#{zn$qfCIeM>AKRu}%x_zwbH8$7xiZkxi#piqC{iK9M z;I>fN;=S9Elxkd{qEe@b(@N?$YI?Z9lQPIkdjwcNTrw-mOorq%H7zZRvIEg`)f~p) zCg4H1%NKJTy)9F+h41bf%7ll0B~vwyI4{cH*vE=Pbw1ZCa#DUi+CS2wWhMgH`x)8Q z0;E)v>h)!4^4n{fQgEW)q!Hr^XGOrFZ;HEb|74P8VG`Ls_j7~d?Db3ZTZd%HS9)29 z2)Szk7yS$6p>Eq<%BMWEidS|QN2ksBqP(%WvvP#FA6#N$tScwOQ|Z4Bn)(lNO&|$! zph1L8t>7n2czcP3$PwUE812F zk7+}Llxem=v&)Q61QQ^+qrou)LI?TrQymbC$g?-T$7%z!7eW z5NEMKx_21;^iB+0rt}Tybx#q%#niKVMBL7Ja-z*+BCtZon$igJL7dHr>~VjR$4T~j zzGMC23B0ST*C#4e7)fz`UW*BIIgsqd-GUHWclyNA_pwtpZyoOaLmjK#uMR2Ft(z#e z6p7d7G}r{9IEAsu($?Q?Veor%J-tdyS+`0$Bz@Lj0m<%n>X6ayz)W^auYme@^Z-RN8$iaJjhb0ihs^1qoT*a>o^OGOQ*M6aYQWNr0qn1*T#PP)E04Fb?iFpoLBwQybFZ^!e%mgo3|DLD=OXviX|3y z>X%M>)9ONmw_T3=O!)3>4bu4vVRnC=(4aQ}(aL_eSI8sN}@3OrPlt zSqkL+@v;S)wb26ozLAk-pk{Wp8QlzjGCdlejnPFY=&h_2O_fyxs5?d4o^_$C^QXho zN2s9RCfjUI5pII-4KCl2&Q*s)_5R^#C&htTAd9m7jN6~Jg+}t#PHRV7Nw=4b`0@}z zCgdRNhO)Ner3$Wq2#RXKxu1(yj{gPi6ycUAvp;CVLAbf(F!>za>#4?4QKek*97l%b ziPQo}fLdN0%9K2rJ_?e1nY+2rMqdIbY_nE{5!ItX8AfTnXV}pJt*%%rbXmcNcM*i; zBjoqJ?vAFod#eJ!fPnZ?HZ!TJPb-uF2s?(8(Y5RTMVA;1M7}j@bQ+Gs@8k>zl9^1w z?Ph0Ula7k!>ZFmW@x_zEPJLx8;1}Zw;guj`n4HSA)9xo+WcENzI3L^?7>M9=mYOAD zJor9`eX%N52?0#I$vuwXaeX=Xg!${zk6t?rdOXn%nuz`Q0Jg&TC3kwE=a`7KDju*k zP=8wVFAOK3zCHzc0c0}))I}|ic)ap-#aYLtdWL>_3?`?nti*hctIo>a)Skn9A*NCC z+KtrxX%k>^#JS+)Sr9x1^^L%6t$tkvJ8XXz4jnw(UeGdu4N6SPsZXa>y7mGB{|c{N ziip$hK1ID?5#*RbN=wB{LaI@%Dh9X=H*av9L2UzAhtn-&^-jTUG2Imq0dpjz;X@Dk zxJFr6P*VAwJ%LlhM_AEjoOVGvE_v$pe2Vs5I6k&$e$$HTAL^9-JvDX5ii*bvlz*v= z84cyqZ5B|vz=^CwwdIvyCmoK>H6}U!)VJ<2q_j!4^($WQdleX{2bX+*1~k-D2$@pv zw4C8^?jj`-sY)NGoWC|V->j`HASD0SFjpI;RxVKU80de@6T- z$OG=>fBP>1PN)Dku^MU{%c=inEb0lI=s7@Pc;=8Axw=2|G8PcRJB_f5w`ByLp5DYU zgm&@Ew|tK1%IRZJA`r8ZGs(20g@(+2+Xp(1$jK^c;JNRt04Cpws*8zJuIf7wz4q6a zfcLJ_HeGRa5Ilt~&j9>Iz&UXM1qDU>;e~*d)Izni2oMzKtFCF=ILcmM=Fn(}5R#W5 zH0h8DY7FPtR*6qd17m5%R6u>mQbg(&$hy1+)dyh4u+`#CI`z6gSFHiW{i$4!i92pw zj#`jizgo$e+&%;NIxdAvMDq5nTY%nnSy`QpO*)pxNqXa=5(6sz>XC6KkuoYg*h!u16CDpeP^kh%62LGhHiB zn8||cJ-B8_6fycQ9P-SWC1e@pi@zYz1=*HQ`fQ-1<9+WK-iC_fX7>)}t^)AoN(*tn z*!r`8**s&Gr=b}ss<=22#m{dStAIz`$@-9Jy8XBl{5^xF;E&pbb>KAuasrK^ol1k_ zJCG8{f2NGweqMDFW2KuqY{6L`4Gp8(96dKNd!L!-tPgaUHw1NVj@MH%tQ#P-*f~hd zWIdSn8iOueMFa$*-t7zu#z#@mQr`vAWM8mg2GKCRLaPt|pk4ctx?#%|r@-f`18Jc7 zceteqDwS$brhgjf|^$CjUr^m8Vn`I>61124ujjZK-bYEqZ-ywDzab-~Ov<}mHI``fFsetE8B+Rx6^ z+-qkc>ob?IkM}8iiSBZ5<8}XUmq6V!_i}u5aCqGfb^q~J;E@Nd)WNI?$(6?u)W8UJ ziD%S}c1`OWa5Qg3X?MZw*QuVdUark&?xq$t`{vW&Od*8OOGc?I-`JC zLCPK)*djdu%GtKRoYQQCZ07Aji$c)Hy%Qxwl)DG$7lA{C!!l(4e zQM1Af?a6W`@c*Ed1|Ea^`)v4MM0oLz{bPQ90l_C~;J$NSexC(eZ}4|5E7h4S0J&ea z$lW&ptIMb6&UHw-tTRC;U|q-S-kB&K0R60&+lM^LdMEV#f742mV9ofi{%&iv+>tXj zm%k^iJ#7n4N%`NeU&>id<1AML8jG&-egdRV=wL@A6IfS5Oj1%(Vp;7{j*~8!y0w-^ zr!JhqRElyXGroTaGN?x+w+vdCvb61`fSrG1Vf8yfe%t4))6?1KDKIkTqSu#_XgEayIpZC~Yozjn@=mJ9lZUM}EaCfN_HENne5CoS0=5Jka3l$d^ z1yx|hyhr0Z5)nm7(=98dei7waThYybquHJhVKx}KWi_5a6gL}OcGO;Kx)#&*-Bq|7 z^RX~S3z%Zub~4gpbM&_9`np`|+{Vv-jr#P`Xhz|Xe)GeC4;#SCproJap<$JnFwQn zh);l&1wGVqz59mEhxYdEB`}+%G`fmAs>D09?E83~rPTWxNHuyN5GeYCY9*7IIZYZ5 zo_VRLSA~8J({>g7{&(7g-U6w@sj;a?%*VBNP-$!N}qS<}|zqYE(ube9&D|6+{=l}UWAMsek$b9smDI8G_%Q$C`e6r7BfHrGrwH5-jxII|)TZdI#y!q)G1`MOr}6ARxU4 zq(dlyge3RG-|o%L&hG5Z-0t4Z+~zMPz_&d8`LxGn|G-Cpg-s6a=~7u{T(2DVrHOKN zAg7f+K7k97mP&9ftST;l#a!M3llD*ZzkC&PzozjVBP2jq8PVo4qK4=|ZmydNQ|zyp zIM(S=b;8bs9WIC)#r!4*o=9&-`nUmn_T;ClQ6tykyCb*3d|||-hAK!+PrZJf5y0rh zWpQy~7(OGT_dqB(KS%2J_Hn;bi%UP4FbmB!!a)TsKNA(bVlbo8kwxPd6?rMsUCYFy zvZlVey69=ud)VJ9Ims(43zv!KepK!jx%I%e!!4D)zTyt~`NJGJ!;@5$gL7UkCOy}- zOdcyz?ykAvcy-RaJ-w5wUB{6ng$@UMw#z;cLt<<;`v-6E9YH9pVM^{b2Q-Xb~ z+`*owbFC<{TbD*sDZ5w8&+jaVUIMt6%jux|+V6EcOrP*>a$&=1yYp%e%nnBQyw1|? zhniNC!BBXRO9l7O>R|w zm!JOV)ijL?ivph2BEuV1SB(K<8vFg74ufOb`?RZw^d&?T&SqF?$oLy-J3}mS<;dxX zrq6kyN0#eG&V8t7f0ybjSiSIj$q!;$r;_f>Q^Z0b8euUpG%jF^7UjFl{*SJOcO32yOa>Sow z>9~7{;U97qE^B+}3m0hK^FpX@4ZJ}P7MiABB++!@Bw#&_gKOelp8!9Zlah*jQMY*s?QUSLWp86c<H)~1S%(duJ`GZ!Fj zt*qE{41$F9Od*h~|1>XicwOKG^Ys7M6SP?%$eQ@o=oJ_HfwK<|fyxFy43Epn0jN<` zRUFWE#9ls2JrTD*)7-p6a5{0AQ3*$fZ{HVyr3_MR~6EUOu~>g^hwZ zK>YJVz%SmtfB&oKHwa-!R}!fVqqt7`9x$jf4Shw|c!_+FSzX!yuJfP~K!_Ga5HHp{ zcbxN0=c#z+bR34rArRin0fDQ3145XNB@g1Y)Ni2IUP)S<(VlxTfA8VrNBodkTECZI zIuZdZuNr`pHJP(|Mf5QwQ|5p#wMk#TdgZk#x|gOCUr+eWPc&;<*u6A-4Q&!OYyD1Tqof z=dGb}Z;BEEp#_hp-oLzLh7VTs)gs{fx~FVxsd=WJ2g_AF5sN|3?fEb_uI+Xsy7%?@ zvLr`Y>seZU`Xc6#w;L5lSOsnPEAGGDhqOw`P%=Uwzn!(2`x4cmkQq!Ns#n;TdIw7= z*)!Exo8r4ERX_9Yua|K|RG!t2%t3mk*G7(O19JT(K5+j?BH4SIWqeDz^i+v-t?4#U zmJm($yYS6EKu`WtcHpc}BmOuksL9x;wrHf$hGNZr z-L!=;4RcY=*0Ly@g?En>x`eeVN{b05PU*3ntSB-Nrk&AW%0de$eQF(aPx2gI^!HtC^sc2inw#3rBZ>@)+9_l+rVihge zxaFg`4##O%bXsOq_iqQb=wtu9^x$vV@*YVsWLa-Du&;XqMJ01zFRmL1Nz`_=(B>$9 z$uqm2J}RImU`l;m&&x~~hbe)N<{l+?G5D%uMKir2P;py+SI{m3F5Zp}iM&L`)hKsB zxh{*^Nif(}VHGmEJKpP#X!ajH#J{JTr(TXKCU&Lh_ciORCYL!4;kh0? z)#*k|H3#GY94eMn(GYh*XO0Yezjy{a7(-5)l+Me7Gee= z4>qD-{>z;&rBtyy*Zcg?Gk@Zbgn+`ZeKxu)IMi{0EhekXwD^7gTvChHVaBP!BBa?| z26nO?6ctK|^4O!ic)i1GgCHvD(jxs)o(8m(&W+Eqo%i(_cONai`~9AZe0U%xU%%gX zuHJgy%rSEzVCZP9ln2IjxfxoJYpR8H?JgfNzlhS7x6v;9S>(W`+*NsxDcMfeXLmEo zVka^62;<*SAm*x$cjFh>$0Xmn;s_OdNM)3LR4;~tqE?we8I7s^x>jq}na&rwOiFsyMsC!rgqhnOZE_J2a#aBS20N$jNUWt zxu_2LxK~^~d8W(~B*a8OtSBTvlH$y@J`rsos@*xT5LX*!{H1SLV77iFK(ERtA$ljd z+Q!c>=-jj|hLwluXgR6HrYqGJ!!=Dmyjo8D{k5QFRE;k7?NW*i%CpMv-sUGkwC`P$ zqXUYF$a)dUsCVH_UIWsF5&lx+hH6og=Dk$7D;4nVQ`omw)@bD4*Xg8Tdt-v3!qEhD zm-b&S$|-nDhXebW`xeLZP3a=5>rV0>Zsc}h1q}$1tSR|Z%5AYVD;#V4sjN>@JiD6 z$rQ7u2eQg&-*B$h+6hlsIx}KmvUjjHS^^!`ch||}G9aI-kT2fAb1GvGXsAa$=e%}< zP8|%}bdgzerE$NSH8EzNQt|RQbe?DKtHoAUa>D7i!j+HQHuBn*gx9($WcM;VvTMq5 zZ_L5^x`y1pd`Qm_nFbaEka_n-FjjpuTaoSbLD%^laQ>Q@?fH85#J@7`#~mzZ@uc%9 zB4sI=+Gc!tBia3I*4ul(o~^S<9xk0~>`O31rT0qFkM_^KeIvcT-K5bYe2a5WTXp=7gp!xK=j8>YW~-@OYDdl%_ZTZ34>VETeJ zK;Sb;_81TjpzTjQ6#D*huB!b=N0H*k#_n&d{S#9UN`Gf*92)nF0<3r`u!imUyoV!n zY7${L(_Rz5p03NETFXW$dgY}5^lrB2pW*2fP@NH8C$D@wak)ETWc2a0@AH_doQSA@ zbZn{~2tl$qBHDe4x8Ij!rH**Vmoas?>L`cLhkAp#3*u!9*2%HF!|oJf0g@L1(hh;p zfmJ>D{AYM*>qu5d=ZdI~4hkhh;kSW(liNt z6~2dKCLA0bYLudNYCsk&kd|3iR}~kBW^5ZAN?xx7u9p-L0hRki?#fb%tbMcDp+32< zv@{Udii?P&?1TYIXa*!zH$ycud>il$=#l)H26rQj-Sjq7ojYbh2fY?*Le3)R{lMGW z)^l@5$|?x0a9-dYOn_)FPRJH6K< z4$8>2vQoC>Pz~~jCLWveLeh$UeywPqe>@#9t}+rvc09M`b)lLd7OOHFHs`(TC54wk z`-%W%!J9YdK)tb-l2&{oK!@b#By*>126I%l3k{utYl1|}Y&7>h7woHS&1??`d^ekmPYd47`hd^H#}&9wUG(`nQQQ^33c z9GW1Cs?mP#&LdCnn#e-+1dm4>b!+0AJiK*WBwMs17ZVJIo9zl*f+w91`;BzoXwMUD zrs6eT9U1nCx$Cl?JGrVcOpfdVp;o00zqw};qB&7YBKsgqQUK$C(~fGtcF!JWrt5~K zEa0SfH$YrkDVUj#-tTAvO&*n5`HQiaNI9is-ENN;G+< z`6G~O)jSJGkR~s8gu6TbsBP$M4}J0YM{ArTU^hSRKx?`UT4FNv(*ew2#s}OQ)amGS zv&tn)VGtGNqoY@07=oLP+3`9lD#OKOZ|r&R15E>vG}@$`?HwK~gWk!cOG^9{HH?cE z*ws{^9u*@vHTHky~hu&qQe!HN2SGSi#x?~fAe3JXpXyVg4`??%Q z%LuUA!j6AwflZ9pQ?MyIJOSw#kLyYYuS)`cr4rweHyS;smDWOBo=BBHB00iy;$CJi z$d}FpD|`XEIeROsyPl`>eCFZ)_^w1ijc~C{eUpY}A2E*|*3is>_)SBeVUzmRzT%13 z=Z*KO&y(DbsHa&*8T#O zAwSkH^9kpln^`sr%v8!N>XxWJRp65OuI00mtJfw~&n#Kgd%`dI7Jtw(8N)dWNa{<3 zX8ggAIr}M3ztztPISUK@W34+0of2(tjozmUW}y{JEvUesPoXthMq^INf^!qhlv7pJ z?d%#r9IKuF|r~j=Lg7yWx$hwS)F)nmTe{wGn8fA&AM>~Xu4&st%i3x96FZf`w2@H z+)N&QZ|?OD8OKuqgsY_Ha)nxr^%mGhqaxb#TpI5Qr^}UqTvCwy!X2ES!G*u5?0-*H8wYr# zffCotr36+*Y53%)UIxLRLXtajML?hlU>JRMkl%9yMF+B2zb_-#-f zIQ4(NYc~M~5FL|S)&%QW(-9@u{?nyNYCdzQpc7_KD&6g8Y+=q>;oDH^_9pq^?1brPrLR+hU_TYE(W4C-}tq9MJJ|R96 zpg|O1Qu9d9$EkN~cbAA0T{hZZm!*vYk_(c>cIKLwzv|VYLW!c#V{NvCRx?1e?M-HK zmDbNT5qFQ92DC3HsTY0x@M_rMM)DY78E|r?g(t^YNo;G(&(G&ER)uWdQ#4HOjJJ4_u%4qJIoSPXjR=@Q5M?ijCS9=fi3M z=gp}#R%{GyR&F|+`Y}?lFdycsP0)Zp;$lh4pUVUtu+2wjy$4L`{O>WuYzvFbLMdNs zd(^&x1Km)H5s*IU{vaEoK3c7q)jhi#*Z5Zr2{;w*PA>qIc{K4JHyeuP0h=e-gAd+Istv1U4PAczi#GZMzG znbB%Ll=VEZz%$Z~3Rw!92phFlqGb@tw*=^Z2F`Rm;V;z=kBo_t6c<;q%~7T2W%pOr z(B3NlJ9~NVs!1+(_0<7@idt-6;Luf=kD5}G=`~FhqwC^y_7p4t^E)r7J_bk z&-hAdCUyN-!!~~3BfEvhFL8Bw*@|bwet(+-DhCsH3$e20j{y1d@SoeHP0&&{r9U}pj`ZEPE!0sV?T z`lZiGpc~MxkEe3ZzsFTq68WKCy`QgODUV|^n(|v~&2W(qu|O~CVZR>;VANd&KvfaB zY}F{GnKnu#WeX;7RmA0{u!`7kmDI(vyAwMD5=n@w8`JPlSmt?*l%J&2X@xekFYVu8 zHI@>QXM*Lh#yw3~i_lj-2S}&Lp_ znyG56ZYhQduo^9&}B_0M5s zJh0lICj}$Kj|X>D=D*&M9bsz)X)^6w$d6MW!bH|~c!o2@@-4g4k`h;6np?N~t-NQ- z!l;Q8Zh9WZV3=jaUz^FNmb%feufFsTUml*B-G$gO9SaXvys=~{k(9$t*!x78CY+|h1vY3e1Mad(%5 z6xQU%N9U_oDVV~J9?XojnH3y@ECL>lje=$dGYegng1V%FT^uHoZO6+y#Twa;2sHt% zCRvju%dWQ+WISMGrTwI_3>4X_+{*)~cAKGR9Ex3Rg~F}wn9pxMrwX$0a6CGc1BZu< zxISAATuS1^sV_*C0Ub$|9rgk~p_w$drF^EG-al{k#OfD+>(&!Q4p>Fso5>jQNp@TT z;zZzQsjI0%oU+r7PG_)yR%*TqG9Yt3ol~;-I$v`G zPSPle09?h3pTkqne{64JeX|?6sj>>m^w<0VXn2A*$Eu0G*zC?ZKyPpjO=yJAnI}~8e z0a1vctg1(({dErbr=@|Eg||WsvSePngUS2aeZ#atR74ExyEFuj17R`3_y)?bUW8V- z8aTivU4I=1n(9?NSw}}n%by$ln2h4GbhD zRFM}IEeQem-|$~wqJuoQ>|ZYf0TW=X2QS46UdmiSp(_-BfEx<#EjWprO8~Lu=rj-; z1)2W_un!XJ;Tr%p{2J=CI|6t8_Y4y}=H%qsCi>ousCU96NwheGE_CI-e#unx_%=r& z@Jw>985pXG*-3*a2@+`0$pj+M%(yx_Km0EG4K@?%V3})3OuJLEU${f!gCMgInZFqO ze4>x)&BGZI-&S{1x&k&345DxUP5Dcs^kpwDG-~jrot+yNc?iO`Y)i%fS*E2`qy9%k-!F?O(UE zFs;B-%b(1veH*R`_6Wm@pj`e~+CZTd7pu%LqWtl}xvz6wn_I3iGIH7{v2q@$x*IUs z81VCSIL;lG>(sQ0W!&`~?f*4L-e&)nj!=)W*###XIFRJ%lr#U%tF-(+lQO{p4io zqp>xuo7P;{0as0`=-oMGpO>!4vE$=60U!SMmFD-)#=dCvAFi_ZjplD=;K0{;7%cfD zX+(-PCicgYglSn|NeQZiBaxlzrRAbH757WfGA%clohs(Sp1eNKGX zJ4gF>{BA* z3&YG1$gzAhxZy-X;JFt%gh2pO5|{cwsmP|v4<{kP|55@KL4Y0b%)V)X|AROssqJ3b znZ0_I4+`a!#GJ>LDQ~450D@DSeEu_-7te4hbY< zZ}}r8Cxs@F47HE|G3Og%v6)~EZx<{N%rj=PGa=tk2|$UNObj6kf$ z|1tz+ zIdraRL$ECRNCNL$UvL#9VMx>vkDEecPA{%})60DV1@VWdZ!hf4%OQr3Aa6gm)k8Kn z81DKLN_KaZNwyC|abDAs%1Qk9y?t8=w%E6-sPH=bAIjk6C*GZ#_l3&Y)P%OURspv} zQ_C%)IN9ym$Z_^F6&V%q?~x~^TzC+(;fCFm^~cif=n5KDSa@6C_1$|53b$H#M~UD| zooU#z)j4Y93AQG55F#)d?Apju_(}QxNUPkvP>lDm+lLmv`YV5_-*p^zjfhLrz!fyT z%;ZB$3kl82vjbr>WWrF{>Q}$3fq}yWIDsuG_Pf(xof}{o1kEYD@v?@Yj0(ocO^U6> z9^l5)GC{61@TMSvF_KRn$$sOxx1bp^=Or<1Y@{%!|063iEn`88$-QEbahLA7-QC?y zimVYuHcs4@;6B2Z`Pw#Y&AIrzLSA0Xz2o*RQe`8WtI!#~`3+r`{x#^DGb3 z?c2v|O%dZiM@FH*Op7B2Qt5fFqVY!+Q?Ok?%v4onygCoP2TDj)Im~_Ki#QOF6I437 zuZ--h?T-_U+Y}k?%F28^@n0u?Xxfy(%O-Iv=+56(`=pDz_Y!Jd%+dS)c2 zA#n1z*aTR)uQ6S}Cq2o-^!;1+S^^E@)YOD>&1xIi!w^6R^|D4A9DkkQ=a<-7XttWy zTPiTSKb8=_y%}Qbk0*1-;3f1%b|xYulV#O9WLs*ToU2m~z^SBfdtqm|dwRfGa0RVG zM;`SS78Wu7*;o5g)C}2h=Np@(n80p+nITrvfZBusZor_VsMwWYDg6BTGiAeMAICOB zy~wC246D>0w9Ak@Is;8}0qwo%xw;+fxiKy#Dhle;b-FJvgMG;A?1a{!{@h6w-+B>G4KUW{ga4s zwqu#_7`0s|PhQiHjaZSMT8kce_FY8XlonYK%cgw+E=-g`-o{W1|9d@38p9?NU)*?4 z8sQ}%;Pr+Z!hY5zd}4Z9KtzO3RJL^d(SX+CelF$8K+;kN94Z!zzZS)OPy0+QA9hqG z*cpYLk=*twUTbjM3qqnc3)11ACd+@9`)yC4t{AL-J;34En0BVs{AipuR;Vs6A3lNR zeVG3{tggwmS#2M*B{XX9_U+q#jI_z3p{3;mqcTmdckfNk9irQbol4a{VXs0q?d@N^ zdR;NIE1=KWkzSLKf_ZIV*~>?#`uzFxhe}F2_q@J*o-J1|zDfIDE4$<7^{KHQzO}37 z%f2*OeJPTbC&=zCJNu!G4!&SDgvMmdn_(~{=qJ;gt1(2qdl&D<^7mEUDsarubJHg0 zUZ8mD{|r^wSy-~n%gr4~65^&?V`|t2;`ZhiF4mjOcYO>mj_dI!C(jOGSFB)j*KgC)>u#dp`UWpQM9SO^)wyzW70J6Mvt+#cgY-cz?Vg2ZeJVJ@C~;?jY>r zr#bnLRO4ZebN;uoVQAP40u|0%k&<3Ia-tR{3=Mx52}+*t5DLtXqN^3Rnk}6}-acw< zA-Ik`|6(?wJ#4WvVU%ERUj`1eWns1}P%q)scWC4KWguXnke129Vs|NZx;t4{#sz{vyu zo?mX*mo&NaInNT2D8Svl5F1;mV2#)ziwrw z#=Wsidgd*}^;!QT6YFK(CW9ZcpO?{fMs>EjEM>r$*KK5$4vX?tzBMsppu@ zgD{_*9e&jIy4y`YzGG|U7f;@0M%9@;;tgE?;`E-FaQ~iYS;d?xi+ESnU+4b9Vab(Q z^z4&XcHik{S=n3I+O|^*D-SumOxvjIqI;07gW+b!kPb~)A^V1N4Y z$ntSTQ){cS%I4Ff)}0!+(IaA^DFeAm$9`79oacr?nNeP=n9S~8W{a2qL!hWvH}+F# z&3TpI`ig#K#iL##UYAR&xk*ZmfwxFX>etB$xiZAV(-l@;)!oyh@=V;@LpJH5M%Pt% zDHrFCv=?%A_Hn!i!LNz)KA$k>V^3{kc|q%2Pd|My92eBdeHYpi0Y-iXA=3^w)Z;S7 zB+e<2^x|Lfbjv6Uli%`n1%XmS^(S$BOAXUH#yt8)to{0x3(A-MzCSxNJvMA|@)>~a z%oL1`GctOpfZW!m;;NTBgOQ4&nOnFdH8&UBDx40XbIVcaaF{tj-~kZyA=}7fmaZr! zKG2*A=09SV;wVd|w`h~9i#=dsNq7%)^p=Z_SsyR?)#yO?2qX6KUIgt(MOc+{i@ru= z6m*2`9xs_Z#f6I(d<^T&p4LUAMS$ ze#?CYc?dLC7U&z#d;lUwxjNjXSQ%gt2o^@q*K)Q^j;KAx*83*|It$S)Hr??K< z%#O|M1EUthCJfW)6F03FS#?e(UJvu*;ue5t>6S5j3c6W&VSHTpwwRc4$p@@Z46A&c zE3!P*i=!&4PS5GdJ8aJvnR_$%Q?hL@AC<|-@bFpq^3Q(Pl!QY^fQ;RM6Z7)8GH07o zj@|fCA$RZnQu`q5P6&6<-CGrc&b6;rKGaX$1`8Z>R!*AGHO=z$R4%Ao8?LizWVNEt zPn8DPqkcwPe^Z~8x!3jOW^}gP4!#ewekcv4@%y=5?G7eR=k-n0lNs}0NdK9SGwZ(j ztS4ADX?G}hnx-glLIHGIG#eJB5g9>`z=1TM_0b|_V4JV9Blmyn1j=QR=jkyG-^|Q-k)12Y#gVY+Vu_i zffV|&!`VEA!yFxlMn~5euYs<###VjUYF0K@{&elv8YNTxc7m7nYy0N~3f8>8RIAIL zm45}Zsu?S%JFo`v8{dvO5N0u#8@HR}l2sdRFf0&R8;q<=dIVZpe-oB6#ZhW8qxA{br?Sa=}* zKak9j9>9ra#Q`5;@5l?szjbVer|}d{z3%8#5l!L3gQI$k4r=J5xGGhgk5lFhLf*Vx zL+*qE%vX0-0R#qMPX+Q3Hdbc0)DrcHPs1dox!$j34WGs@Uk!f3-|SPJ%TEK8!Dk^V z4f=Ry

T_$;w5e^|_++@jgklSzO`rGwnC(G+5sim1O0QE65w3QW$P!i`$Dc38@FuTcndDytp5Lr3kTIGU(#u1zx0Xs`d<6(pQHQNcj4ME)sH@ zZM>BMysov8kV_iAkIpZoJh0n|GxTE#E#Q%&9sLs=S7 zAbbS-%}o$?)749GoE#kjH@|-7TG~-KS>-x79(rJW>#1pS9#|~QN~7k=Orsk5$RM#> zvPU#dwKWfI3MBc1gh{ab?mT6{7KPaykF55gE`{;4=)80;>f99uBTdyiWT0pKM+K0lCBf v?j!umKUjc+#~>0FN@z(Yob#l6Bu3`hE_oC8!3q2sB zbclxDLNB3(nm|Ix9PZ5f?f-H9GZQ|X^9v^@`|Q2;TF-i(wetR{z83qrE9XEU5WDu1 zhlU`~8Q}Ef@4rq0hkHUgCqSV8fV3apH}+3mn>-V8ac`=9t8RRC<3@zXY2oY#5AR(( zb^qd-UzhHbv9a;+pJID>F)n^%BjVfY-HqCf4OCOZP5K1$$_9rrnw-$CqN<9Ot8Caw zBL5Qy|3ojN|o-UG25A8)+^oj*Q4J$d5t@iFuC z$=k=rduLDE9Unuu{`!7=1d04_yTNVqfPsF{_n&sRecbyeI{#?MT-PqV>MRzP!@hE9 zo;Sd_>G;Q_bIe3rR=reRy=$oLF~y*ha6p?}!Ws^Z z{1CWR@)K_<5?;$#Tx#bSPT8|y99ZF9nAjgiCW%$C;<2$MCS$=hQ-?jv4G0(8Gob~a zR?KriB?^%djf`+lPKNBk;4D}h5C@HA=qT^6VLszBVvdievTX%c--kcU#Yt5$7v#%zU z?kg?d#*wmw7DBzO+toPO%N0f>hWU}svej}K zrL}Z&Z&Co;`Mc{`^Y~!nl<36_#N=fqNoq*tO^(qaTP$GMj2*{^$(HWK3y5&*d&s zI4FLy+<*H?-K~A`Hg@epC%ql?3K-=p)c5~tG_E_dJ1dO1>hUHm$H5A z)2q1zlmk2%l;8@{#=TMeb``9(elf}V4WxIfedn+`fbhG}+bTetXuma>|5GWunJr%$ z2^;nr6oPh1(GFtFdL37+P{>hP{M{(6p~IuD7|tisW^w`r5+V~1Bry=Rij~cRr05ka z)22)IbEVZHdbf*PQE^B_-+TtUTJj1P-@7m6H5I+k94v#FQ1k9e9@NQqKkV`vmT9lOL+yxtA_9dr_^ewhMk(kF5x==HKXcY< zV9ud-M*qm3klm6+EU0(=qX2t4tTs-SN%LDO#_7XpbR2KHYc}R_6-2U3>YXn}k2KbV ztz)d>)o#gMcV!ZO^@}OFT{Ndi6>Q^^H_pXp>Nm9uJL`n{91P^C_O1)nJ5L`iGjLjt&+7cwfK+~Kw4SiLx%KkFP-aG%NyVb<-)q#l+ z7>O`I!pgANC`!eQB)^iR;#vo|11vi)CU|+Q@fl9Utp@9EjTssycU=17r1IYF4>-fW zRvBXBIsEDyK7x-z*}t>+C7eIlputqnAid$*_z(s|PZu8iaq6l@R)%{3YnmE$i;B7E zQA%B1b^-TtAvA(Wz?gb~k{UHG9NTRleMV=Q5S{~sGNI~wBuk-6Z zQfyoq-h#>oSFBz6{2t%Y6Mz0-l;2<#Tufthd#qbwnDp0YBo)K!-9p^gNv*)WLg`_| ziWhSc3;cd+KGg3PMxrZ|2FzWBQ4Nl>A18vBYu!8f&Od7QM+&IrD6O%n)Oog}VV{K+ zw+_N8y}d3Kj}mIE=)d|kV3*AyV>O6Lgk6*P}jAb2tyNOWZe7LFb}J?FW*G4+lH0u7K=E{B>RmI+=$9c=(`~=QMs;EN#ib>@MMaZJyrpH} z;uK_v)LWTQ^fBB(s!1hoKS#d*oH(1y_Nsx_<8~gGGZhziKTp-7)81(6MVDjt*AZfP zTYu^st+t5Uf+8ZLdmn4sY(9NpY^F>RWMv&1tnrGA*-|?P9i5fZi%JiR_&&`aAjd;$0=8&*~d=|-h7<>7LrP5kSg&beHCh6=A`oAb}clS~z7 zi+&d0gd-IT=&Nj8k>b*Wn>lz}38P!6pxtZ6W5=6mEKd4RKirFXoT>NTpFaK~t zlS6P!uU z^JC@Cp0Mr~M8gOfABWD@)s3f;i5YYT>#0EBqu>?^SiZ>>EyEMPcdacQx=FR9e0R2z zvBriqB0iOq|MtomZJl4xbCDm`)erdsv=x6cX-=~bT3H0|21>}Q(MsB(39@s)Ixdf9 zCtR-5S-~&b3UfYTSX@EVXZ6nNa~%wMOcpi^`wEK-Jb?wy*CMyKb#OTlRU_PPE5gZr zoL@NKwz?6|B)1fDp|j%mClNc)!)dDk34YtOF?n#bO!`&d+{j!y&H#t??={VLgD3B# z#X?1csSVh;`?8(1b-IX-wllMc81%>IcZK_2*X@jy{XNdgqo`c*eAt{&li7!`?E-gO zAvHhC3{Jy9+798;mn8QKU5cr(BwL36O8jJ)!z!gtYa@g?V7;TP`)x-n?V8K2Mcdi&smY>bvvZ~ua0zxo4wkh0<42(p z451^(lPnsvKeiFtE-VdMe*$LL4$!$YRD?0b9CWXVH}}O{u4681!60qF8!lCRvQ%<=d0nl*DvV!#E@=k7YliJ!azP;b1tQ-(Y8%+1Dp##2 zjq>cx6+JT0^si~0@;lXjRnyznDc1NUTFwEF4k-4Ex9FkW2bEMT%*IJqD76P?2EaWEZlkL`AY6=sx*p2`@W$m zsWhio#`99=TJitfFW$}D%DDAK4?~YZv-8ektN}hq&y^s$`Pr|L=bP6XIBnPd)P)z7 z^P~=nl(c2zS{PdTI^DneH3OFF?o^n$(VK2I_t}^R6@0KNdtv_`Quz?rHWG3RGtcgb z$aHzNFRU&+8$7KyYc^WR8mP9}ZG-@b+T z-92d{=aguZu-Mw?)!cw^AAG2IE*pa5?Sxs3DhuegCh)nWOLTgj49*&{%@r46Rtm7Po$t?rchqG?(fBXi!5*$l%XZMV zpj*f{v2i*p#ERT|Z|z~QgT39lj4pSi*V@<@Y-1Xn{<^9gx&PCG5#$v>CQNL-UG%F~ zoK(58G?RiqLS3tGBIwA+d3@NFSy072AFYRTB{}@+zZuixr_MOWOcmZr6YhSTFYVHFGpjz`?YB9?7d$zX5KDxdarS**=m|qS zJz4f}HTb?*M&PClY^TQx>cV3*Mv!_26Z(2X2H6F%j$Md=FgtH~CdE_2g;iM{Jv)@r zPum@|^-Gj>bB4IyJ$)sln)=(bw^pv{*-SiKcPVh4UE=u^ChFT8ZwR=}2|7lWnVetZ z?GUYXD&XpH`dw+iQC?LRi`#o76zi_mkli%*qedY^+A|xK zO6eMv0TBmMrfD%Sd$Xfp$xHv`0t)Uqo_|;$C4R>slvo~`-~NgdcNaZtR?(~EG%_9T z-NabL@%FTS(tGj&@+4k!^aY)S|78fOQNS&%rKbMaxi1+r$J9@EKj^H^AsRYms>MBD z20QXfxD}-yyqW*tSDc)B_N?Sp&+M$LUUya~N0W|oR*D8oGUK1)(rhTFpM?7yeF&FZ zDs9{;A9h3qPb{N^YmBLek{Wrk#MlmSEWGDt9DH_>@mT-pd3@zJhNJ-z~?esE0*# z`!V&Ds@H~^_VT^I7?#F7*LfX%yUt^rDB18zZ^rjQvSUSYadDQEl);{f@sR^hTyq6# zn19!xgwkAztHQst7ta9WaHlT^uK^IiVU7M;KlaYYg2PHbM@X5w2*t3)!e-XhdW?Sn zimf{=tVpPqXOqJeCQz;y%*Q6w-s%=G0vZ_Ac7&>Uhf}12X&wQ@azOjCVY}#*eaVF>i@8djmELx2e+wvNB9)0ld=aDFssD@A7AvP+Pb$-R^nFfduurMRs3 zKq%qHr;83m)XatA7eR=tvdnhQF!qg`XTpCtOYuG%#{++JghuXV!<`!cb*2XjBh)o=nL)bZGSFx$p;S>t~*~%AiXmU)`<1D{i@Hj z9KQML;I)C0JYs&{r5Rr(HoN&=PS(#tWn2%NQ<6{*BV?BNFjprQ43bOEKie-bOD3&t zm}LaoRgs*OD`j{dIG}32Ka@-h$6|5C#kfXix78IZ*K9^&93w|(esR|4LFMBN-LyT* zQgg@bICgnS>+$)h@VP&&EEyaea3z;dYZ}Xgb?ZR7l!q(@FK{Ty_bWw-sc9Ih+=lAZ z?ElkAp^bk2NcaetmAf@mrGK>)e78*l*X zGB@=Y%eILc${Ekr3DA@1RFZe|*N`#tC>6Pk zbTQ(9!PrFbrukHf($QY;L-F&JwFy(dO|zG*sTZ;6#9hJ+w{!M~Tl}tshN@5R6g@5w z1Khy*YMOh~Hav~J?xubb?u{C1&k01w{-*T1=<)Pc-XGs|*0w_v1o=Jby%piN#LJz< z=A!(?`rcmLuv;I(RdmwFjJ{-zCRY^I%}1Em=_8PB{&bF>E@ z8=@CP=3ivF_ac&aisxhAL-mnJUkY_AwzH$7Cwk(>F9Lq*>XMx^Z$hH{)`6BoOwFTa zx`_W1wa*92z61y`e;S_5r{0X}G614<&e`8YBy30M61VqMS@v+GZe$y)facCx;&a0j z>B^WlKDRNya)6|7yL;ZOnE^+qEQYHjMcf|H)hd8^xeZs8JIv91Po*DDNw)OSoUx~; zmD<#WQ;q@`6LwK{2c?Frt3mDGeu+J<&>FuKZj&lN_k)ucW+b?8KAKxGcO4y7mK9qF z@8E~ldU;2oyU|7>^+1@O!rZ;jq7d9?7;FWF?#gae-1Q)hii?RYwI&ED9osr7qe&D} z7oSmc+CJ$3dU%NBfn2=duF2hdB8s%ryS=AK89QiU`BlElYbhVVEDGM}FRZwBaqxd!{p`Ky)6(KHt?cr!gE+E6;skN$PX8>IT%VHEUwi|Grb;QK;H*JFpMg*1V5pmo$(1r{&UFxBz%_l z-#z_)fja|bUi~49=G{JG)=yyO-mSD}Mm@vh5>gVx#+^AQ<(?}3ae&y2L0VSI&BW+| zs2=)EsE}r$wj$}RI;_z%%J?&jy8Mjs%jH;PMoh77K3JO?xaB+&gYcL_K9mp?YT6x{ z7jnvWjD32>vKmb%_>RAwm|y_#BPy2~i9rI~X2l6<`-EIh+n_-&%L@^0 zy>TwzM#ajO!s|X3a1SF985WTi!I8c(;%Q5hLh57Sa-HU(XF z4rW9Y9cUSxnbJJMaiK8OZj!y$gp3LG_vSKPrnvJ_EQRlnO!0T>lxKGjcq&?*EwWkB zLM5Ien|DT1DIDWJ0%WNh{d;Xnnske1n(_O}ykreGv$E4{blA{W8`enNR!^wC)_L)7 zl^^gcvpT5Iij;dR^WY|CxcgzSLU2TnQ0Mm(O|}dAgR3!^TTSv1x+!@5fpL;J82-%E z!8%*|*yB4`1yG`~gfiIh{hhSI>@oMUDyccpRnRcUfmci>eY}x}s!1<7xXUZv+0mc0 zV-_K(_@tTh7f)pwe8B2d2*kk*=WY|{pPCpRjupPKz8z3o_hj_T69|H{=2KD8IYDm! zGY+omlh$2*oE5=~b_grVpGes)ZE@Hyu*&-CLX?ydv)N3Jpc|)@6c%0nu_|FdAw9{0 za>AeDSH1lRp>hA2grZ@Cq{gazfAA;nXu}UxRTZ)w#+{2!Ti9Wo57Ra`*mf%2$s-%`s1g+Me{zn{YiNZ%|HB)%t;xLg)&Pn`IU&GZN%pF*-H?ymQ6LE{95 zzPCmkrx6L2{fq~Qq)H#g18`902}g;mAqtr}u;GgL#$j(cNePLm zfvq8|rS7@XA*=jhuR-7U++P9#7mHv3OF2h8GCbAA&7a|;IR9dF_z7!?1@Iq*GJ^!b zoFi?EzSN24^_xHa#U@Z{pjA1$4NbMRt<5-upo}S}YyOI|0hf`qWbd5E_n-OD<*w+Yf-T4WjFJrG0P(5U4=^{{5C58&)&;7|)dpa5wm1?fo=>^O_uJit`JPBCRQHj~Xq>_|$$G z?Mi~WCY1hrFH0mF9y^)(8S6I6|MTeQ{K5kN`GnwolZ7x~RR28EaeZsbelB-Pq#3=kx&S=X(?_zh zm2Wk7hA6oEG^5ZKJGkTL*gQ@6P#Lj;FQZOH)phuj06jv=rkPPNebn69VMp z;cKPUr?B0;sLgX=Srh3K`{4!{n&;5pusr_hh8K&xfZg01yt1`x*(&Z?W11tn)>AYc}z&OBEm9K2MmQoK~VdwVM@ zWPwSYAUe8AOH2P_s(eP&5zhw#$w0z6zrX1(9YB}nhXRH%{Aw~Rf-`XW4D7SJcpOo& zmp=un5;Utjbzv^6rF(LHdCX$*Q@t;F=8dEhd^C77$lQUUa-@VL?{>vd8}2f!ZlI^@ zsD45!qU`NMJv3D3*g?0_nu<@6;~ecKxz@1<7s?h%n|QN1#al8_y|me4pX*5V2vh8x zx}sEne-gwPOk<5u-~WLk+}@5j!l~5I?YcfaXg9zg9fhK8#+68Ne~&dNt8Un;^{UD9 z4b$K#k(&0OVnghkbvr{u1C5ut4TawC$@TmH(a&IGOgBtLq^XD@x>|d)0euP|A?W8# zjaameHY8~5FAm$VMx<#7x!S`gYi-jSSoZ>h=AWv!B92|1M>oZ^KB2U( z(0pLjIjzz}*1h3b>}vJKX#LVl9m~KN7T?DlxP>Sb;^x^?AlFn04c3uVv}1xAi5K*g zUA`~;zg&R8kphh zCQBD8t|*Edao$8xUFz1rlqP-4V5fij_d789 z$cUTw=$i1H5 z%z6%IznNuJVaq$a`p{bpaTqD-P1p{8rB}I5nL0_tOe8)Bw)h8wc_B745@~q6m9f6X zi9ehz_7L#+oSWNcvp-MQtU9;9Ko$w41+V9`au)6Dt@LcOERc}Ry~E=G0)AxB3~0gQ z<(o?YQ?dcjWNJ1IzLBR<#XlSj_jbp(-a27|_WCCBg;rpg{*qJuOEUc+P~NkGqLWgn zirmC%uH9XJ^KP?Kpv)`F!w)X?@(;^|aiJiy&fx~(Wl$v!?=7H~JkPq3$(NtK#7rKN9kMc-xS8Kl`UYpSs^2*L)}RzCKH5>sQ}hXEanR2|iLoj0Q4(L&aEG z1Jt}$Qi&}Fm?NX0r!8M*CUsd^R#UI8ol_X+-7q}&Junn43g4Q!h+dglEQr%r(Z}Bw zl(lJRV$iq1lG-}uw6!-M+05X5y}edgSi+$;1ZlRU4c1h4e<=h5%yMtXUQ0IvCoeAI zi=W}9vBwk_osW#DWdK=w@ifnu-WZ9dv^%98g|aKZI}%xZ4}tJnGQczJRjO;UfeogQ z-m$Py*6g#qEXX6t=l`n*e3u>A$P4wz#^*BydW5pTosF}RL}z<@>0ZtW<$D<4nXd4_ z2htd!fFkIyro@nc$1Z^PDiwS!%)3R|u^;j=BttycHIyIBYTg7R1TOxua(Z89C~ge) zP&d^tJL?vsH7fy%aoU~8UZn`B*9AkL>bM5}ZP@iuNSE(-VGR1!s#1Eu^5~z|MJcNq z`gFMfJ+h;;^5IZEMrv)U(g?55WL~O}vC#&|ofT_;DJ&ycSZ;NglMeXBj|Oie!%|p4 z_j;fG3X=}rC)jWABUl7?xOiW)fQ;+L=~RFEM<{_H7m=z)CumItGy1$26Pf@X;ibzz z^726ag-6I85YlB*S5tnhy%o2b{et{v(?v4WF@)@xWy%*poSs>fEu-oMdcW{;)wO=n z`aPt({&L0q=8zu$hNhS7^*aaO{}=&v2Y+-~`Kebav4xcn+7+dV!Oz|6PSsuJ@jldj z7uT{mYfpu>J&2VfqbTd^sL(9ChM?An3lBWvZv%n&oe!mB^ZIr|Mh24@3*D0_x!5TJ zq6Y*lhmKX_M-oDDXuhe--jaJe6giwR{ATicOBDkVuA zyK%F*M9+*80bw3Us~iGzG)dYOe_-<83CjxV9b7RTN>-yy89C2cE0!cCCfML%cB-2%jDIz+H!$By7t-Rn2}SD zdVtKsv4P?RGY>LXW5X2VpV%Nc+}#Pi=ZBAgznJB=pNrWw1N4k*k(^DN#E4)m`MJH4 zIl+2oJAM81gW2cOdIIw2ixz+ave;+|lPOgp6nr1fkoiZeFSWU%qn76920mfVwcX0C zk-Gn5MCGVSQgP#RlDOl{30y-DZAAc>C!M@4-WGx`-(6KP`ml2M?s+L+JK5Wy%yWAy z6O^S$TfQBFu(-QUY??BYi8&*}ti5mWq>d48@l8A0}5GYR!oZUGh~Gc`@W{rqXR9W!?|l!p6&H#un>g{p0)4TLUG zWU_~IwI&%r+^(#s4Ab1pX|e0*rc=lM1bfs2^*i=W!K5vEm^#3VV5o&KvcUnB*zvVG zgZleK8+Wd>l$0Rfo%xR$;Wc;1czymp%9`n}Y1d>Dhu^gCZO?J0a&XuH5n&9DTEI`( zQe@Q_-)#UFt)Sy;Q@7PWoDrqf`F-i{R*_R+=DIgnpZg@pIIV?I-+OQrN=B!MDU7^+ zeG=B6sei9su~%zSp{zSxnhkj{((#4j=jWYJI)&WN5`Nrkd>G_5u{`dvQDVcPU02y0 z%p9LuB5S;qZ}E8#10!qN9~19*?5dABR#a8RdilUH8GGYYpu9*w2RfVAcr!_2NBUj2 z;>=bQ`|4o&?)sfV%}qcePr?@Qo8Ac4dd21WCgLKu%@k*EsHpT7ZdO(v<7xWx3}(X;_I8*; z^_B9$E=gXyzUkia2+8Ygz-d-Y_$0>FE;>-WZgzOZh9@dMo$ty8p6OK+O9d-J=|2v< z&SA0sfE^rOhAjt7?ah7sbk7ae7MfiNh?;>jFLYyA+Hy%r7CZ6q?!Hd;&AnTwYrH?~ z9q#&`Y+Q4PcWrphMYOi-hbfi_J)JECYKoafW2D8k<&{|%kEF!Z>gV(sT$q8Cg#}P) z{3Io`y>ER4DZA|OQr9{GgFBW@9|3k8_Y~Mec_KQvCsycXD!@Y(2&>V#*5kgE_(a`W%ajfH)$cDKJ*_ym&*vD#y_Y$dBcf zN)J)0CJv?`&`b4l0x-4Ow&;U=eEazNeBJHNdSK|^UU1{otY^6lzs_+jzK= zfGW2zr)ou!Lv!Tn?;rh>%2H~NOQ&@rxyDkIO1>q@yp%q1S5d=a%q1?J2L!S<$OD#Q zz%Ys%8mw+3V&3*s1mazF+78KU=B)L;DLMb-8fnoT4);|Q-u@m{`3l=OUY#d&MbLY5 z*&<+XpTw5?8a#A%Gd3^i&^pmCDa$=2F0D98wCq~*y<|wEG4^9j@H&KKNeuUiNscFN zEN0bgz4VUP%C}2URp~pP+_ylz`cufG4YcrqsiB9DwIRT^cJu|VH#Zh5&GYu>AH!HqSwe0fILe&JA*+W=`-)U%W?>90+1_A_~+AAifuZHJuH zrkL}=v!1lSULlUU37BdHp#B5J?@zka{Zx#*nh9%jB+;`wy9X~B`GO(#v*CL)XC$jr zP=KVZt>2xuPDQ{?KeRcX*A!Ro{qf_+uhIL3@~T!=q1ld3PVqG+3dRf2X7gtzl*;(1F(03}HiaqW9So?8Np8O5V zE62?bSo4=>o`6COw|92><+_^h8K3FIyZGss6o>oH#B;qbKF{KPvbVcCqtXXbDpQ>8 zV%>;55U8%I+Mg1ax<$yD_1gLLt;DXKgHmm#cH`KW9Y;K@`4tP}!84z;e_IMO&bbvb z-)H8N_zeAqIJ0Gls4X41?xqz;Xdyjx3B4y=;jI%+wifYA+VCz$$my`CxVBkNBL#ADVASr}3v55r~ z5_tSrJQn8WBcr3Y5<{e=F&iPJ&_| z0iHQeMhUE`s;Z>P42)=rkB?VWG&Qx}I|s^CJnkBKd~oJWU0r}7dmkQLtAW0nO>l$3 zu75Fm4K{&-K=S{qvE+jFX)X}(`ajTV^8aKUHhwiR(Kyi<^z3OSPlx7!?xn}cz?KE( z8DLt0X{l{8FDvIT27I_akdVAxjlyE%irGHRa+zaClKZ z3((GEE%ev-?{OWSoxNRsgPAP+{F2%&$MDMa?#bIP1Lkkud^0sM5yUQ0UM>6Nykhzd zKu!p`(rtL^rLr(B8qE_MlhW=5MAsK9 zC(MxO^%<4Z=Lg1;mw){3!u;Ze;EQ5238#(;dRLi~x1U}&q;_}CRN#MCc;7k+e0;$8 zv|Z-k=gvJ*za2doVgg*2`F6H1{@xqwp;x7U&UT7{AJS3vihpfZR5cDEQe)C$kfKtS zS57k21=ii=W3^0y$Ds%UjQ4=r@r|~(B`${8#~8K;)2NipN}3VIAb||6W47h^Vww6b z#y<$~`vNJfelCLMFIXj!f_&7`DZ=K;gHsueg}J8eU1@KVvtLUB;{v)^F5U?_*I7F9 ztaMH@1Gnn3Ic|?gq5k2{2p5WvN>g1)Bf*kJ$1qD1zY-%cR^@xsNdy)1s)Nrdl@Dw* zBQc7CzmLAb@+`KMfk*+$bgGzkP|Unx*e~0?E(+Fc{h!_ zzcMyDvKzeeF~xE?V|(^57)IlU$B3Ke4yj zp-TKy8D2FIMwximv`S5%2-+OTo!j7J{&L6BL5N|2=0;10WNssZ>Zf%1%^Xt=!M=hgD@C+B zm1_DFK3tC7l?sRqZNR#k;-M^Y=}CLjnJMYDg-hG;`v&?)m7idF?^Ca?AEzP%^2Q;r zB<#zoVy6S|hdMLZWDiU~u;1{!+0l{ED9@xOIz;^P(( zOCD+hSArqAmvaC9ntrfb+UopcK6_wSH-Hp}SZYuiMZzZSCeyhigYL?WUi@W7R-yDP zVZt7^X>YK9)c+hSsobwtReEGAboIh`&4ZnKbh_ROZRI8or-{{r;2LQzBN{XcZ zrhGMG5*tNl7!U>*&~gHl!eX z{E2w=<95UEgZIx|&eQQ_sG$Rb2P(iBd`N^40=YD0w$@0;3KB^*sU zGumIb_JXzJOc1qo-rfOfff_gDRF{4pbi}it5@zV|Lpc)^!&g(!e$?;XM?O>^lOGB* zRn+bq5ACC&>MpsQ*%Hr}{l1x)HK~Q?Z}*b zd}(pG$^HBHO-&;Lu34UuQV>uOxR5ds(9^yc&!!}_Hc@~sA^qLHco7bJ9w5$iCGoYD z``=Zqt-Gu9AcN8tw6R1obQp+d3H^6m5Wv;OMO|4dp36PIF$8pFXmuHhC(&#_yovq* z2Z|$$k~5&!PEa2{F^)o0ud0?Rt!aAR{{i0#L@xq0C(0P3LKZ^V>XR}x#lj`>iUU;h z`;N=Yhn7y&-_rCRo9h2g+O#BkI9_#=Rv^ww^CDygnTMP3$&;tlvaZWXQol`^N=;qb zC4Q@^ah(Rk71uQt(Egf9)6%R4-Lb2|SPyH6pwX8bfPT0yQ3H>x6bi6p|Bg`Z+!{Ai z&k0RQQ;khiyT8WgNtqtSJzzbn3KO#b%4tUgDZ z!wUvMyX~LT;qh?x=!>-NQ>S#&3ux$QJHQjzcDFN#==I)pyw*nejxVc4l=4cTX z@B)gAyDgo4)dR{#UH`HxX534F;lPMl?R2Ea^*w^NTs(NBtQkHx#wMl-K-_EKV8xAe zz_nU&q>a{+2PD6FNE7A&VoQbp5@Q1o>6h~+20FZ);^^%GT)BOAJnkW`LHNURJH7JJtd+?Ayw zPa#}QYUl3Tgc~sL*zm26pQWk?YEg?)8-M5h15R%k?rXCoKVIpb&ujM;oWyMTH7GYK6f{&TfTYms*^hmJ(yKJoZZL zBlT$vzR54?=X23EB0SvZ?!z*n38`m^FB!5nXokMFc~?+~5x)#?Hc zNJ*nI3EtGPqST3zp97Y}wpDj+mW{3b+!`N`t$sJoMAeRtuFj4g%1hs(X;7)%l@f~y zR-ijMQ*`z}E}1LZuLn!<*49zGRuA7_FcY$aflY)Nt~qW-Hm2J>0P@^f$bbIlY$WG8 zY2Jm7Z#xye!s-LFsO3jlxg8Yl-v%T9waWqk&mP%x_$lrks!yg~{m?wWF?$OaePp>EV{v}_6x_T0Dc~80j z&s9bIe>7wd{^;tuh5<$$f9aCtU?=!i2Oiwf+Z&_#uQy%*aIlFsp(Gr8+hy>9n+>EB~muQ^h$$xi)Bf!uO8?|&}iYI}R&L0or z9%#n*vbzHkhdK4T~I=D3AEHX?@QmChY=+XtwqS`O1ufY<-}f@@`H+t<@8Hh1P( zdH<4B_`44R%0BmMIEn#n3S>R9HMN;SSS4~zGdd+97nq-WCr;blgGDol^gtS7C*DlkdC+;Cf|EUQ z{dtrnkFuwt@cfGbTqu15s`1Tisrq=_5z6_&jZpAI*x*{vLD`G}Zm^cRY040~ zOr`JwOw)xw<;AoqFc?0Ls$J-%3f&FVNghT_DcKF|ys{xjD=nE16bc^pck=r|0e^x(CSL8pz5JtXxt3YnwmwCT>t1JIuo!wVrKthjx0a` zizy-78W(^bNt^MS_=*)Wo2j63fa3SzemfZUdHYfS(gQ&Lov4F<%#3kPK#X`&ziR+p z=SlkoaVPj z-ShJG3!?riZ|WL+MuV4&o!c*x)_}5~ z4|-1~RG48-?562?cR#D+?NmMg@y_z3hDYC17=sH#- zx45qARq@=eID=_v{g^Ck;+>1W#cp`dw#+ML-~+ifd+7^M`u*482Ae&k!7{EUcYK>K zga3RyqX8MTzo3mds;q~9Yy&#{rA3xAL~&BjG?kP}r#hS+8wus-%c`3rFV+h+GxZq) z$9v}tuy@WHa>QBJ2T)u39tf=u=C4nN(gsTBpp9}$ytypCZoO93GxMXLzvbeW78XRZ zb9l(^OXxY@EK%kr(EL7qXYT%=MM-0v6nB z1b&CtA|S0_qAonYYur$b8zT&qUqxBjXNf9#h{XtjP=UesC6ff71=kb(cL$V@{Zawo ztqROH=d^$wBp$ULi80m7sNDU6VhKK4ti3T=G+B)VIw;kwZ33qTJb<56;Qaw0m?z_4V2(q4MJ5N0hXL|oxp+bWO?9L^f+0N*k`ZzSdv{Xt*=}1En zq=i<5^F_gTb(J!3tMs!R!Oz#;fuP`kKpFWT9p+w1smqr$GJJm2L zf=f~Td=~9Do1n=!z}F%*5^`&(WRJkL>yh|IMp6}qk_M)nNjok#%)EW_cIH1oOPWCd z^$qZ2AkXm4O6b{y-YY{XfZ|MpfHm&`t$+&)kxk>(O3UwAH|l{JDWEzRwzp}l3`5G%Lf>eRQG@>1KhDb&`Vfht`XMs$9*pL4SK z*vg+@;sK9=Bx3$!@&YBMrCRL86>YdAJub>lJ-@NIv65w?59^0r;^8N+EnmY<-B_RF zj@c_TIh(%)kOC^I$`{GFm(DaJD}H{hFv--j3FD~1$2s#3`BrW@q4n8lX?8$ozPvYq zLa3`c9kx>2L^|e`c{>T)(g~kq4gD)5OaJR;04&N3uiDjB-%sjKu4_3o^xfRszEvJP z6STZOV{1u%^uj^JXkme3d$(qlRP$HCjr~|K*9&}Ay2@EdP52>kR%*-tz z1@7h}GrznD^z~>!b;=*_vRj-dF7I>hE)U2mNZ(YGKrbD|-0|!T><$jYr#9F%l08C7 zwQ3@y|HTg`A3oZh;4xg_ZRzUnhE}5ujCEf`QKHd^^+}>#?T>!$F5F+23QqI300<=S z{ax0}wO0U5H8S=5djs7&HBs+W^AN0>WMt?D_qDH8r&XGFFZ$7e0!`B+MZo(#UaRWjk+u_4}ni(a4BWLg|wQ zznPOp>sBI}G&N|2&ye+b)|)s5+ydk3D7$oKL_|@yPLOcn<=F>-vI1sX;u=xkR&qJ~ zy&REAu?OusLGL*4-xBBXUE2%=@yEQF4@P zwgH&pN86ex?SkoBVdZwPK8%F1Vt}^eL}f+2TMFM@8g%1yamb~ls3ULA-E&RwbQ-F) zY))l%}?toZIMl0P(0Dv}1k2=nel3KJ5?dQ=xt`_-|y4gV)?E zjV&-QBN)o=v1QHnk-=PtsjZ@OHhd&xX=EdykeE3UM7NC(| zzV@l9s>9*yXZcn93EN+~JpL+bpupCjF0CwV8b9IRHPY7%2x+r${4?qeY3CQo$wZrp zsEuI4<=UVqGran-az1l^%#?73wBGo9<3oRVIN;Y=ZsDd!jdYqyw6(XB(soYx#Wl7N z2(Wec^5D~4IAwNkPRaUbE625Aw{(1L3Xw`eY~zE2k8q`SchvoTYXOoAR&DO0gW?ll zR)7XNN+i9vyWoRdTzuFN=;C5&rksmKRkjTRov3vBy4y>WVE3d97kTV@0x&iNE)M&a zL)wiJWQ#w3D8;L#d46Oh8&>i5ddq0 z7rWW?>CKxz5$B_$rZ_z3kxfX|bzq)xRWI<@e;l=c0nAl<+a*Cr(^SiJHKOUK`O@;c z)eWhag3V(Z>MdFvqf_Nue>O$uDH#>Z;;;-#biB0jEnk~$f2*tOU@lj3(a`ATJPZ-` zBG}kKF#!vRo{#GrharDx<(;E+21mjkXTDPJ>J}*zHE|fDBiEbSnv_2~eEUu&;L{|> zFcXCUEUev;9ltjAv&mjJ1CXf6>5aIFp+Laj=KZODn9E3eVpdhJl2?tJ1j<>%Rl5i% z#DV2{L$~6wS%vq`W+=FPqONSOtG32-+pjbo+1lEU6li%gXq@rK>iICHR(eh?r=3X= zE9AEs1_u=EWaFkaQX1@R|W}~>6v08i-k9l;bL<-3y+ekxTOO92=6dD8s>k~#a z>FXwiOWm{)VvbH=pJry$Fc_*^x-{BlTikTt*~X~nZ=W>_2yV}YIa*Mv#-|96U~sk) zQ)4PvI@N=vV2~M6Wio*7enmxvdzG_dBH{ZV;3+CJrR?KHaHyq$Px}uFQ3-j zrF^Y0E%nuWzmDKLgx!={Ns2!X4x*4Cz(f6Si85uBA5WA0ed6<4c;+BJ zoCpn7rjH8}&lM%^hGwaf2Fu^bkhr9{S-(|1n}W;XA*SSsmJ)I&;+leenJD0{RzJ#Y z0y`ym?LF8qVnY=e89Na(Ze+}JTj(URX6r}6>23)hrM5BT*tarxu{IQyA?@+Vj7#2cIl!SUwACfy^OjJ9G&;w&t&Nuw5No->MH^NH4U7hC zb!4PYUtd?r(uxpN8soe27UiR`0-`DPog?Lv6tgahYtL< z0K^GM8@Mv4j_wQ>Cj;FAG7F7FYE35ATGF&u6a}xbo!PqEIB3~OcvO}?2CYP z&H5>wzN*OR&A0zYs2`?B2S%78Y|z`Vi_dnh{dX_m6kQLbB1;vicb^{2mic+OSWcH4 z@JLiSTw}SswXoz;c0@w`_)-1ZWRb10R#gpICmwo|e2qs6p`+$&4ugUs|dFnNk z_(xQ7f{@PR&o}J*ObdLHQ)5?Wf504lPUZ~2IA<+Znl~>83_ucuJUn*1H)ZhXz;1Vy zLS#HRQr!ih4>)WH4C;Ws`AwIjm1dfd4%+rb0It5*|IpK!zxOA7my0mMuQQ#iBqBOZ z==MM#ot1f|8wp%LG#VMrfY2c0}I4^L&3?$#s&uSnCP{Dh{1GoY`PjeSW9&@`gcitpKPp2 z_+xp$Lxn3J>80VLT`i5!wT{pOuVU3|Ott7Z-i#8g^BqBp{q@D*(!gK8P6H4fL^xZ| zxU@9zxfbseZ2`HBj|1gcLQz|rH;{k0PHUF!}|J1ffL{Yhs^=sA^#$T!}%`l5`e~o*`olfWhrjG zyQLqzT8tA=M7}<=vEd!Dq8OFfHIyfn?j69=fqPj zvJx{$`KO@*U`Fl%dGBJACV$d#_OD+j>FUq#nRsuE*9Vm%v=Kz$uWK8XI@)~e>&zgP zU5w^;1<#$6%;R2F*XkZFNd!eAEU&k}=WF8f@;h2`fGl7%gQ`zhM{Y8eUsf(wAMjzv zlC`i*mQhha2NwGzTM_6fZs2DZ9rCCF==9jpScpoc^Ok0^xQLwG(C5$dub$E3F-{+A zX6n}lLTpj!tj7~}H@8+xsX?;;=c(D4fwK)#&HY8D*?xf7j5%SazItatTvZVhiO@Q- z!w0A}0r=l$ljFH|(+B?*@xXdaTsZBcJWyJ@lFvm)Su}ZVfgWpghEzseToLuY$i?7S zt1QSES+9DeL_u6$4Oq|^CVsd| z?o4#QMdP!v78`*A0()>#B%t?XFUD6DJYePH%~Lx_z?Vvqk1^I-e#&;50Uu;BfTFD6 z7!&i+f<<(7xax$>*xlXfeK|im$1YjhSXfX-W@Wc7=F=xxU<8Ir0{k4y%Zur&9?!pJ4)wu*D7bnfV@6(PTz-gTqD2E@eg(TIs1r5PGY!0S;zAt9%7;A(^#x2yA z`+%0Ey4GIlX=k(+*61C8B{4;xXF3heH7>`(G+%tGjC)*BzKBBCym|YsM)c|@*~z!N z-`6&LNimQ~^E9-S-_e4<_TiQkxBupch@>mzkl>{)OgHq*JFdSc&DY{N zy`?Be<~l1awx)j&c<>(kNp9*E`FQ_uK0oQD^W7pDt=KRbDk_Gzl=}$K5eG_GC5*2? zAQSoDYw7l!x`*<=zqH~ODXMJ)sw>U-<6QA_`DrfDUGZnS=2lgMtPLL*7QGZ4W+#aA=+plq&9=E=?*{#w4WjyG|OVD=i=?~vqeF$?(-SGYOD0R^}R4=V5A%Hif!M{K26rphd#Cnv{#gUx%L_uS@{7wCRQsPWnf$1OL%9}4P$ppy{+aIoxwpP~q)6@|pq^Y^FEyxbpvGt$sj72Uos@Z=9@ zni(7yaPPZPLp?PsYhssxBjzYj6N(rsUf3ecdCNaM`rRb#`5tR8dtII6%HndmB+KMS zwLmM8{(&F2xkaM2C92&$tNQ!;ESqd=u&6c0;uKZuX`k;&&tF`z@BC~N{|Yum|1oJk9-(@sL^$`V5||O9&<+)J&rH%a>SB&Wxn-M(AAuQ!oS3*U|BHXiDq% zt!j%ipszYn?ZU@GFtG2lx3fF=#V&D4d~2Nl^;+KsM8h6j4R>#MPdD$_cn&VMsb3u& ztE2RiK|HczVpB8y*s3@03->#O{AFbq-#v^QRO?C9N;K-_=xSbim%-RLQ2%5B0QdlL zO1nUhoH|qSC!pJ?uWT)hRH%`Sv^a)KIj$>I$a8I}|_>?%uw=QNgZ%iiwHw z0@J%;!H2)d2^KiBgxDELMqL(X1>JT;s$>mmK+UmsrFz=$K<+ldXH#9xC6*;aARgYI z-gLWkIuNpjfTA3Yk)_cqSvNa4L~Nhi@#W>HH;h@|TUYSZd|(lIS4^n2e`ThjkC|_x zs{Wxbax-OpetvOod46;AhOK_DNgX57dZrP2-jTTNisVrh9;vKKUYw&2#1aa|dq;k0 z{y1I03i(3Dw}dD@lI@rxX&%nM-3x}q!kBPufPh{X~3Z&YeZ zVoG{)VtRUP;`>BCg%*MUfz3BJ?$zGH2NJi`NSRA9>$^3z`=+$Ij-ajnws@k#n2Tei zG|-^a2jxqtb+)$!)8S(Nhe6Jt#Yac=tuU44b;Hx*?Vd}kVQtU9nV*&RHiKnQXlT0E zQRhWl7+1%>HJqp_J&(%B@Ubv(gQzC^9F~OY?-#SoUN(_wc6H?UdviH1CSLH;nEB`) z(NiIdjke4)9)-NZ%*y3=Uq47dLBUGg6g1<4i(W0f3oZn7vclMSZ{X`ST4zzP*v{GW zmzw$^dnZ7@!veP@g2ZhV2g;B(@!ISF$qG7x>Hz^CZEZ{VqeisdKw)x2T3S(L<*lb( zagFkpIOTJNU`qIY`A_CxazKNyvjZ4-VKyoj;nd;P8>$$GVZs*QL5Q_=QX04ttJ3OI zsvBOo_cd3J^Kn)}ZmgK;W|wADdO=<`D}xP0gVN%r!!8ymdh zJ}<@!U7#oW{CrKbK^L5kNov~ZKXrX=_?oIY|o^a0P41<0=ES3%v}8-=#(sMUJhS_`>tBU4ttWY zx5#!YSBJKeXYctIz@mvYyPG-5B^|r13^oUt>I!zT7r_a0cgSDVE=dZ@-ucOPY_! zr6=HyGwckB(Fk^2?IS24KYoUJYX0uzRB!mG7kVRm{&_}~&go5n2QDVXCw-VHFLM!U zN=n-IXvvBdudJw$fX-l7W(RLfO-xMKf7uH1uFe_q;OQuWI2yMB2TZdu{kT&z^sB;E zPxp}nD6^^Ur%0n0F^(~^sdo4}7iVYL06&1e9Wrso5#pI*4RE`OXe~|aDKke$HY|}7 zq;B%?ZF2{cuxZOgZEMbxhBJuV6sFch;)$R|-i*o}QEBg$>Bpit(G+T`uj*=Ar7sUK z71d7o+dujs{(A&``tEtd%XgX;pKfovjzs0f=Bm%fGOTYc@SL8OJEQ&5zji~~_2I3D z>Xr=!Hbq&Xz!kj(3FYS4>)33-xg*r*k&BS<*U?C9m?D@bOa94vlcLV(Z z);&-LPqXm9&~H^?r#MsMQX(wj<#f~3b7szlS%4ha0JP2xa}E!@oS!Il2%0rc<38iY zI)?(@7kBe|VC=`3z$l;Pr$4gWG{EhDKp0F6DZVv9ghH+{Nb?L`FQ^1r!$Pkwz%` zQ#P)?Uo?H)(Av3P-h;PGm@doMv-i2SMj}bLY?f@pEFOFM?Cv z&*}SYoVJslhh_=DO(YSW)G+owSX$kS^7x9>`=4gX)O*wl*AV9S@Ns6*k14g zEaSr12vlWg1JqA>w4?Zt`HS$byt!^<=JyvOi^0N<+ea(KY-hL{A|64^%=kuL2oFD5iK-8VWjmIqzS zr4#l(O;xMkez$q-^I)H0aF!?y_&`o-6U{@9Z-YQ4IZGoVvF5^e7vp?ftxf-X-l7^? zJ6j0ERPMf?1G)Z~QmJXPFg2fa4fgSZ%F6bX_3h#mnzF@-x=4v@XQe>~7wYBZ=IP<4 z-v32YQ@ywE{xR}}m0L-t$l<6Xr!px(#iLQ>i zrQV^6C(Na|Q=kg-y7;dbPEqRGS+mDf;a_VR{~Cq-U)*f#uiCkpmr^^k{#85Ao|uA) w3G)Be2Mjv_mEy@sX2=tkD_c?6qKJUNMnt45y<-EUE4@eQz1L8J4O9fAcY;*uozOxMlwPFQ z5Fixk5(p)bmTKJb9?=m$rd9mv)Un>DZ~?(xjDaXl!J8^3UzRbpN?@ z?Gpd@M~WL~&c((dhA zT#X;K%-|Lf2%U`D&59cg#PcA(<_AQ|JWZWFZU8Eq6&HS?ww+`^yp32jZ)MrNIia&?tu=G6PVXaIlq9qAbfk(w=Y+&JikA+$vbN=sNToBWc_5YVrPnV*Tc8u zS)P@Ls)Mm{gZ|AuDxzOQPuq{K`*#F6WMoVl%_Phm={{OaHf;UOG*54+b#7Rna~Bb* zGS=ghoS5vM;&BV_!@S9>wKyqecnTfy5f`u6IatjSoZoz2S`7_+q^+cs=VQBA^s@K; zs{mT+oa@8T)M-!TI8Vs&Lmp_ELB=ZDc{}EeF}AW2{4nxWfjc=N}?5W3HFXi{6pu$j-J#c=4mAS4d)>QFjaW|V})&U zddV(4Mv=JtUBbDzcrc-742$W7GSgremx>Mz@R< zAkgzb&6mz_Fcua}Ji#7zFYCKWP zWg8`{d*{I&^MsP#Y<0sWJh}Zx@QWYpQQpp;KI>R^wEQE!6Ok;;M2NMb(}|<@?)jQe zn>ZyoE}HqNFAP0-K}Ize$ytogE8t^Z$cppccwcpoJg=t+A^D@E3esvvBdf8PSswSn z2N4Og5k;r=bNzE_S_knv^2gt^hXfF`M$6O&nz6X}cN+1}3+T<%}KKNrb|yftP|Mn4ta-<@9R>K5D2b{!bXKR@>h-&$8jHg0&hu+_wa z=*%}^3lBch%}~v5%E2&!WKiOcalmgw)y7#&Vsx|G;BCshvZ)%J(ux^X zS^F43N8Kb_jHNESBtq-F_p@VtwAf>L!Fm!&KJJL(Dp+Ql3&~)$8aO^o4o7GmiTE~> z&8ewnRulYoS5pVxg(_<#$+or51d>Ozte8Cx_&IF94IpUQf8u_1?Fy%&q-$bW^JHbr zmsArCORvv%$dP>hi znG&&IT~!-*b;V|Vt5j(I`YbP}TH>poB3=nhg2#3HC657G*ofpmU2U9Ud$V=85PrSY z$?~`9$PKAmjZtr4zGKHcIzt9Ne`%#P8}5}oF5wrj_6bDIaEaPjTlbkXueuc1e9wtP zC%TR_t_WgpFfqi!J$c>oMBC2u@crs>7kc*&IxtpgE%1v2tzAD94o|;pN!ja2wiPrb z+x7mwa@MmyCT4@Ud7NEl9)9A5@BO}LH7PV+72z(><00?9-0H}AK9Dlc_f8|J*;_?4 zorIxYB9X}E&0o}cuuRyd=|;8nti;EIEIGyDxp&t_NOl za(0oEGrZcfsQr%MTB|s&)#mpBI-v(@Gg)Ysb`$EKhd7ZUxwp5z{l0i6pBL$hh1a*# z7|lTmt@2NZp)Z{!fB6*Ym`@b)JDJuq)N0b4gkNL0%FSXMcKz}RDR6H#RCY{}_L_^p zV&YrKmhC{3)DB#2{9t3kVpa>$ixB>zmU^d(u_Rt;Gt&JI14LsW*MC1vlPROSd2Y)2 z_+#+#k(xn+tdvrWHaYQ8sbVebJyr7<)*bYnD0l-D*+ymi=5gz>tb%6uCxfB@gDcR_knAsZBS}sB}qhhzk zU4>A&610ltE>k^>HeS#YVQiM4SN7+XF^^c+?THms0&xc!W9r~jHecAziY^QT7aM5M~M7mx_p3FK2_L6^xjuf7Zfe53RR1@YCHJ@|LkW zv*^~=pn!IquAT*l$g|9P)p=?hCoC*XlX@?G<5zx8p7+hYkNngZWIM+CVlZ1=j!1@z zn=TCxOD8LpiB8&O4aM2UdOc9u`mwB+&H^im^WJ*~H6|-BY^RIIJq=oKsV4rGZ}7>x z`H^VdKa`lU#BGmOd<|CG6KyK$KZt@7*?1b6Bk5EsD+?EeijC|hYqNpFRuU1kboHSi~?unB~RCu9*M0uXOlae!o2!Z){<|`*@RrN&6Ka#Zd=Ea z$x-#^1aBEN{nu#;EDO|RU!8c=T|x|Hx9u^UW(4}l_Y+P{-a&dGn*+zA; zm#(x2yK|2(i{65q{kgZk2dk?$4gVx84y#*=n`-r}xu&!qN~4apz|;m2)uhg$ii@R3 zc~yO#0$;yPCYceI5j%$X&VgC)X+a$tN-!5g#hc zj1Cv=!pEKBg>ADx-${!{tL7Fy0+gMNX#K{woPc_vv)LiXuH1j{e>8d1{po0)QDWds8f9VMt z5R2S}RQ&8FCAsCLSQ6GK3B|FH6T?Jt`ck&-S#*+zK#!G6GQgv{=!~3s-~h z9(Akdd065{P)ko=UFw*bISI4hMaTTu`t`Hvr9g6eI(3vIa%*ChhPd5jJ)7J3eAA&; zd`y6OiNEc@oV@bHD=0hOj+eCP-x#;O7a7mut7;WY?^km9f!3F@_nzX$nov>mXNt%Wh#oJL9lw!;*Gyha{>!>UL2Sve`1IW8DAk@MAs8`iRrJ4)SiAK-zP^`{JAJ?!K$s ze0(N8-^3orKkxx(Yrn2Ox&CI!fG0-)?k66LZ2Q4u8+Ei9{kZy;^OSe_!>D=BMAx`h z%=mV*SManfuFTLoGBn++;0(iksx_C?|KMGCZ??`OPx&=s-ccGl0BGqf-<8$NQEfq9>=zc8z-2WQM z2lbe;=s+>wco(E}^Wst9>L&e*y$a|>+V~1~+2Drvin<$!nrV|aihREXK$1Tr0h0VP z_$YC#tlNBIs<-dZ098&m$g1i8stBKl)(J7=G{^kCY3u5Kefl3m2Kn}$erc*na);m9!Y zYf@cE)hoOx?;Z1xextkza^B<^WLPRPY<=mon@*@x)1k&NOlU+p4MdP0H7H*WGTqOo-rq<% ze*KF2((w%ay_|n;v(nRYF=?iwBo7|sAf!k6RF7X-O35KbM?7g+6*#Z*D~{Kg9FT== z3mS`dBj4#Dwx$Us9~4++qdjvT@ZRvetiVp#@k&$})Q0k!bW7NphKd*ymY&UR(|Z#U z-g5LegbsBZjTM!t8{dR-Ug^+8fJ{3zY5i493_Yj?ik%_K-cgYfbLpk%<3fX!`EiUT z^rrBLTUDBr#7B-wbgFYEL;qBFcYk}k=JI6s+Ez(TYjf|%jDj=u(lTSE#yjB!p7Mvw zzD7X-iDdD>7h022Y*S?&^(}KSOjy_a8&C{JvJm2C(ai}gW2J@Lx6nH$|E_-XqMaI?=yNavGncg5336a2(9E2+Za4Kcz!&arnt8!X1u4Cm{11y z*uA?UlGPEPM*nNTQ?zYcRv929C%>>uh-mv={pOQ--xQ9}`2F2zg7(BMVqiDoKl#*; zMIAbF#2^mRF;^d;n$%C+$3WqhF1-^`OHxku5P$K?v8o)s_AcnX=+$fDOYcaNaTo5w zOgpp?;}?zc6;Bk2*cifNwsri;=UGs~eCA-8UXuCMza4gIZuWE8oINxmAj_a*iN#0$ zzVu4dmv;(q-j2>*(W;lOfrWqSw#|uxUDrV3u*7zRbhQI&iX{l{X8(=7a|O4$BU75pm>;)!?*GlcKBdGv zAS6H9@qM~CFE6*Je_g~?UA7z_#omst^rTeuaExEv@N7*TxExrWy}_xf>V%P8QEwV% zeC=^i4nXPTQqal|?p&EW;$+-7B7X7+zXHdst_&zTS~EHj$8oo5^~!WhZ9A^hKYv{s z+BJJJ=C7bC&qTq9N-*XFazL1=i>DyQB*&207m&T_>|ECI{j^@+!0T_{GkhKP{j5=j z4jA{z-aE$g2jXL{o-$qw#V5xznaa6MoUurU;jOLB)S!cTz7DN)xWA!70c60q&KPD` z2V0+;O$S8QMwAHSSb=^;#r&nmeQtRTw~?TLQ4tqh*_Oa#4PK{LS*8Cq=c}wB&`5?Y zC+-FKjd~Tl5nClVkv~LlJV(N~Y`Mes`&j%M8aux~9XtfpT6MLTQH(IBFb?30p556j z+rkGy4YkSmRTkl|VDJ1oCK3x1KO~M0)_ddj_V%=}u-OT))ANG}k;O|f&F1u&nLx5?r!zYsTx%h)4hHSw844eLvw46@1(b2uZ|n za3hsr7N!+?N6@go01$!r4`kf-Qp$YbAqPrkAQj(*L{m1`77r&Uq7%zP#nz;5QKqen zhA&Nb0i4M}-J0_#6^&?>=fuY}Q^6I;@n+PKQT{%e{9Q+U;wsln8=bM`!I<2~^49=E zH(2CZ^eS84w9?1qRd^>lX8?0pjA>`?)sQc&eAs+||Czg93*|lOH_Q`SJGq$p4HU6u zSQaiJ6^po~&z{az4^ey2>E#UG@B`Wt|tJSdK_lD4_qPMxi}}EpAS7cYu$8N8?k~-Ctw1G z?jGOv_2aP9e0BaaE?y;ta%oYc@q@4RQmv_t+Xb}{!9lJ2LCRunS_N3LQ{lC@*3qihB*u6x7wFqT z0d>u@wx6QnBc0!8mK-oA+y{&3KQxgv@=DN87#mACC;0e-AiZ%YVVy(j8LznT{V6jO z$E$Y+KFe*Zu%i+vd18tKy02^mjCb3r{V`KwX7Zao24Qrch5CIjOH>92EL>ALRtr9M z+R{Rtpq#QRC^L>mwEq-O*A5(t%(dQ84;3=ejp$wqK3;I<=~s)#=SiqEubwZ!9M&}5 z6q;yQDJSca-_mt*pPQIRF~TFH7l99IC4B$-G`N zN$eIOC6LMfjmre=F;RlFrgKg(t8ve$F+wb9y38|SOU?t2d9D?4W1{N)?02$%Ibvz`#`GZXq^zeA1zLETx32G_DeIbsa(&#yY8aNX|#_CTB`4n zc-jH@?N7e?i4Ilo>iw^9&q3h;=dFg7XFG>CyvadJK z!sIf<$MMUpL*F?<4)>~QlVlfqq)BM`RRveZZ?zDfjQuO}9?!Mb_8Jq1b9q~>J?$j(N zk0UriV3+Hq4388S#iSYVvW3yRsl4Z zXBG)OOhxq7Kc^bIl=z@=mEj-A3>{!|LibjMcRGyDHSt?S1zPHI1^~0o2Y^vNMtle?i!6 zN(^cvZ(FO6yQCK z>tNyMAt5n4RTXke zuF_|HcJ8Me3tx}wk{Z(9w9M#D%2be2UR7IW;(&S6Ou5gfSXJ||3nrZo^hxb}#UL!h zu17<`eB{HSVT|5cNM_P^?%M?T4u;gBx#Ds{Or&@_4gOB0GZ$RNg#RR&K&&%9+A#}Y%>qj(8-DPk1lAW z55bbw#-Ypn3AAtp#AsDB7wHV-<9JKrEVg_%#Ov_LPcv}$XiQ^WN@`+T+RtK-8f!{h ze`k~Pye#oZhI1D^_#JPrub)uw{~VPpd9!N~6zCG00-T(~Lqm^1-!!OEy`;WA`RA5; z?ZZJeFzF`v7V5c~RRag~!KjKIBqXc_5aJCAa!S&-O;mG{nl-BJiIM5@*x)(4)!$y8 zmnZIZBt7@NDOu~+H@0~kY1%-qU+VQCQh)_CZpy203S0{uWx-9`zf@Kw>x28TJrew; zHU8U!v0}XOEF(p`=gIK#jT!hxXP2jkAM$0o!*&##@hnyg?maLcs}*r% zD1Dx=Hj<|T60OZmId7M(xC^W)5?^OrGOHVTt#p@ee&)yA1%*CY|NX4qxW*#S_={vAy3O$lYfhYr1@0=hdPeiX?=H{8 z*Ci#zyzv4UWI*Z)HDT;(vAaWKt&8kx_I;uWZMvk0+g{kvV5vO{DjzT{`4yeuJW*#! zi6nI_T#e;T6q5VuyE0E==eTed!Y29Sgyn@LG5#?gob?t=xz(mPDx1Q|>X8q99-E69 z{ckRy(c#BgzC1OOzDOJ&L8*X&&fmHzPKA&U3Lo>ojhmcE`vlMC0|NeYLfzZ9Uw;{{ z8rHt-C}IyPJMaM*MK;Hc2(SbpVeaQ|bIw`X%TXf;)ebyidfXW!b9_t(BGSJCOvy|{ zx6-$7(T7{(SDp{Hcm15Mx_mY7IB;#F41R>zv47$A3yCJxSm62@hMQwVQ{kn%mtOW{ zv}rXHpL?hllJ{U<8>MGEmUae{Y@x8iD0zRm(B?1WZHh2hdvvVrQbiE9f}+8U2yn2x z8uQ6gWuS9}1?M3aEN(p}fhJoA!T@CVl}9w#>}DM4aDRS91))n=IGH8-4ee31(8JAl zz^2{`ZJGNAm94boaRe1G={8WU#Qz+)DDN-mHnD#7HVGC`r$s&_#EUfhyF7F4`%H&D z!Y9gZ2d#t`fX)tandXi4zpx&<;pJ6UdNFqR%d{r7TiAf?u+j{|rmdy&l%Yv}UM^q; za{x4*X)~&HF@Y%&uSWSW%N0Xa*auFo9<40Yz=#A7-{ZM>TbrQc;PB9zrPaysM0659 z-{v)zVqTh$^)(M%`@NQ|ZJWRpm#@%g`V}c;hBVVc%Sl&Hpbn7)&tSXR@&s8-xh49q zfIchX0vOR*w{Z=Dg-gQM@?lfhhJ)YLM|@%s2;1Es`cw&=ld+cqF2INaIz!55A#;L1 z%fk2z_3k=4#idxa9JS1#Kwq_7sivr8qE3zOtwOM{l9r3cnm+o5mtuFTHah-R$&+U| zlTgC;eV@hHp&fM!+o;T?goP5T&d#qlw0y@ObN8#R8k#f9?dv4%`bxPJA0YWc=kkM_oF z02lwGGv)4*CI>gWdWkCrbzxd*sQYPiGua6T*Q0}h7-WyRI5r!#!`(=`U&}p4vbVNg zJHX@2rh|7pHB0I?LlUK{?Itp9Mnk;|#fuDv535$1&8GWJ%T=Y@!A73%*3e)XDTJat z*7FE1JS1jrvHLv!L!XY?_t!&*bIuBd<>cEY$@f7ZCo3N})zoN{^K)&VUXeV3=?>V= zX8wH%Ql}OuC$|?F;+Y7;d~KxNm@M#rlUDVfv3Mp2L0q?U6{2KF20Y)M2Yg?b!q=9$ z&(}k*zVVLSudQ#`^f=%-_X6-@T+ZbA#o)EAwJZirn5JsKly!sG2YAr)S?!oO3&Oh| z1I98;LUeS!&$_w))9lpn;qOSL{q;f3hE$1S!qod$!^@@bBeVc)t`2UqQd0}JrIsh| ztPiC4Dca$3KC;{LckUK`l9uGp3S6ZJ>LTQNtz$hhUovV6bp6XmM)p7=qOHHq!{kRO zc0FRc2g#hMancRgnJO8oaG7qF$Ovkkno?~D15Glx_fCocOu37G7Vj!Wp&!5HshLbB z_4qnn3R>3B_Bhuky5wTIW^~4CJKx2xQSzApE+fPM$;F>wPec7csu>JH#`Q-XK zo%Qw-77B$wXSOaT!RD~QbE_hbcU~ab2pv4Jv8~Xlz%;v63LcOy(n8*Obyxxs3#W9o zw9PI)`UwfKIIzptXAe9Mu+YYT5Mi7>*~n*?4;(!l0_?&0^I@vWpxH|zd7N=;Gu9*M zWTP;c6ztu7bJ`2Y>U#&f_3uV_+XiK3%+eccob!A;zH>b>jVgH7zMUW#$}iQVAAd1S zGwnm<2br60wdN7sDf3piF_O8Ld!?&xK2h0HX~yecgEGZ5X?837mpecaVkebLZ_`$Ay;Wj4fiO4_mK)BFapY z5U>&v+vW{&zHotXG~OZg4q-3&dos;Kd2_bX{>ak_h+CjwBJZCCEvq|BhHEYI63rG= zkUCEnLCvb>^Zr)14d0g(;01>DH3kgNEx?{5daB9+dedFpNs^!j5mGBu~@x9 z_$HwcgjY&mV~9f3AGXRs{|XCD+}oLH{%woHgoKX)gtICl$g$IC@7a^Ezdlm89W>j)V&qXt9 z*a%$?F0e;2BjbX9fEIn)<)$V#LIO=<=_z5*?jz)jtPqm-9!9FI5^}kZgXO(_9E_(z z7_n95<(<7JD^r7;DWhdZT-?~^qm#K{PHgOcR5Jw^t3sJ!F^asVrSP;_@zMrd}Z!kK1^6OiTTQMezRS z>N0UA(>yy&z7%uI)6dEl_J4SzRP4N&6~i7T8I+rY60OQF(EPHVD&0cJeB>vAvW{t z*O$e|5|)>jK_+Ih7qR<>bm$6SRikp!omNd<%ZH-QwhM2aKm1y$6C}PdeRHkNu~n3>h&i z378Je44P8i{<0PWKxQQtV{mJWa|;_UoIZU}n`v5~cxAc5FCc)LNzYyC>YD4-KN8?Ww439n)tcNXZ@kFa)4#eDvg0=It@V8Ry|5$^ruV)O=oA*5h`8X=o&a9 z>ihr!>&7H|?~bf&F2R-;7`hsANlm4S?S2h1(5Of{G#jT@8>eNiT_N?059y3=T4QvE zRpWmbL#tk2)6Ff0{W?6#EV@FwFP^LB=YafmlKa^*|22T;cL~#-zv`R=&ZI~bJ6M{v zBzlL3j>^rXmA-xQi^0@C`|Z9%ysBd;jj0+W^3%uI>zk2`K(S7Dl@}~VO%lG%fSr!< zLc*1CT#k(Cnex;sKvg_}_C2H8ck>3Dpy{7$K8N4m33H*=^o1C2(!1ZDd))+En^~%Q zWuSKK|5R7rd*`E1c#czQN+}Xm8a_Np57xS03a175o#0LU-Go+{ZlAM{}>lHi&_U=0>+En`sq=MWH$(e~=d7XR0hnZz7jkz~qc5x7~MkRTRH($QVjv$CL0ljP- zeXQCMStm6<%l%4QRD_zSh=`|8<@i=oNkjx-ZdE~DEXu{Tg6xj;IaT5xO8j{OdKnFA znyDUvxs4^TJTaEePQ^`^6^NIc=v`jE+_JA*t4s~ge)Gm`_q<+&YxFe-R|bL=>c6=F z$JDqJt$#GsRRt^#l{L?|b8{&SHAg3SDra3{dgqmtx{{X6GU`KduEsWA6 zwv~2#Pvlt@h$s!s+KJYAB|Pi-iX)UwLt3p~^__O~;EwVc!SiO96?}Mqw)IW+v>oMb z3v+|0a_#Zy0lLmJZ)1%)x&AQ5110M*R2-EbK6^`SzV7X9TyDIhoB_sEq{Y|h{SfBT%s>?%fDB!r-JDVX*xp`Ypz|wzNB?>I$;qXj zLshmh4}6s)*qdkGv@I;q7wO>>D;)Cx^p=pn&+{8!Gt5{DYzCr*CX;b%rS<1PM$Eh} zQVO{Md8Vqbudj{o6Z*&c))BLY+CX_lZ8zp_^u$nWYtjQyIxd$?%;wn(@G!5y8w2T5 zQ&UL+7pr!wlLR?MY)9_wJGt6=PII1t%sqVa=RtRG+WhdQvf`;;RzbIRD0Sn><~2@~@d(Im{ax625(Q9l3&nPfQxC@s9)7 z#)Z>?W{p&X_F*c8{_v%>wRJSw?J}h0anxfp1j1lsV#FJ8ro6YVxA`^q!`A<&o#oP zD?I8L&v@An5=7WiJB8I$b&98!pUgDyb3l71W!oiv+3!UJtYMpR`%`s!`AuZyzr^OpeQ@0|QNCcWHPm zu9G8ba>`H*iAHE$aHU&y^Ytz@tc}wTltwztWW5v^p5B&Yaa;$QjB82F>WI!6n%z7X zOAf3;_`1vITjfsE%#f?6m?VTZp#=L<)iq{GJ~-%m&EuZ@AUT)m6NKQl`N$W=v!vY6 znj+d?lMlB`4YG&86ph4AtXL{XGTnbBO+2tuSP7%6S=eo-d1D?}7-jPnAK~xwg1k?8 zvbFSV7V8S!Kj}9&b`NZbPnOM-B)nN@j?iYNM@!}*KU%*#vxMZHE_rB>vS!NA^yiL- zOM%svo1k4wbE&qr76vN&a|&?dW^44X3BQ612X@%NEquh#bb?$MX5Fi&vvXuCuT(3t ztGhcUQk$!ZyP0ru0Ph_}Fi%si)+au`0v@^%b&oRrBOdXA=BL*kmubi!P_Fk@@k#QK zNlD?7RytkG9v{Zueif=ts&X0s0<;&}S#f_)WxXtENvCN*y6P90u6mlRNJ~#(yh2WW zA9OhAXd~#k1o#i!!mZY!gYNI2QYWGfIB zJ@{BQ*~3WwoxnWMhf3GG_n%r-Z2iM4P_`Wj!0vH1*zd=3eQfGTNrtVe)f3@K??QuK zg&OUnL~E88H}c}b;iQvIz?60>%(mJrMyyb8nh{ekc>-A?DNvT6WP~>}mdF5hErSCA zj5u5sw5~6jg!p!X<%FCTSYH=X>^WWnPZX<(=*e9COZtBa37JJN0hCebHM3=(?-aq#^{h(W*4R{l6&kGQ!+i7gBT#ZP(zU(y?nU+WN7?DdTVf+y&Hk=l z$2J7zCW2AptL!S_s$h~u@hm#EDz+-SZA>ogwAU!RDQB@E1Q)zts}}Yq-5Kau8*1%| zYsv~IlAiN?1AB>9cQ+Ffu6yPOYEKOWOspd`lVov4zEEC+hPN7}nSRrFernWC&6Tsw zq|GwWfh&a;b&qdHcFXMy4T6JUpLWmRW|}G+h%!!GhYn@idQtbVtc19?O353+LQUKs zcCyDqR}i7Xxq)2oo2AKN5(=aO&myIE98C;wK3vY_h{y^$2L8VelL6DrYcCo1B8@)i ziKWsdQO(UnocR!zuY(pecuod=;j#z&j8R^8gEh`q^8+Uv%bEO1_1qaj3xfmsl>>~B{>D-JI*(Of@~eRwgZz0vzgM|;oL zT%21={YgGv^%;DX!`vxj8ueC#!|< zO@rTN`zt@I1ujK@j!R2E8aV1YCm6O3c=g=}nhAV5L%wd?n*OuqxlQPsPYlZ)s=T5C z4;(LSDt=Of%{Cl#0&z!f&)6K=G#H>u0r>hvr7fxqmFiB9&;5 z_7Bkcz2?PCkusu%Ize{mEnpW&UfD$V8u9k1ikPCDK)ZYB<;#~lL&HIc8>IO{jmD+b zL7kNW*Hn|a>LtV#BORPMtZ@82(sx7Qj#d<+`JZZ;{LZq0))avNGSVxkUszo|A*(tn zKOp0rA;0=eE!7`GDc%?vge9^bvy2yv0fR|Y`=#xJr^jrMv7g@-U=qG2NnRMD56ZPM zEU`##9*B}b9SL1P(CdTFZ$eTWKMAyCM@!7@4_lOZ69l*}jLr6sO?C5%3G~IygIFo9 zIXxJ=!W+*g!0PP?iVyjxT4;-vZ@44Z;QC+VADmN0M%91X|1N^1@qMxiKt0aFtk(;U z<(dFOQzkn@x)RoYqL<28I_p6Ue0JqcO1)bjRw4K{VU2;smxOY9Y-|i82LsRIuT_H$ zshgiu63p{qiph_7SdaY!{2JsBW!JI+_=uXsEn4c;zO5s-6Z7$| z>vz0U$BOQsio4t7!j3^TL~2PqCHl2;Kk-m(9DU;wL06Wu#?h5df>gZ*++Is)J=gDj z7S8U`&w(mgD@7j4ChQv?Oxgf4RA55BK2a4><9$d-I%p>Au_2m|W>N>}y4gc|67b&c z9-krxE;PjV9IH|7Ppqa5e*hq^*{LnEYnbsD3P73Lj9%xxgSq?7<9Hix#{ATJ_Rsb?_aYhL!jpAS^|6N9Z@g|Qjx=jFIw$?mCwKw0JIN*K?t3!x4dE!)*>d%pi-B%Oa|`e(ya z@bkWKAP*xUV&&gURqXFAQ^dX+<&v&uXIm^R=8A!qp1k}1Q2#lhORFWAEWpjHeNMPt z%rsO0`gw8h2#L2-BFS@&109i{q-%))LG(y${KV^p5hd6X|s(yH8=OYB(R1YLxqWhB}bPIT@f z56J8t^88oA5xJ!@zXv1BWQQr^QfeS*z94@36nL;M!YH(DLJlx(X<%1i0<$U6L4dtV#!H-aE-qR#xAW0JZR)&%C)60~g6$T24u3Wyj@N zLJz#vT)d^8E+dgOVmtKDw5{V`F$TF?CPVnt)JRW$$>w|(%*B~>pJ*E=k}OCT&$1ODOh~ z4!*`ODcaPsf9m`f{M>8VRO$XQr!=)R`AO^OV2w!rfQgjtj&wnGvz*88rO&@D)X`HV zLSct*Z~G@rc)qfgPnD<2%YT-AYB9aC2Rz(>>y)3&R|D?+%hPn%CTw4d+;aS#(C(N< zmSrfI>A0q@j)th8mg4}F>GQSFHRwmI2a4J}Nq1n#eUAdNXyCbk8s>!A)VIxcE zs22cu5}x?@O#k@|noDDlnR7GeFMxr)p8j_}*wWSZxb^d_0Lxfpgw`cm)ExnGuskqY zr~0rHNoCdmJLb`Soc@<84~z)h{e(L0<|f*2MbTFI*eHCgc&3_9PVO;tNl6LJ+hG)G zlB#O07C{PhIITG|51*ZCx(dN>UXAB7%K{={Mxii%LjuKGkA@~3sJcCVPb9DI8Z573 z`^WNtI?cPt;3Dk>Xp@hlm2UCu)(UPC*wwq2RN3Xcg9lSen3$65v0~(!d9di)!Gv|f zuQak+;_n1$H`7O1UWvc=4>vX$7tOFFc>|Hl3@=))of6BC5EcOK_7-A;Zl~ns=C8Q0 zA1zu2&t|Cb@DPW)mq?BVMR!?&L}@z`Hq|}_+s~@mD(ycD^BD>F%+a(vq+{@?ZH3EhT4ZN{{aZ*c~#06)7344!VN*zG0zIguH8R(+})`^4VvBf5J+OLUBn6 z(B)vsfrYWzk6V%-SRS`1%jD(fQ=bn@lk|M<$~0CM5DyFvJGzL{6~S|q8ZQ#CCH!zi zs#J~!_&F|qW&$a1#{c(XeTzTmr~gPdwt2P?<*p%><2s z>mUmtY3dIDTK#HVp{{Q;_6=uCwjb}cz|ng|Jw3C3Dj?MQwD+vF=kfpjWayD%ZS(G0 zVvWbzc<~Uxm#)FbjSlaGwO2=l^tkp6fK%6fd12}8cPrH3z%+Qn#wRa;sGq6UwNnvc z0Z*R%SY%T)hBf5oDsCk#F2;fWCtxC2Og`bx{3j$oU;q+R<`=J-<9n#n`lKlJui;(b z1rKCkeb8nq_0PVK1U09S*#!}KXJd4G>6;F*HinYdrQ~72aSmqc#{;SJVlZyB{OZVOo;9_g6?;A`)DiNL?oZ9sIzDW6R{dGFhZKpr#Wmt4e+PJ3 z#LJ)DuCmirmf=*O7SPR4sitH9{j_aLFSo64lt-RJ1)f-zc>30Zz0CFHTbmzX0qgDF z{m=%BripkLY;^3FVE<(@=_(c&W19DOZp)ak0In2(a48w-4Y`|{|FvG37uki*+;b^? ziwq;5ec|c33#Kt(Xzv_w@S0Wnq&Dq+L!#BLOaQV3$OGxBU(P>|d0SG}o_;WUI5XeBMk1{>`ailpC!8^?e*^ zenU4e&K1CxAC9#ySYKV&#F^s-ohTssqc*#oD(`Cj!p)9+MJ4%Qw}R8!_T(75VqSMb zVf-F*5b5vpz%+0H$dUm&Gpybg6hfu*AL?VwWqRE4aGLqq3^*r4Lqi&_5|zlRpw$BT zzO`{s7)6Ph%1@0>7Q~f}12b%XpGk6xjRSEML_+&NG!z3px2nYIRbO0Ua!XJa&~54Q z#T9_Jw7`_wfAmNpnVVNxWALD$!p-z^3F<$1`n|Ll)kog$^vcPyNRd<{Dd$Z@;NyKjJt7^p0-{o$Q> ziT(?zh$z+qD$uGqC89pUy!|Nk3PBqyIPiZXEzW?H3|X@gFx?$4ey6OJF&;vF9r^H# z`oq&7?!0n^p$fK}@wj8)uQVE2|E(dyFGHXd2b1(?qCfysEgu2@g5mxt^8cS1Af!={ zM`~%YUJdv;--SY>!nM?&s%mPejfdQtG&BKABqKEiLgQES0;kc7)&O#eL3~5PUjPZ} zjAB87J@D_&G^{+e(N8!(Lq+4cTdM)W>d)S>cBTK~`yjqRiASY2<`k}`J``fHu<+|= zqay&uMIg1Zo}Qq&(uwjQ%0kWm)!ui8HMOnlMqKJfM3yuK1T28kRRjeERC<>xEud8C zJ#?_6A{|06B1J$zAfbg&q!WsOl+c^>fb=GCK3Hr2x&Q8S?>W!j_ildWp=D;wImY7NEoAi55~@*!mg+gh(bn0So8s5 zdsRyh#mcceX4{XM3%0g60Q(Nc<*IUe>j_?cywuSJF=B^u0xspqHHL$?OX@W-F=>sf z;POw58chi1C4s-kB&!n<_2%I2JhT=fi8Kf%I`G%lTse4Dk3K%2b`baSh-GuZ#|QMF zvWMTTQ-hM=p2I&*oeofv?1yf|(2_!cE>s9NIbalk4p}IP*OI%twet^Y2 z+>4Qv{?~SS)&4-+C~VJ#8_n7|+=5eh7frM`?vb4xQ&f$X(22hM?(?z%~k z`-4^xq;i-6yF6^+Y(Jh9>e63G(i~Il*$cwtYGusBk~T&m9$n@XTSxv*j`|s7Wl-op zm3rF#8I|go%AsT!oR0Ti*_I(|-JOzQ0OPm2jpV*6tCy?7VyI0J;_vF>;axr?InF{H z_JIgL=bPDjc$K0;h_tEQR3qn)B^wS6Mej|fi<_BcI6mQ(OP?zu^=nNm>ViYXaQ+da zCq|tBPq=Ywmi<@WS}aI+!8M`So`t7hCBkC%J_d%G`ukGs=;$ z&7UmwAO)3h8;$KnH?(Fm-TJIkJJ_|=T$^+(t8kixrlCQm$Kpu}~mIjB|MGHVSUwXL2;GeeCjy!a4Z0nbp z%z^X=$Y{X!ualtf_8gnj%4Pd$_{hBec@S#8uW+WP;l^5*S9tIcFLN{dk%U=QXUm#>CGq!b)3aCnieygb73O>VjP{u#ds(q# zoN%){Q@0#hunk1&34_zfBI995X;{hq@~vCydDzm>bo$v`Vqc0u0BE;i>ulD^7n%Lt819FazADDr@Nn1X7 zBDUOE5BhixmBjtIeKZ*G(wycvgh`WTZIrV=CuiA?>R{bB#s~Ysd@mcpv-;aB6%J44JVb~C6xqqbN-G~J=9@=y{{xQ!k`o1ifQpIk6JdbBvyhQukgmH zCV*zTIqa&+zD8&2l*E0QtIQJ+-!IWlm`zWbLI+2s1i3;D>C4-HftikgK7T!FDw^Vk zsK3PJoS^f|+T{E}cNe*=k+eb#`P4Y99crw(7V0n1Gl<^$Tr^aH^Y5gP@Brg6p;$SN z@@CE_JvE+wmLxK+N?5~cFLSbM;*6P4=Wnxyt6PT;^cLG<@8$xsB2!z#eAhv`6iP@t zW8|;$fj6Wb2uBGIwSy?G+*eZPh20Gpi{`}!vXPJ;akCq5Q;=xJ|062~0yxB&b{+to z*oG{9j)Li6@rL4!+;fb?%A0x>!<)(|BS%XkN6F2#Y4BCPkZ{T& z!!ClK^ZuhtpT<)>yCy}|bRtf=KTqGGzpf5YWG_gE(~u{?FxKv9Qq>$R8<1kp`$<7T zh3YgpmH*pNG+#E;qKLD;<&SN>lK!35kSgV*a{Epn zVHA-;4kM#*L??6VET11#e^|SrdpN1ue9{rF!~J?d2J%T}nW@R{V6CI3x~h*%L0rrF z9zg=&C8y~c2GQ)FSE>{jWfkS+uKl6fVvqh3fW?~0+6oD2QCewc8Gg#&3ynxMRbItA zn{Ir3W}Z0BNz`+k+541! z&hZ{IH%>hJGG}|sgI?4i+Mju6e_z8yZj={t9y(Q?`=`Eb{O*`N04|ZO9$?gfHSO6_ zsH$VVso+>1fH+ktc~Hhb+j)DP&+-^@f>TIL^?eouv1O>1=odjI)x2}w3vpqRVxC+yXc+5C@_b= zqxcXQz;FH;F-7}`qU3Hwb^!KAKmfMsQ+liU;Czz-bb*ZD3&ufH&rjp=?!-T_0KZqP z2dd%uE<}eyDr51B~?H>@+>}9NT#>2TtnqNGZNSTm3Yf(v|E6%g2?* z@W!L|{fEsE)hPj`Fs^BMfPSieQX___h!vvd%3q~S=rJ}xolc&thWVi)(0z(QQM{kn z^zFfHsY16+q|9P#b}7b|=SXp72zrHobaH-keM-HPlBnj<+U@vY@MrK>NEon3eGL-O zx5|9sD>uGX2VPTx+av3NRB>!RMz*b3uGgxoxV1?M*KIwR#bA2^=zp-Bh@?gLSlL%*;$1%VX9HwABDrry>$Ct9ekL>va#~$ zcwUZ8-G_4*mee|oqt(hozo;pe?`Nxem`&~(r$e_nKZABU?zkHhQNg$o>NjdV(Ua(F z1g5P};&~ED-pzA5Qj5zz<$4}@s0{wDzF@V~hgiYS7HArBC!YZ(@C`$jMH3-S!Y zbJD!4BkwvR2oX?>plhrd_CBZYh_z=Kw}&w@$6g4#=wc>UfyZG&DlPtzP$Z;&A~1e- zAXBwApWU=fO)ckPU2f$?egC)Ul7g=dMA0GdU$4qi9ZS?;yL8Fav>r<8PLMd_zRho+ zM|+*&weWfS95rHSHd9Z?sK&1it4>V9QfeSCW7GA20O)hbm;S1RYR^~BFt@F){DNuS zW=)FtWKJ<&zzkNnTN8PFi)E|fulVF_V4bxe{8cbo5ovp_uVM$-oX}=Y!8Wi?(Fv(# zgxC_Do(?Fe$kE*bw{~TFry57ssk8OlTWz(2{eqDb^pU}vQ=u+)71v~0EgGW3=-*$$0Q^Cu=w%W)Wgc02Dg|o!NBpD{w|8-1X&D*?*nw|Bd6* zwfr~``CnmQg=#A(Kt;dMX9q0YYmu2+JeQBQ=hhxe(gwj{mZQ20%vaoO?M8X~0=XWO z6=z4?;8MIDY>8}>40dQMHqDmR4<9Ccd(af;GYq%`-v_h}M(qrk@w@gc<%+H}rZAM` z_ONH%5EZ>F$cq#Rh%GQylh1s&dsaxDe$s3mRalqhL?>+~;HrwOc<9u=d$ySGWbB7a z#kYlJ5v_yCXK_jcI*B(%T_`I-A_Y~mTczv+T!2903Zbc$@7l*1@nJ#J6pI-K_m13@ zDk=kp!nu(pyq}7QaG$G9CmR=sSX);n4YeIfhR|L_eVHOQqxs9c8%Ej@kAUL2Bj;5+}jQJ|H+jXi|Hw)#%Opdk01J7XS-Pu(W44Noa6k$+{1?lHjv zV-wSzrG1dfsq1VVhxw7EngJ_cI={hC-r`da>9pfX+K2tev;Ak;0WNUoCx!?8$O02f zf}^9^0}h}6N1$fQ5Ay2=)}pfm0)lGm57Y;UhruWJe4y_)C=9n-r~^6~($r8+sOI5{ ztODbLZ9z|0+pe!#bQDek_;$dpl)_5m^=`FxIze#+%Pa(^IZsRo%T+A!DK9rPG~adm z-CRZ&JAs5*)rFwtKIlde!I2TEQX42GZrSy%K&nkAI35y9?SUHhBMf& z09v}Si(am-NXZ4j?~rhNR(2`b2<&6rGvf@5KBE2KjciPbSfwzMXHZ{%-?`2&Y`r=Z zJt-+^TOK9m=B5O_4-&GZ)H1L_xj1<~Kv3%-St#+&*<4i1jr5wxs_26aK zvzYyzl~moo9tP5SUBpMYdJ6cGkqtT0o&dd2_RxQKc-zaeR!1jZH?ne%syi<`<;t&$Pk1QCWtq>5LWk<19>x zZljK}S#>Ve0RQ>&dj0CP7|#kkjk=NCd6uvK3daF9M)w$2wlUSF9=&Rb8tsw*1;tW& zhE#E$<`$~>+cc)P1-5>tX5mgJeD@TypLc$p&+fN2H}qK<3y@cB4f{0d*d9W=$fj+y z`AhRqyq&aS=?cH<=IsRHlS9_o% zBI`>w52Qfxr;daE1b^Dv+zPrZx8;FUNQp=vZc0j1bVDAs7lg|)HheS*z-*9 zj3cS^?oEU%YsJALdQ}KQ2VES$b|MfP?0N2EiHQb5X@u)e{rd`cE60{cThIf(ccmFhKIj2K*y{r`I$FmUpa~Ndm&LRh^X{^e}HG$y9z=F$wL0F5qQ5-rPTC{K1i0OcXDtU zL;oB-SoZ%1-P4D|_y4oR?!P0j`rl1v_@6O=N&DXrT>hWu;9ns={@?p9+=CkS*~$N# z%k(wt<>Y^20r1@a=NA2E4E|M4dieLAs{ap9rc`#eG~iVcTn^4(yAYaxJ2d3~b?(<= zAaH?xy`p#Y*VjX23qf0{2(J&Tbbu@lT?2@FA8(&wbc_1)*>k_+hTlO(BwqdQyH_I_ z6*)+JSr~t-kpuqoieG2s;vs7Z%>$fvGGGN_b`VPDN=fc)xrK9*tgMW)58kbK=O2}o zc$&oEVvo*pZ$Gm9{JkyCkk_x>SER8bSc{42$LYXA`zdJGxKKSh%gv1{dA{>n2PIT7 z<5x;et&Z_zXX9C}7pG540S6#f-qZ^}_%g=;=f)|SP>K`5FLhjyuL5(QNg-s03GyUA zh*s<3MmsoSdKRbxPZu&KagkjMN41gJyGgF5h|{yNr-F-ua;7KMphezeClD`lHhX}B znQ~5x`A2#fG(s$wsGq%jA+csFZrGLy=)>|FoZIoorw zgo)RK(BmUR04=$yg0$cVgTL`OHT$kSsVB;3Ul$_=nO>K;q!apxx&#RkHa4q`S06+! zE*Xx$_g82TSnu6@rBU4$;iRCTAntnos(^qxTxr_XiTXDTGYsZHIU5wXK%ggtW#2?> zoP>+jqoxk^i7C9wCBk=Qcj)sj%WV(oj$HK03e!$`5GEp!@FYzrqj^Y(6gtJbVV`F7YrBC+SM>F9l4{VU8M%=#p=y5{T}5;JS) zFC?pgkBPAUl8y091`!FZR{;C)+lH%GWOj<emiS%3NpkN!5ciG+T_4)bP zF}A_y_a8}Iz6AbH`}zqzx8c#S6)oGbvZo`q<7p=MmcrgI{Fw2tzMM>1u|rBrAFi%% zYb^=!F=QO7Csb>8EbQ zwxmY18v7~U5FpdR;k!eXX#ce!48yZFgvw^r)77;d%FFev*Xl-0r?Xw45l{5$ZWMOM^pJpR=a6z15J9XTq2l%HMt_PG|CkdSlBcS$x9~3 zNi6oWFYtI|b!ivK4^Ok+Vc$ouuUKeQpdngn6%54=H%AnoYD=ODwbGp5#dLA$XiuCc zD99^|IZtRvYG8QUPMRt_$_g#$6)4CL1b&JACJ(nje_KzM0m*W^IhJ98jjWQ7HWfGzfbd$G1e_A=%lx!V}h3l7EIc z;iuvMM{F+Uo~69&#Zwo=ntLQu9(Skbaa@J-IMKh#V>??j`vnG6rVDMytOTp0+xRIL z=Sb0EKE69nTeV4XI|!=j!H-@H;v|;$?*byV^-5E6omqu_O21SjMn}8rdl^B_$s4r# z9#F`|ID?a?OT5@M@;%lv>5%d99RfF$6Dhywmc#|6%h)#v+No^RtyziO?oCZiwdpHw zYw1cwwKP#jQL%_}GV~K0FK(PO9f>Vk0QVvT}qoGaXH5lC2V_PU2l~g!Oe)w`e|CY{mPjt9Q`C3 zKFjQLB);L`K)}z@^pnv<;%lUq1X#POEGvsyTU>i{(&M8I!9L3WaiZbwN2fp>JfM)k`Qt9uFtk zy3DLQyM|qq{jRK;$WNKHe&gdb+}-Puq7?4VEB07yUI>uu!e3k$r$_$hg}R{+N4QNnSZ*Ad4aAF}3}+*W4r}{uIqXbFBq& zwgr}@Z4d`tjqz$M6}i)NS;f=CLt6iV(E~MkBQ-TIBS)iTuVYtgudtgl`keZbc-DKm zh6s{g&}Vbknow3UgW{^zdVKxpvK<8E#Rz6{GRvsM#d=@f4fNr-vMbNr73IT9>dTiJ z*Nquf(-xS@LAf}1S!f&XN)F?idisqGEiJ9}y_^-x{KGz(NO3-A{AO=7vG!E>hRv5@ zdFHh=3IF%+D1$zPie&C-l!k>;6enW?uo1K_shd1qQ*)|$2`pO>Z(_coqSV{hBtM%~ z*xh}r&t{DBQBGjsyRgC6<8W#O364=A5{db^3pD!W`ZL~s%CFu?ZEkyPF}?49&{T$7 zX9@XcCnvsmBlS0r?RiOld!uCIakeo=La70ZZ8|m%!6OQg@^K_Nc{yWR1yI85I38eb zEAf?E()X62fT)#|$5lP5PW>cPZ?tUWqC`W-nEqR^+-4abGNBEAh%qv~YUUl*uwv zJ%>q3tMWlo4@X0Db9i+zxm=?fDS|9_)P#FRR*v9WUo|k{pJpkw|K3!7#?T4z9(>JL zmG3{wG5z`KYC-55ibvLc4zdY-{lpK9-7+0^x*p2gqO5LG2A0E__ooU21gqhJNC*k@ zfwO25=X+{9##p=?<<4w+XeeqZ>c=x`uF|veQ_0mpuC1kJmNU0R$G|`wI|Jh|yWtA` z*5sI@{o6n03wfDlRN6oyHc0P!%N+8=Vl=sDD=J!qzWbhMmtnm-I5=^U;I$HnKYKLy zM-~!}S0HrP=}xM$z@sVbvYz}0WWm<8bO}%BwqmiX=gvEb%G<%TMYl)r&15Am^QK(z zYU`#&j8uWBGI}L4C&ujz&!-Nbp@^m}r!zbwwt(T@b11bej>Lnx-sh_jmx%J7O-It3 zpd#E4pZ%y6m2I0r%);gP7^^qj*Hog***Jx3zkT~nyyB_FWhjq(l=6#- zV})MqoTFl2|EoLHcZ5Z1>T2d(c~<#nn9iJWxqeN%!_3mMuN!${ot3ATKNs%g!c8i4D=v{w6187VdGb(4CNNP4XOS+c#jx1X6>G}KMs4pm21TH$l-EOQBC|+eN zDv=OhaaB2Cfls$pMUIBeLGC!5d&hTdXxC50vjn#wdj3yZFC_z;?$2h63&OWVPX>M1 zve-6iR8`xFSk=oLz$;XI>+e*0kmQH+_YveA9bJeCjo~8ioUG%w^MCkJe+GXdkXOIF zTw!N2hp)ATu_uJb<5#cTyKr`|yH=XnQR(}@!AMkJLg)0E*qSYjlb)0{bZ3XA_LaI< zi=GJL)x%4TrG|a-9^b~MJmzjJygxV#1BbQ<{-M*?macZpcgB#>ROD=S?*&nXt|E)- zJ}JcypI##S(Ud>6ck`Y(fmnQPR%1ACCwNcolZW62vq+xpigJb>VcYUy(VLf})ZOjv z4j-R{sPz}+6>@#igUw1dj&y23z>M~4QvRzN+H7s9+8UKj)EnorZ7<*bP_dp%$rZmH zlowQVszo#traN`WVrl(+H*M~FJxIQ|E2f*LFGo5Oo{;dQx$Qybmr44`8#1rg5@S}3 z)2*4R*P<7L&l~;eA=M*`#-w;yU^X|HFSGR?8tQchosAq5E4-o7GCpGNx$!j7YLn=A zLA;ImS!%nRc*?iZBM8gB(kc7z5nnHPT-EuOCI1>Mm5Ubmp7!T@L{$8S+bN$1RGBPseR%TbG1+r06?b4n@-vhY{AZpvhyRslk zklg-yQ@oEBINF&~KEJ!2heSh1TR?hf$v}2wG$lx%T7EOx*{C>@{%N-~(zwq!H zp6kQK`H}+xbvMQF#K``H^%U8eXCH4Iv{;d3Unlyv2tusT2nocUm>ZiPnVpXckE5oE z4Mrz8$>^xKMqb=41^o#`Tvb*pkp1ei3fbr>*ke~?f1Z=vh}P@#lfI3U3vn3JP;~zUZaI%Lob6KOSEt%r1yc zUo*yMeg}aL@by1%(&zt*CyegH`|j^g{pNcKFODCqQb!R>H0B!LjqJd&gh0qiDNE)& Ic>LnO05|wRmjD0& literal 0 HcmV?d00001 diff --git a/e2e/support-chat.spec.ts b/e2e/support-chat.spec.ts new file mode 100644 index 000000000..83314ea0e --- /dev/null +++ b/e2e/support-chat.spec.ts @@ -0,0 +1,206 @@ +import { test, expect, Page, Route } from '@playwright/test'; +import { getCachedAuth } from './helpers/auth-cache'; + +/** + * E2E Visual Regression Tests: customer support chat + * + * Route: + * - /support/chat/:id (customer chat; file src/screens/chat.screen.tsx) + * + * Auth is REAL customer auth via getCachedAuth (same pattern as e2e/user-flows.spec.ts and + * e2e/support-issue-receiver-iban.spec.ts): the api must be reachable for the session token. + * + * Feature data is MOCKED with synthetic fixtures via page.route(...), so the baselines are + * deterministic and contain no production data. Only the support-issue endpoints used by the + * chat screen are intercepted; everything else (auth/user/settings) is passed through. + * + * Intercepted endpoints (base `/v1/` is prepended by useApi): + * - GET support/issue/:uid (loadSupportIssue) + * - GET support/issue/:uid?fromMessageId=… (sync poller — same body) + * - POST support/issue/:uid/message (submitMessage; not exercised in screenshots) + * + * Synthetic fixtures: fixed uid, fixed ISO dates, fake names — no production data. + * + * Baselines are produced by the assignee on macOS against a local api; this spec must remain + * runnable without committed PNGs (toHaveScreenshot creates them on first update run). + */ + +const ISSUE_UID = 'chat-e2e-uid-1'; + +const CUSTOMER = 'Customer'; +const SUPPORT_AUTHOR = 'Support Agent'; + +interface ChatMessageFixture { + id: number; + author?: string; + created: string; + message?: string; + fileName?: string; + file?: { file: string; type: string; size: number; url: string }; + status?: 'Sent' | 'Received' | 'Failed'; +} + +interface SupportIssueFixture { + uid: string; + state: string; + type: string; + reason: string; + name: string; + created: string; + messages: ChatMessageFixture[]; +} + +function issueWithMessages(messages: ChatMessageFixture[]): SupportIssueFixture { + return { + uid: ISSUE_UID, + state: 'Pending', + type: 'GenericIssue', + reason: 'Other', + name: 'E2E Chat Issue', + created: '2024-07-09T08:00:00.000Z', + messages, + }; +} + +// Two calendar days so the date separator is visible (first message + day change). +const THREAD_MESSAGES: ChatMessageFixture[] = [ + { + id: 1, + author: CUSTOMER, + created: '2024-07-09T10:00:00.000Z', + message: 'Hello, I need help with my transfer.', + status: 'Received', + }, + { + id: 2, + author: SUPPORT_AUTHOR, + created: '2024-07-10T09:15:00.000Z', + message: 'Thanks for reaching out. We are looking into it.', + }, +]; + +const ATTACHMENT_MESSAGES: ChatMessageFixture[] = [ + { + id: 10, + author: SUPPORT_AUTHOR, + created: '2024-07-10T11:00:00.000Z', + fileName: 'statement.pdf', + message: undefined, + }, +]; + +const STATUS_MESSAGES: ChatMessageFixture[] = [ + { + id: 20, + author: CUSTOMER, + created: '2024-07-10T12:00:00.000Z', + message: 'Still waiting — this is being sent…', + status: 'Sent', + }, + { + id: 21, + author: CUSTOMER, + created: '2024-07-10T12:01:00.000Z', + message: 'This message failed to send.', + status: 'Failed', + }, +]; + +const ISSUE_RE = /\/v1\/support\/issue\/[^/?]+(?:\?|$)/; +const MESSAGE_RE = /\/v1\/support\/issue\/[^/]+\/message(?:\?|$)/; + +async function installChatRoutes(page: Page, issue: SupportIssueFixture): Promise { + await page.route('**/v1/**', async (route: Route) => { + const request = route.request(); + const url = request.url(); + + if (request.method() === 'GET' && ISSUE_RE.test(url)) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(issue), + }); + return; + } + + if (request.method() === 'POST' && MESSAGE_RE.test(url)) { + await route.fulfill({ + status: 201, + contentType: 'application/json', + body: JSON.stringify({ + id: 999, + author: CUSTOMER, + created: new Date().toISOString(), + message: 'ok', + status: 'Received', + }), + }); + return; + } + + await route.continue(); + }); +} + +function chatUrl(token: string): string { + // Force English so text selectors stay stable regardless of the test account's language preference. + return `/support/chat/${ISSUE_UID}?session=${token}&lang=en`; +} + +test.describe('Support Chat - Visual Regression Tests', () => { + let token: string; + + test.beforeAll(async ({ request }) => { + const auth = await getCachedAuth(request, 'evm'); + token = auth.token; + }); + + test('thread with customer message, support reply and date separator', async ({ page }) => { + await installChatRoutes(page, issueWithMessages(THREAD_MESSAGES)); + + await page.goto(chatUrl(token)); + await page.waitForLoadState('networkidle'); + + await expect(page.getByText('Hello, I need help with my transfer.')).toBeVisible(); + await expect(page.getByText('Thanks for reaching out. We are looking into it.')).toBeVisible(); + await expect(page.getByText(SUPPORT_AUTHOR)).toBeVisible(); + + await page.waitForTimeout(1500); + await expect(page).toHaveScreenshot('support-chat-01-thread.png', { + fullPage: true, + maxDiffPixels: 5000, + }); + }); + + test('attachment message shows support author name', async ({ page }) => { + await installChatRoutes(page, issueWithMessages(ATTACHMENT_MESSAGES)); + + await page.goto(chatUrl(token)); + await page.waitForLoadState('networkidle'); + + await expect(page.getByText(SUPPORT_AUTHOR)).toBeVisible(); + await expect(page.getByText('statement.pdf')).toBeVisible(); + + await page.waitForTimeout(1500); + await expect(page).toHaveScreenshot('support-chat-02-attachment.png', { + fullPage: true, + maxDiffPixels: 5000, + }); + }); + + test('customer messages in sending and failed states', async ({ page }) => { + await installChatRoutes(page, issueWithMessages(STATUS_MESSAGES)); + + await page.goto(chatUrl(token)); + await page.waitForLoadState('networkidle'); + + await expect(page.getByText('Still waiting — this is being sent…')).toBeVisible(); + await expect(page.getByText('This message failed to send.')).toBeVisible(); + + await page.waitForTimeout(1500); + await expect(page).toHaveScreenshot('support-chat-03-status.png', { + fullPage: true, + maxDiffPixels: 5000, + }); + }); +}); diff --git a/scripts/handbook/metadata.json b/scripts/handbook/metadata.json index ffd402b3d..32b77be9a 100644 --- a/scripts/handbook/metadata.json +++ b/scripts/handbook/metadata.json @@ -127,6 +127,10 @@ "title": "Support-Dashboard Übersicht", "description": "Support-Dashboard-Übersicht und Statistiken." }, + "support-chat": { + "title": "Kunden-Support-Chat", + "description": "Kundenchat unter /support/chat: Verlauf mit Datumstrenner, Anhang mit Absendername, Zustände „wird gesendet“ und fehlgeschlagen." + }, "subpage": { "title": "Unterseiten", "description": "Buy-, Sell-, Swap- und Transaktions-Unterseiten." diff --git a/src/__tests__/chat.screen.test.tsx b/src/__tests__/chat.screen.test.tsx new file mode 100644 index 000000000..7f65d5a8f --- /dev/null +++ b/src/__tests__/chat.screen.test.tsx @@ -0,0 +1,1025 @@ +// Component tests for the customer support chat screen (src/screens/chat.screen.tsx). +// Mirrors the mock pattern of support-issue-receiver-iban.test.tsx: full object-literal +// factories for @dfx.swiss/react and @dfx.swiss/react-components (no requireActual). + +const mockNavigate = jest.fn(); +const mockLoadSupportIssue = jest.fn(); +const mockSetSync = jest.fn(); +const mockSubmitMessage = jest.fn(); +const mockLoadFileData = jest.fn(); +const mockGetTransactionByUid = jest.fn(); +const mockTranslate = jest.fn((_ns: string, key: string) => key); +const mockTranslateError = jest.fn((message: string) => message); +const mockSupportIssueUidGet = jest.fn(); +const mockSupportIssueUidSet = jest.fn(); +const mockUseLayoutOptions = jest.fn(); + +let mockSupportIssue: any; +let mockIsLoading = false; +let mockIssueUidParam: string | undefined; + +jest.mock('@dfx.swiss/react', () => { + const SupportMessageStatus = { + SENT: 'Sent', + RECEIVED: 'Received', + FAILED: 'Failed', + }; + + const SupportIssueType = { + GENERIC_ISSUE: 'GenericIssue', + TRANSACTION_ISSUE: 'TransactionIssue', + VERIFICATION_CALL: 'VerificationCall', + KYC_ISSUE: 'KycIssue', + LIMIT_REQUEST: 'LimitRequest', + PARTNERSHIP_REQUEST: 'PartnershipRequest', + NOTIFICATION_OF_CHANGES: 'NotificationOfChanges', + BUG_REPORT: 'BugReport', + }; + + const TransactionState = { + UNASSIGNED: 'Unassigned', + WAITING_FOR_PAYMENT: 'WaitingForPayment', + CREATED: 'Created', + PROCESSING: 'Processing', + COMPLETED: 'Completed', + FAILED: 'Failed', + }; + + const TransactionType = { + BUY: 'Buy', + SELL: 'Sell', + SWAP: 'Swap', + }; + + return { + SupportMessageStatus, + SupportIssueType, + SupportIssueReason: {}, + SupportIssueState: { PENDING: 'Pending' }, + TransactionState, + TransactionType, + Department: { + SUPPORT: 'Support', + COMPLIANCE: 'Compliance', + MARKETING: 'Marketing', + COOPERATION: 'Cooperation', + }, + UserRole: { + ADMIN: 'Admin', + SUPPORT: 'Support', + COMPLIANCE: 'Compliance', + MARKETING: 'Marketing', + CUSTODY: 'Custody', + }, + useSupportChatContext: () => ({ + supportIssue: mockSupportIssue, + isLoading: mockIsLoading, + loadSupportIssue: mockLoadSupportIssue, + setSync: mockSetSync, + submitMessage: mockSubmitMessage, + loadFileData: mockLoadFileData, + }), + useTransaction: () => ({ + getTransactionByUid: mockGetTransactionByUid, + }), + }; +}); + +jest.mock('@dfx.swiss/react-components', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const React = require('react'); + + return { + AssetIconVariant: { BTC: 'BTC', ETH: 'ETH', USDT: 'USDT' }, + DfxAssetIcon: ({ asset }: any) => React.createElement('div', { 'data-testid': `asset-icon-${asset}` }), + DfxIcon: () => React.createElement('div', { 'data-testid': 'dfx-icon-help' }), + IconSize: { LG: 'lg' }, + IconVariant: { HELP: 'help' }, + SpinnerSize: { LG: 'lg', MD: 'md' }, + SpinnerVariant: { LIGHT_MODE: 'light' }, + StyledCollapsible: ({ titleContent, children }: any) => + React.createElement('div', { 'data-testid': 'tx-collapsible' }, titleContent, children), + StyledLoadingSpinner: () => React.createElement('div', { 'data-testid': 'loading-spinner' }), + StyledVerticalStack: ({ children }: any) => React.createElement('div', null, children), + }; +}); + +jest.mock('src/contexts/settings.context', () => ({ + useSettingsContext: () => ({ + translate: (ns: string, key: string) => mockTranslate(ns, key), + translateError: (message: string) => mockTranslateError(message), + locale: 'en-US', + }), +})); + +jest.mock('src/hooks/layout-config.hook', () => ({ + // Capture options so onBack (chat.screen.tsx) can be exercised without the layout shell. + useLayoutOptions: (options: unknown) => mockUseLayoutOptions(options), +})); + +jest.mock('src/hooks/navigation.hook', () => ({ + useNavigation: () => ({ + navigate: mockNavigate, + }), +})); + +jest.mock('src/hooks/session-store.hook', () => ({ + useSessionStore: () => ({ + supportIssueUid: { + get: () => mockSupportIssueUidGet(), + set: (v: string) => mockSupportIssueUidSet(v), + remove: jest.fn(), + }, + }), +})); + +jest.mock('src/screens/transaction.screen', () => ({ + // eslint-disable-next-line @typescript-eslint/no-var-requires + TxInfo: () => require('react').createElement('div', { 'data-testid': 'tx-info' }), +})); + +jest.mock('src/config/labels', () => ({ + IssueTypeLabels: { + GenericIssue: 'Generic issue', + TransactionIssue: 'Transaction issue', + }, + toPaymentStateLabel: (state: string) => `label-${state}`, +})); + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { + ...actual, + useParams: () => ({ id: mockIssueUidParam }), + }; +}); + +import { SupportMessageStatus } from '@dfx.swiss/react'; +import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; +import ChatScreen from 'src/screens/chat.screen'; + +function makeMessage(overrides: Record = {}) { + return { + id: 1, + author: 'Customer', + created: new Date(2024, 6, 10, 14, 30), + message: 'Hello from customer', + status: SupportMessageStatus.RECEIVED, + ...overrides, + }; +} + +function makeIssue(overrides: Record = {}) { + return { + uid: 'issue-uid-1', + state: 'Pending', + type: 'GenericIssue', + reason: 'Other', + name: 'Test', + created: new Date(2024, 6, 10), + messages: [makeMessage()], + ...overrides, + }; +} + +function renderChat() { + return render(); +} + +describe('ChatScreen', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + mockIsLoading = false; + mockIssueUidParam = undefined; + mockSupportIssue = makeIssue(); + mockSupportIssueUidGet.mockReturnValue('issue-uid-1'); + mockLoadSupportIssue.mockResolvedValue(undefined); + mockSubmitMessage.mockResolvedValue(undefined); + mockLoadFileData.mockResolvedValue(undefined); + mockGetTransactionByUid.mockReset(); + mockTranslate.mockImplementation((_ns: string, key: string) => key); + mockTranslateError.mockImplementation((message: string) => message); + Element.prototype.scrollIntoView = jest.fn(); + }); + + afterEach(() => { + act(() => { + jest.runOnlyPendingTimers(); + }); + jest.useRealTimers(); + }); + + // --- Screen shell / routing --- + + it('shows a loading spinner while the issue is loading', () => { + mockIsLoading = true; + mockSupportIssue = undefined; + renderChat(); + expect(screen.getByTestId('loading-spinner')).toBeInTheDocument(); + }); + + it('shows a loading spinner when there is no support issue yet', () => { + mockSupportIssue = undefined; + renderChat(); + expect(screen.getByTestId('loading-spinner')).toBeInTheDocument(); + }); + + it('loads the session issue and enables sync when a stored uid is present', async () => { + renderChat(); + await waitFor(() => { + expect(mockSetSync).toHaveBeenCalledWith(true); + expect(mockLoadSupportIssue).toHaveBeenCalledWith('issue-uid-1'); + }); + }); + + it('navigates to the issue form when no session uid is available', async () => { + mockSupportIssueUidGet.mockReturnValue(undefined); + mockSupportIssue = undefined; + renderChat(); + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('/support/issue', { replace: true }); + }); + }); + + it('stores the route param uid and replaces the URL', async () => { + mockIssueUidParam = 'param-uid-9'; + mockSupportIssueUidGet.mockReturnValue(undefined); + mockSupportIssue = undefined; + renderChat(); + await waitFor(() => { + expect(mockSupportIssueUidSet).toHaveBeenCalledWith('param-uid-9'); + expect(mockNavigate).toHaveBeenCalledWith('/support/chat', { replace: true }); + }); + }); + + it('redirects to the issue form when loadSupportIssue rejects', async () => { + mockLoadSupportIssue.mockRejectedValue(new Error('not found')); + mockSupportIssue = undefined; + renderChat(); + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('/support/issue', { replace: true }); + }); + }); + + it('disables sync on unmount', async () => { + const { unmount } = renderChat(); + await waitFor(() => expect(mockSetSync).toHaveBeenCalledWith(true)); + unmount(); + expect(mockSetSync).toHaveBeenCalledWith(false); + }); + + it('registers an onBack handler that navigates to the tickets list', () => { + renderChat(); + expect(mockUseLayoutOptions).toHaveBeenCalled(); + const options = mockUseLayoutOptions.mock.calls[mockUseLayoutOptions.mock.calls.length - 1][0] as { + onBack?: () => void; + }; + expect(options.onBack).toEqual(expect.any(Function)); + options.onBack?.(); + expect(mockNavigate).toHaveBeenCalledWith('/support/tickets'); + }); + + it('scrolls to the latest message when messages are present', async () => { + renderChat(); + await waitFor(() => { + expect(Element.prototype.scrollIntoView).toHaveBeenCalled(); + }); + }); + + // --- B2 DFX colours --- + + it('styles the customer bubble with dfxBlue-800 and the support bubble with dfxGray-300', () => { + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ id: 1, author: 'Customer', message: 'Customer text' }), + makeMessage({ id: 2, author: 'Support Agent', message: 'Support text', status: undefined }), + ], + }); + renderChat(); + + const customerText = screen.getByText('Customer text'); + const supportText = screen.getByText('Support text'); + const customerBubble = customerText.closest('div.flex.flex-col.max-w-xs'); + const supportBubble = supportText.closest('div.flex.flex-col.max-w-xs'); + + expect(customerBubble).toHaveClass('bg-dfxBlue-800'); + expect(customerBubble).toHaveClass('text-white'); + expect(customerBubble?.className).not.toContain('24A1DE'); + expect(supportBubble).toHaveClass('bg-dfxGray-300'); + expect(supportBubble).toHaveClass('text-dfxBlue-800'); + }); + + it('uses ground-dependent timestamp colours (light on blue, dfxGray-800 on grey)', () => { + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ id: 1, author: 'Customer', message: 'Customer text', created: new Date(2024, 6, 10, 10, 15) }), + makeMessage({ + id: 2, + author: 'Support Agent', + message: 'Support text', + status: undefined, + created: new Date(2024, 6, 10, 10, 20), + }), + ], + }); + renderChat(); + + const customerBubble = screen.getByText('Customer text').closest('div.flex.flex-col.max-w-xs'); + const supportBubble = screen.getByText('Support text').closest('div.flex.flex-col.max-w-xs'); + const customerTime = customerBubble?.querySelector('.text-white\\/70, [class*="text-white"]'); + // Tailwind class is text-white/70 — check class string + const customerTimeRow = customerBubble?.querySelector('.text-xs.italic'); + const supportTimeRow = supportBubble?.querySelector('.text-xs.italic'); + expect(customerTimeRow?.className).toContain('text-white/70'); + expect(supportTimeRow?.className).toContain('text-dfxGray-800'); + expect(customerTime).toBeTruthy(); + }); + + it('renders the support author name in text-dfxBlue-400 (not red)', () => { + mockSupportIssue = makeIssue({ + messages: [makeMessage({ id: 1, author: 'Support Agent', message: 'Hi', status: undefined })], + }); + renderChat(); + const author = screen.getByText('Support Agent'); + expect(author).toHaveClass('text-dfxBlue-400'); + expect(author).not.toHaveClass('text-dfxRed-150'); + }); + + // --- B3 date separators --- + + it('renders a date separator above the first message', () => { + const now = new Date(); + mockSupportIssue = makeIssue({ + messages: [makeMessage({ created: now, message: 'First' })], + }); + renderChat(); + expect(screen.getByText('Today')).toBeInTheDocument(); + expect(mockTranslate).toHaveBeenCalledWith('screens/support', 'Today'); + }); + + it('renders a date separator when the calendar day changes, including same day-of-month across months', () => { + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ id: 1, created: new Date(2024, 6, 6, 12, 0), message: 'July' }), + makeMessage({ id: 2, created: new Date(2024, 7, 6, 12, 0), message: 'August' }), + ], + }); + renderChat(); + // Two separators — one for each calendar day (first message + day change). + const separators = document.querySelectorAll('.bg-dfxGray-300.text-dfxGray-700.rounded-full'); + expect(separators.length).toBe(2); + }); + + it('does not insert a second separator for messages on the same calendar day', () => { + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ id: 1, created: new Date(2024, 6, 6, 9, 0), message: 'Morning' }), + makeMessage({ id: 2, created: new Date(2024, 6, 6, 18, 0), message: 'Evening' }), + ], + }); + renderChat(); + const separators = document.querySelectorAll('.bg-dfxGray-300.text-dfxGray-700.rounded-full'); + expect(separators.length).toBe(1); + }); + + it('labels yesterday messages with Yesterday', () => { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + mockSupportIssue = makeIssue({ + messages: [makeMessage({ created: yesterday, message: 'Y-msg' })], + }); + renderChat(); + expect(screen.getByText('Yesterday')).toBeInTheDocument(); + expect(mockTranslate).toHaveBeenCalledWith('screens/support', 'Yesterday'); + }); + + // --- B4 delivery status only on own messages --- + + it('shows delivery status icons only on customer messages', () => { + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ id: 1, author: 'Customer', message: 'Mine', status: SupportMessageStatus.RECEIVED }), + makeMessage({ id: 2, author: 'Support Agent', message: 'Theirs', status: undefined }), + ], + }); + renderChat(); + expect(screen.getByTestId('msg-status-received')).toBeInTheDocument(); + const supportBubble = screen.getByText('Theirs').closest('div.flex.flex-col.max-w-xs'); + expect(within(supportBubble as HTMLElement).queryByTestId('msg-status-received')).not.toBeInTheDocument(); + expect(within(supportBubble as HTMLElement).queryByTestId('msg-status-sent')).not.toBeInTheDocument(); + expect(within(supportBubble as HTMLElement).queryByTestId('msg-status-failed')).not.toBeInTheDocument(); + }); + + it('shows the sending clock for SENT and the error icon for FAILED customer messages', () => { + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ id: 1, author: 'Customer', message: 'Sending', status: SupportMessageStatus.SENT }), + makeMessage({ id: 2, author: 'Customer', message: 'Failed', status: SupportMessageStatus.FAILED }), + ], + }); + renderChat(); + expect(screen.getByTestId('msg-status-sent')).toBeInTheDocument(); + expect(screen.getByTestId('msg-status-failed')).toBeInTheDocument(); + }); + + it('treats a missing author as a customer message (right-aligned, with status)', () => { + mockSupportIssue = makeIssue({ + messages: [makeMessage({ id: 1, author: undefined, message: 'No author', status: SupportMessageStatus.SENT })], + }); + renderChat(); + expect(screen.getByTestId('msg-status-sent')).toBeInTheDocument(); + const bubble = screen.getByText('No author').closest('div.flex.flex-col.max-w-xs'); + expect(bubble).toHaveClass('bg-dfxBlue-800'); + }); + + // --- B5 author visible with attachments --- + + it('shows the support author name above a file attachment', () => { + // B5: author must stay visible when a loaded DataFile is present (the old guard was `!file`). + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ + id: 1, + author: 'Support Agent', + message: undefined, + fileName: 'statement.pdf', + file: { + file: 'x', + type: 'application/pdf', + size: 1024, + url: 'https://example.com/statement.pdf', + }, + status: undefined, + }), + ], + }); + renderChat(); + expect(screen.getByText('Support Agent')).toBeInTheDocument(); + expect(screen.getByText('statement.pdf')).toBeInTheDocument(); + }); + + // --- B1 dead code gone --- + + it('does not render reply previews, reaction chips, or a bubble menu', () => { + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ + id: 1, + author: 'Customer', + message: 'With legacy fields', + replyTo: 99, + reactions: [{ emoji: '👍', users: ['a'] }], + }), + ], + }); + renderChat(); + expect(screen.queryByText('Reply')).not.toBeInTheDocument(); + expect(screen.queryByText('👍')).not.toBeInTheDocument(); + expect(document.body.textContent).not.toContain('Reply to'); + }); + + // --- InputComponent --- + + it('updates the controlled textarea value through onChange alone (onInput was removed as redundant)', () => { + // After removing the duplicate onInput handler, typing must still land in the field via onChange. + renderChat(); + const textarea = screen.getByPlaceholderText('Write a message...') as HTMLTextAreaElement; + fireEvent.change(textarea, { target: { value: 'Typed via change' } }); + expect(textarea.value).toBe('Typed via change'); + }); + + it('submits a typed message and clears the input', async () => { + renderChat(); + const textarea = screen.getByPlaceholderText('Write a message...'); + fireEvent.change(textarea, { target: { value: 'Need help' } }); + fireEvent.click(screen.getByRole('button', { name: '' })); // MdSend button has no accessible name + + // The send button is the one wrapping MdSend — click via parent of textarea row + const sendButtons = screen.getAllByRole('button'); + const sendButton = sendButtons[sendButtons.length - 1]; + fireEvent.click(sendButton); + + await waitFor(() => { + expect(mockSubmitMessage).toHaveBeenCalledWith('Need help', []); + }); + }); + + it('does not submit when the input is empty', () => { + renderChat(); + const sendButtons = screen.getAllByRole('button'); + fireEvent.click(sendButtons[sendButtons.length - 1]); + expect(mockSubmitMessage).not.toHaveBeenCalled(); + }); + + it('shows a length error above 4000 characters and blocks send', async () => { + renderChat(); + const textarea = screen.getByPlaceholderText('Write a message...'); + const long = 'x'.repeat(4001); + fireEvent.change(textarea, { target: { value: long } }); + expect(mockTranslateError).toHaveBeenCalledWith('message_length'); + expect(screen.getByText('message_length')).toBeInTheDocument(); + + const sendButtons = screen.getAllByRole('button'); + fireEvent.click(sendButtons[sendButtons.length - 1]); + expect(mockSubmitMessage).not.toHaveBeenCalled(); + + // Clearing the error when value shrinks again + fireEvent.change(textarea, { target: { value: 'ok' } }); + expect(screen.queryByText('message_length')).not.toBeInTheDocument(); + }); + + it('sends on Enter and inserts a newline on Shift+Enter', async () => { + renderChat(); + const textarea = screen.getByPlaceholderText('Write a message...') as HTMLTextAreaElement; + + // 234:4 false — non-Enter keys must not send or alter the value. + fireEvent.change(textarea, { target: { value: 'stay' } }); + fireEvent.keyDown(textarea, { key: 'a' }); + expect(textarea.value).toBe('stay'); + expect(mockSubmitMessage).not.toHaveBeenCalled(); + + // 237:38 false — Shift+Enter on an empty field keeps the empty string branch. + fireEvent.change(textarea, { target: { value: '' } }); + fireEvent.keyDown(textarea, { key: 'Enter', shiftKey: true }); + expect(textarea.value).toBe(''); + + fireEvent.change(textarea, { target: { value: 'Line1' } }); + fireEvent.keyDown(textarea, { key: 'Enter', shiftKey: true }); + expect(textarea.value).toContain('\n'); + + fireEvent.change(textarea, { target: { value: 'Send me' } }); + fireEvent.keyDown(textarea, { key: 'Enter', shiftKey: false }); + await waitFor(() => { + expect(mockSubmitMessage).toHaveBeenCalledWith('Send me', []); + }); + }); + + it('attaches selected files and allows removing them before send', async () => { + renderChat(); + const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement; + const file = new File(['hello'], 'note.pdf', { type: 'application/pdf' }); + + // 223:4 false — empty / cancelled file pick must not add chips. + await act(async () => { + fireEvent.change(fileInput, { target: { files: [] } }); + }); + expect(screen.queryByText('note.pdf')).not.toBeInTheDocument(); + + await act(async () => { + fireEvent.change(fileInput, { target: { files: [file] } }); + }); + expect(screen.getByText('note.pdf')).toBeInTheDocument(); + + // Chip layout: paperclip svg + name + close svg — click the last svg (MdOutlineClose). + const name = screen.getByText('note.pdf'); + const chip = name.parentElement as HTMLElement; + const svgs = chip.querySelectorAll('svg'); + expect(svgs.length).toBeGreaterThanOrEqual(2); + fireEvent.click(svgs[svgs.length - 1]); + expect(screen.queryByText('note.pdf')).not.toBeInTheDocument(); + }); + + it('submits selected files together with the message text', async () => { + renderChat(); + const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement; + const file = new File(['hello'], 'doc.pdf', { type: 'application/pdf' }); + await act(async () => { + fireEvent.change(fileInput, { target: { files: [file] } }); + }); + + const textarea = screen.getByPlaceholderText('Write a message...'); + fireEvent.change(textarea, { target: { value: 'With file' } }); + const sendButtons = screen.getAllByRole('button'); + fireEvent.click(sendButtons[sendButtons.length - 1]); + + await waitFor(() => { + expect(mockSubmitMessage).toHaveBeenCalled(); + const [msg, files] = mockSubmitMessage.mock.calls[0]; + expect(msg).toBe('With file'); + expect(files).toHaveLength(1); + expect(files[0].name).toBe('doc.pdf'); + }); + }); + + // --- ChatBubbleFileEmbed --- + + it('triggers loadFileData when an unloaded attachment is clicked and shows errors', async () => { + mockLoadFileData.mockRejectedValue(new Error('boom')); + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ + id: 42, + author: 'Support Agent', + message: undefined, + fileName: 'report.pdf', + file: undefined, + status: undefined, + }), + ], + }); + renderChat(); + fireEvent.click(screen.getByText('report.pdf')); + await waitFor(() => { + expect(mockLoadFileData).toHaveBeenCalledWith(42); + expect(screen.getByText('Download failed')).toBeInTheDocument(); + }); + }); + + it('opens a loaded document attachment in a new tab', () => { + const openSpy = jest.spyOn(window, 'open').mockImplementation(() => null); + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ + id: 5, + author: 'Support Agent', + message: undefined, + fileName: 'invoice.pdf', + file: { + file: 'x', + type: 'application/pdf', + size: 2048, + url: 'https://example.com/invoice.pdf', + }, + status: undefined, + }), + ], + }); + renderChat(); + fireEvent.click(screen.getByText('invoice.pdf')); + expect(openSpy).toHaveBeenCalledWith('https://example.com/invoice.pdf', '_blank'); + openSpy.mockRestore(); + }); + + it('opens and closes an image lightbox for a loaded image attachment', () => { + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ + id: 6, + author: 'Support Agent', + message: undefined, + fileName: 'photo.png', + file: { + file: 'x', + type: 'image/png', + size: 4096, + url: 'https://example.com/photo.png', + }, + status: undefined, + }), + ], + }); + renderChat(); + const thumbs = screen.getAllByAltText('photo.png'); + fireEvent.click(thumbs[0]); + // Lightbox image appears (second img with same alt) + expect(screen.getAllByAltText('photo.png').length).toBeGreaterThanOrEqual(2); + + // Close via the top-right button + const closeButtons = screen.getAllByRole('button'); + const lightboxClose = closeButtons.find((b) => b.className.includes('absolute')); + expect(lightboxClose).toBeTruthy(); + fireEvent.click(lightboxClose as HTMLElement); + expect(screen.getAllByAltText('photo.png')).toHaveLength(1); + }); + + it('stops propagation when the lightbox backdrop is clicked', () => { + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ + id: 7, + author: 'Support Agent', + message: undefined, + fileName: 'shot.jpg', + file: { + file: 'x', + type: 'image/jpeg', + size: 1024, + url: 'https://example.com/shot.jpg', + }, + status: undefined, + }), + ], + }); + renderChat(); + fireEvent.click(screen.getAllByAltText('shot.jpg')[0]); + const backdrop = document.querySelector('.fixed.inset-0'); + expect(backdrop).toBeTruthy(); + fireEvent.click(backdrop as Element); + // Still open (stopPropagation only — no close on backdrop) + expect(document.querySelector('.fixed.inset-0')).toBeTruthy(); + }); + + // --- TransactionComponent --- + + it('renders a linked transaction collapsible with asset icon for a completed buy', async () => { + mockGetTransactionByUid.mockResolvedValue({ + type: 'Buy', + state: 'Completed', + inputAsset: 'CHF', + outputAsset: 'BTC', + inputAmount: 100, + outputAmount: 0.01, + }); + mockSupportIssue = makeIssue({ + transaction: { uid: 'tx-1', url: 'https://example.com/tx' }, + }); + renderChat(); + await waitFor(() => { + expect(mockGetTransactionByUid).toHaveBeenCalledWith('tx-1'); + expect(screen.getByTestId('tx-collapsible')).toBeInTheDocument(); + expect(screen.getByTestId('asset-icon-BTC')).toBeInTheDocument(); + expect(screen.getByTestId('tx-info')).toBeInTheDocument(); + }); + }); + + it('uses the help icon when no matching asset icon is found', async () => { + mockGetTransactionByUid.mockResolvedValue({ + type: 'Buy', + state: 'Completed', + inputAsset: 'UNKNOWN', + outputAsset: 'UNKNOWN2', + inputAmount: 1, + outputAmount: 2, + }); + mockSupportIssue = makeIssue({ + transaction: { uid: 'tx-2', url: 'https://example.com/tx' }, + }); + renderChat(); + await waitFor(() => { + expect(screen.getByTestId('dfx-icon-help')).toBeInTheDocument(); + }); + }); + + it('prefers inputAsset for sell transactions and strips a leading d from asset codes', async () => { + mockGetTransactionByUid.mockResolvedValue({ + type: 'Sell', + state: 'Completed', + inputAsset: 'dBTC', + outputAsset: 'CHF', + inputAmount: 0.5, + outputAmount: 20000, + }); + mockSupportIssue = makeIssue({ + transaction: { uid: 'tx-3', url: 'https://example.com/tx' }, + }); + renderChat(); + await waitFor(() => { + expect(screen.getByTestId('asset-icon-BTC')).toBeInTheDocument(); + }); + }); + + it('marks unassigned transactions without an asset icon and with the red state class', async () => { + mockGetTransactionByUid.mockResolvedValue({ + type: 'Buy', + state: 'Unassigned', + inputAsset: 'BTC', + outputAsset: 'BTC', + inputAmount: 1, + outputAmount: 1, + }); + mockSupportIssue = makeIssue({ + transaction: { uid: 'tx-4', url: 'https://example.com/tx' }, + }); + renderChat(); + await waitFor(() => { + expect(screen.getByTestId('dfx-icon-help')).toBeInTheDocument(); + expect(screen.getByText('label-Unassigned')).toHaveClass('text-dfxRed-100'); + }); + }); + + it('shows a transaction loading spinner while the fetch is in flight', async () => { + let resolveTx!: (value: unknown) => void; + mockGetTransactionByUid.mockImplementation( + () => + new Promise((resolve) => { + resolveTx = resolve; + }), + ); + mockSupportIssue = makeIssue({ + transaction: { uid: 'tx-load', url: 'https://example.com/tx' }, + }); + renderChat(); + expect(await screen.findByText('Loading transaction...')).toBeInTheDocument(); + // Spinner next to the loading label (screen shell spinner is gone once the issue is loaded). + expect(screen.getAllByTestId('loading-spinner').length).toBeGreaterThanOrEqual(1); + + await act(async () => { + resolveTx({ + type: 'Buy', + state: 'Completed', + inputAsset: 'CHF', + outputAsset: 'BTC', + inputAmount: 1, + outputAmount: 0.001, + }); + await Promise.resolve(); + }); + await waitFor(() => { + expect(screen.queryByText('Loading transaction...')).not.toBeInTheDocument(); + expect(screen.getByTestId('tx-collapsible')).toBeInTheDocument(); + }); + }); + + it('shows a transaction loading state then an error message when the fetch fails', async () => { + mockGetTransactionByUid.mockRejectedValue({ message: 'tx gone' }); + mockSupportIssue = makeIssue({ + transaction: { uid: 'tx-err', url: 'https://example.com/tx' }, + }); + renderChat(); + await waitFor(() => { + expect(screen.getByText('tx gone')).toBeInTheDocument(); + expect(screen.getByText('tx gone')).toHaveClass('text-dfxRed-100'); + }); + }); + + it('falls back to Unknown error when the transaction fetch rejection has no message', async () => { + mockGetTransactionByUid.mockRejectedValue({}); + mockSupportIssue = makeIssue({ + transaction: { uid: 'tx-err2', url: 'https://example.com/tx' }, + }); + renderChat(); + await waitFor(() => { + expect(screen.getByText('Unknown error')).toBeInTheDocument(); + }); + }); + + it('renders amount arrow only when both input and output assets are present', async () => { + mockGetTransactionByUid.mockResolvedValue({ + type: 'Buy', + state: 'Completed', + inputAsset: 'CHF', + outputAsset: 'ETH', + inputAmount: 50, + outputAmount: 0.02, + }); + mockSupportIssue = makeIssue({ + transaction: { uid: 'tx-5', url: 'https://example.com/tx' }, + }); + renderChat(); + await waitFor(() => { + expect(screen.getByText(/50 CHF/)).toBeInTheDocument(); + expect(screen.getByText(/→/)).toBeInTheDocument(); + expect(screen.getByText(/0.02 ETH/)).toBeInTheDocument(); + }); + }); + + it('renders asset labels with empty amounts when amounts are missing', async () => { + // 164:36 inputAmount ?? '' and 166:37 outputAmount ?? '' — assets present, amounts undefined. + mockGetTransactionByUid.mockResolvedValue({ + type: 'Buy', + state: 'Completed', + inputAsset: 'CHF', + outputAsset: 'ETH', + inputAmount: undefined, + outputAmount: undefined, + }); + mockSupportIssue = makeIssue({ + transaction: { uid: 'tx-amounts-empty', url: 'https://example.com/tx' }, + }); + renderChat(); + await waitFor(() => { + expect(screen.getByTestId('tx-collapsible')).toBeInTheDocument(); + }); + const title = screen.getByTestId('tx-collapsible'); + // Leading space from `${''} ${asset}` is intentional; match asset tokens and the arrow. + expect(title.textContent).toMatch(/CHF/); + expect(title.textContent).toMatch(/→/); + expect(title.textContent).toMatch(/ETH/); + expect(title.textContent).not.toMatch(/\d+\s*CHF/); + expect(title.textContent).not.toMatch(/\d+\s*ETH/); + }); + + it('renders only the available asset side when the other is missing', async () => { + // Output-only side (existing path). + mockGetTransactionByUid.mockResolvedValue({ + type: 'Buy', + state: 'Completed', + inputAsset: undefined, + outputAsset: 'ETH', + inputAmount: undefined, + outputAmount: 1, + }); + mockSupportIssue = makeIssue({ + transaction: { uid: 'tx-6', url: 'https://example.com/tx' }, + }); + const { unmount } = renderChat(); + await waitFor(() => { + expect(screen.getByText(/1 ETH/)).toBeInTheDocument(); + expect(screen.queryByText(/→/)).not.toBeInTheDocument(); + }); + unmount(); + + // 166:17 else — inputAsset only, no outputAsset (and no arrow). + mockGetTransactionByUid.mockResolvedValue({ + type: 'Buy', + state: 'Completed', + inputAsset: 'CHF', + outputAsset: undefined, + inputAmount: 10, + outputAmount: undefined, + }); + mockSupportIssue = makeIssue({ + transaction: { uid: 'tx-6b', url: 'https://example.com/tx' }, + }); + renderChat(); + await waitFor(() => { + expect(screen.getByText(/10 CHF/)).toBeInTheDocument(); + expect(screen.queryByText(/→/)).not.toBeInTheDocument(); + }); + }); + + it('does not show a second author header when consecutive messages share the same author', () => { + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ id: 1, author: 'Support Agent', message: 'One', status: undefined }), + makeMessage({ + id: 2, + author: 'Support Agent', + message: undefined, + fileName: 'follow-up.pdf', + status: undefined, + }), + ], + }); + renderChat(); + expect(screen.getAllByText('Support Agent')).toHaveLength(1); + // 348:12 else — same author + attachment: hasHeader false and hasFile true → no pt-1.5. + const fileBubble = screen.getByText('follow-up.pdf').closest('div.flex.flex-col.max-w-xs'); + expect(fileBubble).toBeTruthy(); + expect(fileBubble?.className).not.toContain('pt-1.5'); + }); + + it('shows Downloading… while loadFileData is in flight', async () => { + let resolveLoad!: () => void; + mockLoadFileData.mockImplementation( + () => + new Promise((resolve) => { + resolveLoad = resolve; + }), + ); + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ + id: 8, + author: 'Support Agent', + message: undefined, + fileName: 'pending.pdf', + file: undefined, + status: undefined, + }), + ], + }); + renderChat(); + fireEvent.click(screen.getByText('pending.pdf')); + expect(await screen.findByText('Downloading...')).toBeInTheDocument(); + await act(async () => { + resolveLoad(); + await Promise.resolve(); + }); + }); + + it('shows Document · size for a loaded non-image attachment', () => { + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ + id: 9, + author: 'Support Agent', + message: undefined, + fileName: 'sheet.xlsx', + file: { + file: 'x', + type: 'application/vnd.ms-excel', + size: 1024, + url: 'https://example.com/sheet.xlsx', + }, + status: undefined, + }), + ], + }); + renderChat(); + expect(screen.getByText(/Document/)).toBeInTheDocument(); + }); + + it('falls back to Document for an unknown MIME type prefix', () => { + mockSupportIssue = makeIssue({ + messages: [ + makeMessage({ + id: 10, + author: 'Support Agent', + message: undefined, + fileName: 'blob.bin', + file: { + file: 'x', + type: 'application/octet-stream', + size: 10, + url: 'https://example.com/blob.bin', + }, + status: undefined, + }), + ], + }); + renderChat(); + expect(screen.getByText(/Document/)).toBeInTheDocument(); + }); +}); diff --git a/src/__tests__/support-helpers.test.ts b/src/__tests__/support-helpers.test.ts index 6d3c46660..23c46f3b6 100644 --- a/src/__tests__/support-helpers.test.ts +++ b/src/__tests__/support-helpers.test.ts @@ -1,7 +1,12 @@ // Mock the label config so support-helpers does not pull in the real @dfx.swiss/react label tables. +// Non-empty maps so typeLabel/reasonLabel hit both the mapped and the fallback branch. jest.mock('src/config/labels', () => ({ - IssueReasonLabels: {}, - IssueTypeLabels: {}, + IssueTypeLabels: { + GenericIssue: 'Generic issue', + }, + IssueReasonLabels: { + Other: 'Other reason', + }, })); // Mock @dfx.swiss/react to avoid ES module issues in jest. @@ -22,7 +27,34 @@ jest.mock('@dfx.swiss/react', () => ({ })); import { Department, UserRole } from '@dfx.swiss/react'; -import { visibleDepartmentsForRole } from 'src/util/support-helpers'; +import { + isSameCalendarDay, + reasonLabel, + relativeDayKey, + shouldShowDateSeparator, + typeLabel, + visibleDepartmentsForRole, +} from 'src/util/support-helpers'; + +describe('typeLabel', () => { + it('returns the mapped label for a known issue type', () => { + expect(typeLabel('GenericIssue')).toBe('Generic issue'); + }); + + it('falls back to the raw type string when no label is configured', () => { + expect(typeLabel('UnknownType')).toBe('UnknownType'); + }); +}); + +describe('reasonLabel', () => { + it('returns the mapped label for a known issue reason', () => { + expect(reasonLabel('Other')).toBe('Other reason'); + }); + + it('falls back to the raw reason string when no label is configured', () => { + expect(reasonLabel('UnknownReason')).toBe('UnknownReason'); + }); +}); describe('visibleDepartmentsForRole', () => { it('limits support to the support department', () => { @@ -49,3 +81,54 @@ describe('visibleDepartmentsForRole', () => { expect(visibleDepartmentsForRole(UserRole.CUSTODY)).toEqual([]); }); }); + +describe('isSameCalendarDay', () => { + it('treats two times on the same local day as equal', () => { + expect(isSameCalendarDay(new Date(2024, 6, 6, 8, 0), new Date(2024, 6, 6, 23, 59))).toBe(true); + }); + + it('does not treat the same day-of-month in different months as equal', () => { + // B3 regression: getDate()-only comparison would wrongly return true here. + expect(isSameCalendarDay(new Date(2024, 6, 6, 12, 0), new Date(2024, 7, 6, 12, 0))).toBe(false); + }); + + it('does not treat the same month/day in different years as equal', () => { + expect(isSameCalendarDay(new Date(2023, 6, 6), new Date(2024, 6, 6))).toBe(false); + }); + + it('accepts ISO string inputs', () => { + const a = new Date(2024, 0, 15, 10, 0); + const b = new Date(2024, 0, 15, 18, 0); + expect(isSameCalendarDay(a.toISOString(), b.toISOString())).toBe(true); + }); +}); + +describe('shouldShowDateSeparator', () => { + it('always shows a separator above the first message', () => { + expect(shouldShowDateSeparator(new Date(2024, 6, 6), undefined)).toBe(true); + }); + + it('hides the separator when the previous message is the same calendar day', () => { + expect(shouldShowDateSeparator(new Date(2024, 6, 6, 18, 0), new Date(2024, 6, 6, 9, 0))).toBe(false); + }); + + it('shows the separator when the calendar day changes (including same day-of-month across months)', () => { + expect(shouldShowDateSeparator(new Date(2024, 7, 6, 9, 0), new Date(2024, 6, 6, 18, 0))).toBe(true); + }); +}); + +describe('relativeDayKey', () => { + const now = new Date(2024, 6, 10, 15, 0, 0); // 10 Jul 2024 + + it('returns Today for the current calendar day', () => { + expect(relativeDayKey(new Date(2024, 6, 10, 1, 0), now)).toBe('Today'); + }); + + it('returns Yesterday for the previous calendar day', () => { + expect(relativeDayKey(new Date(2024, 6, 9, 23, 0), now)).toBe('Yesterday'); + }); + + it('returns null for older days', () => { + expect(relativeDayKey(new Date(2024, 6, 8, 12, 0), now)).toBeNull(); + }); +}); diff --git a/src/screens/chat.screen.tsx b/src/screens/chat.screen.tsx index 90b78c327..ae5e55c98 100644 --- a/src/screens/chat.screen.tsx +++ b/src/screens/chat.screen.tsx @@ -22,33 +22,28 @@ import { StyledVerticalStack, } from '@dfx.swiss/react-components'; import { useEffect, useRef, useState } from 'react'; -import { BsReply } from 'react-icons/bs'; import { HiOutlineDownload, HiOutlinePaperClip } from 'react-icons/hi'; -import { MdAccessTime, MdErrorOutline, MdOutlineCancel, MdOutlineClose, MdSend } from 'react-icons/md'; +import { MdAccessTime, MdErrorOutline, MdOutlineClose, MdSend } from 'react-icons/md'; import { RiCheckFill } from 'react-icons/ri'; import { useParams } from 'react-router-dom'; import { IssueTypeLabels, toPaymentStateLabel } from 'src/config/labels'; import { useSettingsContext } from 'src/contexts/settings.context'; import { useNavigation } from 'src/hooks/navigation.hook'; import { useSessionStore } from 'src/hooks/session-store.hook'; +import { relativeDayKey, shouldShowDateSeparator } from 'src/util/support-helpers'; import { blankedAddress, formatBytes, formatSwissTime } from 'src/util/utils'; import { useLayoutOptions } from '../hooks/layout-config.hook'; import { TxInfo } from './transaction.screen'; -const emojiSet = ['👍', '❤️', '😂', '😮', '😢', '👏']; - export default function ChatScreen(): JSX.Element { const { navigate } = useNavigation(); const { translate } = useSettingsContext(); - const { supportIssue, isLoading, loadSupportIssue, handleEmojiClick, setSync } = useSupportChatContext(); + const { supportIssue, isLoading, loadSupportIssue, setSync } = useSupportChatContext(); const { supportIssueUid: supportIssueUidStore } = useSessionStore(); const { id: issueUidParam } = useParams(); const messagesEndRef = useRef(null); - const [clickedMessage, setClickedMessage] = useState(); - const [replyToMessage, setReplyToMessage] = useState(); - const [menuPosition, _setMenuPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); const [sessionUid, setSessionUid] = useState(() => { return supportIssueUidStore.get() || ''; }); @@ -76,24 +71,6 @@ export default function ChatScreen(): JSX.Element { } }, [supportIssue?.messages.length]); - function onChatBubbleClick(e?: React.MouseEvent, _message?: SupportMessage) { - if (!e) { - setClickedMessage(undefined); - return; - } - - e.stopPropagation(); - // TODO: Uncomment to enable replies & reactions (feature not yet available) - // setMenuPosition({ top: e.clientY, left: e.clientX }); - // setClickedMessage(message); - } - - function onEmojiClick(messageId: number, emoji: string, e?: React.MouseEvent) { - e?.stopPropagation(); - handleEmojiClick(messageId, emoji); - setClickedMessage(undefined); - } - useLayoutOptions({ title: supportIssue && translate('screens/support', IssueTypeLabels[supportIssue?.type]), onBack: () => navigate('/support/tickets'), @@ -113,39 +90,17 @@ export default function ChatScreen(): JSX.Element { {supportIssue.messages.map((message, index) => { const prevSender = index > 0 ? supportIssue.messages[index - 1].author : null; const isNewSender = prevSender !== message.author; + const previousCreated = index > 0 ? supportIssue.messages[index - 1].created : undefined; return (

- {index > 0 && - new Date(message.created).getDate() !== - new Date(supportIssue.messages[index - 1].created).getDate() && ( - - )} - m.id === message.replyTo) : undefined - } - onEmojiClick={onEmojiClick} - onClick={(e) => onChatBubbleClick(e, message)} - {...message} - /> + {shouldShowDateSeparator(message.created, previousCreated) && } +
); })}
- - {clickedMessage?.id !== undefined && ( - { - setReplyToMessage(message); - setClickedMessage(undefined); - }} - onEmojiClick={onEmojiClick} - /> - )} + )} @@ -227,27 +182,25 @@ interface DateTagProps { } function DateTag({ date }: DateTagProps): JSX.Element { - const { locale } = useSettingsContext(); + const { locale, translate } = useSettingsContext(); + + const relativeKey = relativeDayKey(date); + const label = relativeKey + ? translate('screens/support', relativeKey) + : new Date(date).toLocaleDateString([locale, 'en-US'], { + weekday: 'short', + month: 'short', + day: 'numeric', + }); return (
-
- {new Date(date).toLocaleDateString([locale, 'en-US'], { - weekday: 'short', - month: 'short', - day: 'numeric', - })} -
+
{label}
); } -interface InputComponentProps { - replyToMessage?: SupportMessage; - setReplyToMessage: React.Dispatch>; -} - -function InputComponent({ replyToMessage, setReplyToMessage }: InputComponentProps): JSX.Element { +function InputComponent(): JSX.Element { const { translate, translateError } = useSettingsContext(); const { submitMessage } = useSupportChatContext(); const [inputValue, setInputValue] = useState(); @@ -257,11 +210,10 @@ function InputComponent({ replyToMessage, setReplyToMessage }: InputComponentPro function handleSend() { if (!inputValue || error) return; - submitMessage(inputValue, selectedFiles, replyToMessage); + submitMessage(inputValue, selectedFiles); setInputValue(''); setSelectedFiles([]); - setReplyToMessage(undefined); return; } @@ -303,31 +255,6 @@ function InputComponent({ replyToMessage, setReplyToMessage }: InputComponentPro return (
- {replyToMessage && ( -
-
-
- {replyToMessage.file && ( - Media - )} -
-
- -

{`Reply to ${replyToMessage.author}`}

-
-

{replyToMessage.message ?? 'Media'}

-
-
- -
- )} {selectedFiles.length > 0 && (
{selectedFiles.map((file, index) => ( @@ -378,7 +305,6 @@ function InputComponent({ replyToMessage, setReplyToMessage }: InputComponentPro