From 4f26ca1a7b0e4fe7eceeaab503b223d73871c95e Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Mon, 20 Nov 2023 12:49:38 +0000 Subject: [PATCH] feat: Base implementation of v2 of avatars (#12369) * feat: Base implementation of v2 of avatars * Make avatarUrl and logoUrl entirely optional * Made necessary backwards compat changes * fix: type errors * Fix: OG image * fix types * Consistency with other behaviour, ux tweak --------- Co-authored-by: Peer Richelsen --- apps/web/components/ui/avatar/UserAvatar.tsx | 4 +- apps/web/pages/[user].tsx | 7 +- apps/web/pages/api/avatar/[uuid].ts | 61 ++++++++++++++ .../web/pages/settings/my-account/profile.tsx | 24 +++--- apps/web/playwright/fixtures/cal.png | Bin 0 -> 43681 bytes .../playwright/settings/upload-avatar.e2e.ts | 56 +++++++++++++ .../test/handlers/requestReschedule.test.ts | 1 + .../settings/layouts/SettingsLayout.tsx | 3 +- packages/lib/getAvatarUrl.ts | 23 ++++-- packages/lib/test/builder.ts | 1 + .../migration.sql | 19 +++++ packages/prisma/schema.prisma | 16 ++++ .../server/middlewares/sessionMiddleware.ts | 1 + .../routers/loggedInViewer/avatar.handler.ts | 11 ++- .../routers/loggedInViewer/me.handler.ts | 1 + .../loggedInViewer/updateProfile.handler.ts | 77 +++++++++--------- packages/ui/components/dialog/Dialog.tsx | 6 +- .../image-uploader/ImageUploader.tsx | 16 +++- 18 files changed, 258 insertions(+), 69 deletions(-) create mode 100644 apps/web/pages/api/avatar/[uuid].ts create mode 100644 apps/web/playwright/fixtures/cal.png create mode 100644 apps/web/playwright/settings/upload-avatar.e2e.ts create mode 100644 packages/prisma/migrations/20231114090318_add_avatar_url/migration.sql diff --git a/apps/web/components/ui/avatar/UserAvatar.tsx b/apps/web/components/ui/avatar/UserAvatar.tsx index 63fa676676..a542fc3d9f 100644 --- a/apps/web/components/ui/avatar/UserAvatar.tsx +++ b/apps/web/components/ui/avatar/UserAvatar.tsx @@ -14,6 +14,6 @@ type UserAvatarProps = Omit, "alt" | "imageS * It is aware of the user's organization to correctly show the avatar from the correct URL */ export function UserAvatar(props: UserAvatarProps) { - const { user, previewSrc, ...rest } = props; - return ; + const { user, previewSrc = getUserAvatarUrl(user), ...rest } = props; + return ; } diff --git a/apps/web/pages/[user].tsx b/apps/web/pages/[user].tsx index 5a4f46eed0..46a0e5ffa5 100644 --- a/apps/web/pages/[user].tsx +++ b/apps/web/pages/[user].tsx @@ -82,7 +82,7 @@ export function UserPage(props: InferGetServerSidePropsType[]; + users: Pick[]; themeBasis: string | null; markdownStrippedBio: string; safeBio: string; @@ -295,6 +295,7 @@ export const getServerSideProps: GetServerSideProps = async (cont metadata: true, brandColor: true, darkBrandColor: true, + avatarUrl: true, organizationId: true, organization: { select: { @@ -363,6 +364,7 @@ export const getServerSideProps: GetServerSideProps = async (cont image: user.avatar, theme: user.theme, brandColor: user.brandColor, + avatarUrl: user.avatarUrl, darkBrandColor: user.darkBrandColor, allowSEOIndexing: user.allowSEOIndexing ?? true, username: user.username, @@ -397,6 +399,7 @@ export const getServerSideProps: GetServerSideProps = async (cont name: user.name, username: user.username, bio: user.bio, + avatarUrl: user.avatarUrl, away: user.away, verified: user.verified, })), diff --git a/apps/web/pages/api/avatar/[uuid].ts b/apps/web/pages/api/avatar/[uuid].ts new file mode 100644 index 0000000000..2e8a1dfda3 --- /dev/null +++ b/apps/web/pages/api/avatar/[uuid].ts @@ -0,0 +1,61 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; + +import { AVATAR_FALLBACK } from "@calcom/lib/constants"; +import prisma from "@calcom/prisma"; + +const querySchema = z.object({ + uuid: z.string().transform((objectKey) => objectKey.split(".")[0]), +}); + +const handleValidationError = (res: NextApiResponse, error: z.ZodError): void => { + const errors = error.errors.map((err) => ({ + path: err.path.join("."), + errorCode: `error.validation.${err.code}`, + })); + + res.status(400).json({ + message: "VALIDATION_ERROR", + errors, + }); +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const result = querySchema.safeParse(req.query); + if (!result.success) { + return handleValidationError(res, result.error); + } + + const { uuid: objectKey } = result.data; + + let img; + try { + const { data } = await prisma.avatar.findUniqueOrThrow({ + where: { + objectKey, + }, + select: { + data: true, + }, + }); + img = data; + } catch (e) { + // If anything goes wrong or avatar is not found, use default avatar + res.writeHead(302, { + Location: AVATAR_FALLBACK, + }); + + res.end(); + return; + } + + const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", ""); + const imageResp = Buffer.from(decoded, "base64"); + + res.writeHead(200, { + "Content-Type": "image/png", + "Content-Length": imageResp.length, + }); + + res.end(imageResp); +} diff --git a/apps/web/pages/settings/my-account/profile.tsx b/apps/web/pages/settings/my-account/profile.tsx index 99e2418d80..6bd871bf7c 100644 --- a/apps/web/pages/settings/my-account/profile.tsx +++ b/apps/web/pages/settings/my-account/profile.tsx @@ -9,8 +9,8 @@ import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; import OrganizationMemberAvatar from "@calcom/features/ee/organizations/components/OrganizationMemberAvatar"; import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import checkIfItFallbackImage from "@calcom/lib/checkIfItFallbackImage"; import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { md } from "@calcom/lib/markdownIt"; import turndown from "@calcom/lib/turndownService"; @@ -82,19 +82,12 @@ const ProfileView = () => { const { t } = useLocale(); const utils = trpc.useContext(); const { update } = useSession(); + const { data: user, isLoading } = trpc.viewer.me.useQuery(); - const [fetchedImgSrc, setFetchedImgSrc] = useState(""); - - const { data: user, isLoading } = trpc.viewer.me.useQuery(undefined, { - onSuccess: async (userData) => { - try { - const res = await fetch(userData.avatar); - if (res.url) setFetchedImgSrc(res.url); - } catch (err) { - setFetchedImgSrc(""); - } - }, + const { data: avatarData } = trpc.viewer.avatar.useQuery(undefined, { + enabled: !isLoading && !user?.avatarUrl, }); + const updateProfileMutation = trpc.viewer.updateProfile.useMutation({ onSuccess: async (res) => { await update(res); @@ -226,7 +219,7 @@ const ProfileView = () => { const defaultValues = { username: user.username || "", - avatar: fetchedImgSrc || "", + avatar: getUserAvatarUrl(user), name: user.name || "", email: user.email || "", bio: user.bio || "", @@ -243,6 +236,7 @@ const ProfileView = () => { key={JSON.stringify(defaultValues)} defaultValues={defaultValues} isLoading={updateProfileMutation.isLoading} + isFallbackImg={!user.avatarUrl && !avatarData?.avatar} user={user} userOrganization={user.organization} onSubmit={(values) => { @@ -387,6 +381,7 @@ const ProfileForm = ({ onSubmit, extraField, isLoading = false, + isFallbackImg, user, userOrganization, }: { @@ -394,6 +389,7 @@ const ProfileForm = ({ onSubmit: (values: FormValues) => void; extraField?: React.ReactNode; isLoading: boolean; + isFallbackImg: boolean; user: RouterOutputs["viewer"]["me"]; userOrganization: RouterOutputs["viewer"]["me"]["organization"]; }) => { @@ -432,7 +428,7 @@ const ProfileForm = ({ control={formMethods.control} name="avatar" render={({ field: { value } }) => { - const showRemoveAvatarButton = !checkIfItFallbackImage(value); + const showRemoveAvatarButton = value === null ? false : !isFallbackImg; const organization = userOrganization && userOrganization.id ? { diff --git a/apps/web/playwright/fixtures/cal.png b/apps/web/playwright/fixtures/cal.png new file mode 100644 index 0000000000000000000000000000000000000000..8d300dfa0f407fd270f6fa5abfc191bab241eff2 GIT binary patch literal 43681 zcmbSxWmH^Cw=E=r;O_43?gV#-0158yuEB#792$4m;O_43+PF8akDTw^@$S9vdv<>8 z(W7_mQB|vIt~u9QRiTRV67VoMFkoO{@KTbZ%3xq0G~ORgs1NUN9yL}O-hZI&BsCqu zzz{%x9&kLB6rT5&$WCG!PO9ckR>ok8qT)m%4#xUUwhlr%tyJ%gsDB!ToSn>U9sX)n zwY9Z!BodOqop5D(Z>IN9)o>Cub~JP_w{tSLwE+WzN*SS|rmmV^U}R)mpp}%Gp&y!< zpq`f*pMU_f$%ML0s-$ZK11nya5*1Q)TRv&~oMvV0{U&HSe7iy|q#BQc8=B?+SSz8L z)6akrp?zT+p3@bD6A(&F6Q;*lgoci61@%)LUPI`cVptH#>8)pjJn!2b$jc;yx5ah* zWc+0GY;^ptr#-Q$mUW+RpL#YA=sY|HOrP z7je_^ogwMimq)t6*eYMS_|;r$Q_{YhO#(VPgY9*#vJ#mhk~Jzd9pt346T0p2y@HT2 zPIUhH+37a9A$;7)<9SR+5a32+$)NhwycC+unw5yH&oXUo3YSil+7Go-8p;4pY<7e( zL|F0WLlIZ}RM2ZeZ@d z%?Jk?yp421#UU8g!db0@J6p-m$eBB&uY-@-OePL2t?!=-LvAErIDaEeGMPb;5m`m+ zkukz5-osMPRy4E02h2jno$)d;Dc&)J!U+LCP6VyDb%>`xDC12gN`gC@t8Ob-<7 z-FbI$SQgtpma)hbYn)7*+=P-i3ZMuk3!68#p~1L{4oMk}Xp2DaUZ^-P$g$YvAk^#r zg4TK2T_yTDo`(*4B1rb6bwMKxSnJAP!4B?ukr_)GBdPTuvT@2djdm zQ;VZtE6`drWL9dVVj8!gP-pr)wuFk)1h?#bmb0XQ%ub*HqAcBs&*c_hT)bW5?_wD{ z@`p9LppWa22YkYGc*!@CAHkrj`W<^73vDMQP;SuFv>8QJa=wg(HOSrvmTDX*wQm^} z6uLA$7?;tYDc*>*v1{d!Y#!Tp4Vg$wk)__h@mrj$GNWOTYM2{mv~^z2?>9`g{i!Ra5^@Y9UXKPF)vz8DmWQ~S`8;qc<2tl2 zdL^*}pdT@*ry{u;jU-eOjBT@Tv`-ThbwJavM<(hp)tbbrMGY&fwE@^kzXi3#7 z)ydV*{anezB^y@&5!}K2`)W&R;WmH|TvP{ZQ60 z|LJeAfSAM;l&P!R`DWkKXZDv!&l}|B8aw`l&s=Asr!1u#D@kVO%@gLoSM~wJY=3ol z&Hi_fSnqcK)#LwR)JenxNdkRq#14jY!3D0!0wLD!+ek`_t4IFCvl-xnRQkGlx<*A` zS|!&}J-TFK-^y%rWiITRV`#ZH%c~85^Is-SF8poNzjlEMiW&vtlI{ zL*>=gM8ofGo~9~}??ebI#ek*s66>aSWrkyOITgzdZ4Mjtk*sI^taTTf+WNcsmV^WU z$h<+0%*E<(*Lj}?j{UOw@C|9X+V91uW7~~O6n07q6C$2AXoapV@&!EnK%nM8k;J$X zw}pkSSaINktSqUl>s;_j0^gyDZPILW-O7P)njMquU$%+p_WaAZ|GImB!|=Z^z{`&l zgZ8v|-f0{U+MOy9?~|`z06k}T?wk|*FgE5*Msf2G?XhjupA(aRlqJ`f75s&W?8-kF z|G#aZ-oL&H_W4q;jrq-sAXHhonS(H2}YyC4{QnqXS53pdB-WgLie z5#~ys{8tab?ZJOL%l`>j|KU)N;Pb)7{&~-}7!|X-`y=T-HGu<~8SMe-?N*I?m2h0_ zyi%o0#{)_Y{7N-e_;vcFNv`~Mv!Ub%PCo0~l^w0r(QAozOBI*@Y;4$J(zNu_sX6ag z48e>j1S|JU59S98{Pe&{#wttG#=RP)@BQ^!@au_sYb!PFZHtVo=H4qT``+uK%`;D3 z{|Mt6!9IbfVfth~2xXqk7a%LH4H=o?QePYF%{_@KC&23w3m8h6eJLx*$FrN)t!qUr_(w_|M-%MoboT{wIexP%#4R2g%(k9n0!S5*EIQ;cHj=0JfCl<&*0g zOF1m!?NWm7sAnaY=hSP?&~dK8V~AZjnA4p^g|TsA-ca9$r0ZaOqP)jENj0BG^b9NJz>R9}9066ddG z_(z=4S;p6GbC5f4?)2Ey?7Qw^Z@iSEzj%u8TX_zOqO@`0^X05@Z1evWf3sr!5dZIo zI8ZnGmtsvqUx(Sw{d)kvgjpqBS6kZE%|A}m+47C;bvXg#yw2R%(oa(}&OqS3k@FSH z+Kg~5no?D(?5A_@u7J`9ir_dJH9r(3ob1!G4pkqR#>3<${`ZskpG5O-B=wyeaV;WU zNP@_B5&8+%SanAPl6_>bEF3eAVN%=VjmGPYRa+2?BLlRC_fPj;%i35oiYVpd{)PSn z%fH3!->UV0+sJIf$zZyB@O*H z`5#&KrG9}htL-qrS+J(YRF7Ew`#4!Q*V?QZAmLrf)>7m7%hkk?u zafH7rK~5Z{Va8!Mbbg&SG(P1YDYWwyR12%033Lt6W4r0GTq&ok%3r_G5Lywyo6NTL zk4)*T;Ol0PnmzXNt#Od3BosTO$TVBw9)$+7a`X3Lf(?){*UerjCB2 zL94?1$e40xY@O4x=EooXSSHtk6qvQ1{_-)|eD;q9o*D&?lp^5#MGHMw7v0?QpZ}KP zYsgWFKRHcspFz~?f(U}}-!Zxg!=hvBYGb zlM4@Gowj)(t(EQ^pS&rxJYVG$70uoC?*`XE?M|@iF?w5oNu|xBC}P*2SnfR{{QslJ zsztu;`n&WaMbc8^Axll!4c?K$lPNLX)RqOjxQdLldcu93!=1%rZ-%GS=hT0MtdaTX zsyJ>E2Ipc8x`LTQS&E+X8j;X=ir1m^!oos#em1r|1rVpesN1!EB>i>DXaVclNIpu( zjr_962EF@*2}9D3Tzdi|#&wDvzV|OgJ4~IB4sn7<%atj^Ls5GkDN$Skstvmy_@17g zQ3A)o(@X!PmvK3vT|~cmt9?5%G3<%%F7sRTbDUc!*vZJ z-?tZXDL^VCw5+~1`RPB!&YKZ{`p1tlX@Sk3T(zq4k7}37H1q^A_Z!?4vK4>sY3=1<3i{QR*T+U82MqG}gqL|2VEr$K8L3Wz7-itjmZQ#=7lj zIbRW^p&%!Bxdna}Kh|&Gs}2){&Cm_V5HTg5Z~c*lh=2g#ng?6Z^?fhXuG}u z8GV~>=D#;sE!A9}wNO%0y1HF3Qzz-YJl=R2TUl96@ZYVD7;bpqt$CieowvNbA{n?@ z4+BKDQd!!Bd#6712{a+o#eiFFw0oPG^}f%r^HzhaAWChMeL7gZW`{d?0L=Vhlr_BX znxuhgWe{&>#vLFV&^4)!qn|crGTC`CncdNanjPIrOAcoZN6~9Z?XP^}b6cCa<}@Rx zu4UgB$+-?S5fFO?zPQ+X@78E$hjjHpp4u6yneHPU<0M3NJb_&&c(hwDDT3hh#YaRw zqs8u|V6w=yFtA3=NW2C7dLg6wYZEf}F>iB@y>L3rTsbLO z@Q@-M%-vzRYG37S_eb3dfY+=Cq_VI&_XC2jm(wzgFNX@g9%Jka2lzy}R2^ICRi)Ka zN=j%b)4ann4VLGyuutXk!@O^r`fdu=q>IrMqR()%$@bDuYR{YXu6 zsj9?_ZjF<)Z^Lz;{e53|1YdXJGM{%P1g?Hj+n(WGT@Z+_1j2Kgaf~z%dYz)T2x$W1 z1au=8oJ)JIYszv-OT7kdmMCWQn9AHDnM!~`b}X6#g@u&Ng56CnXRAId)(zTrT@XoH z4$##<-#KqzFY-dnjhKSVek_KI*ro)NSXUUT7_DwqNq53c#{5()PeR9ON^IkCzTwQZ zk)d>=P)Mr59wpKGHel&^zu{kC_Fa{dOdNXr#8^=$*`*8&FIASJF zW>Wtbu&{|yN1_?T`TM2`?=xE67Zyz*TOr z4kX6ZPDCQGC|>-JPUfHzbRZc;2S`}94|1b}>l&{^Wu*_jsr zT^7#Dq^UnNBa9I>2>rFN6(Q>?@6!#_;QOgTWu+I8)_n{x!u4l;n8X(i6&<0IR}p>h zdkmwvF~m`hy7e%DLCXR~eo2; zucydp$R*K1I985(^iKVDzH*q`v$F97c$rv)__%pB*DLETt4STtK<~S@Bt#PDlI+sS zbk~~|H=Rd*cHZo=R#ux7-CKk)d(+14G^d?;Z)A z$>11sh6K|!UhOs??kxi;>(|)ssFeiWghd^u%NMJ*AW4rg<7G$rxD3uz=az?ED;HJO zGVXx7_rs>}llA8lCk}ktinrFo)Q4!3U)8GSXK)jsPGC@(E!%|K@?GoY)vl0Bsd(ec zl^&DoFn3sb>O;16m)oYhs?|g1X91#o>B#SM&)G}R%68NXsw+A^j+I{Uy(_A{%EpVz z0=^lBNEHulpT$9?_guY>mMYLtDikOshKg-en@k3#;3G0{C1p6HX-Hl0q$j+xrubIq@{B%*r zWEFSwz%&Rg0e;UwIRV?~kstO+hXj`_V*$+F1*I>Ct99Bt>` z=PKu4bCmRm7iG-hiyY$R*1k&PYDwT^ADKNR9=Jp71acs^JEDp;H+0!xL*E&wX8)|O zd3tD$#p%uQGGA&cXhYUG}K%?!+~{cFmgL6{XEdQD@<`@Rum}`rr;+< zQl1~V-*HwV^)Q8#iM!G~cZsGMMSLR0Q?mLfAMDVd>DA!3hd}lWX99^CuND{1w@Zx3eLX3H$ai7OX|?!e ziPdLh8zvwS-Z7k)PZIoj?cB$8&3R#HVn8oXuDi2WsPCwk;+U!1$>un*yviN&$5v9R z-P5V7ZbYY6$PYwKO=I1Jf$E18%UC9{{51~m*`Jsk-rklWvrtl9yy2vRhpFoM#@N2F z${H_}p(@(!uObszY3FaZNb`8U zZlw*0hM-<|^a8(ff!E7uO6<$50EF1rkRbL?Q&~jAo6rN~Zl>=+Spp<9;!f1Ni!)9L zzCD*#JmM$VY)(Elp9iY%vTldB1pIPTsQ^ue2wZo(9(S1KRaAJ|Sl4g|RY4MWiskQ3 zk1)UeIt*&-ma;CA@#K<4ViRvv)l!U51oCXzziTA+Z#88{Xk+OMfy=~2F@5eT3@j<- zWUJWP$H@@BlYB<+LqozR4R$B=ozUqehvH2S$nJPHT>;(?zH5^gb~=62MP@rI1%!_= zA$<2SB3Pr*KG&quD_WkXb?C2{#@oA3D0ai@Q9MUMww^01v$7iNa>CZTNbuf2rVv?| zDneyLhv`v1i0Ia$8h%`>N18`;29K!X_*^06+upBU1aM6ryepv&oqG~3wpgV&iDYO2 zaN72V7dU-bc=zGi&%3{69*%NhPE>P+9?;l*Zt`JIcjf@E=dWky>(>RG{P$gm^6m=y zB?a(|zG?+}SQ&I_9;{xn%yc;1u7%kA<*vRrrMfTszOVb`4eKQgN~@`rRNs-&6ORvc zKFup)+;C60N41aZdLvRNNnr{bvV}Z)1h}if!m=;Lm`+WIlxg*E7Vh#Kd0@rjvumef zv}3AgPdp`Au;Wpyi#}EtG;bONNKBpv6h9g|hk>s~4W-T#bM@co1!SO45 zLt&)Ux4Fd3mI^Iw<@iKLp#hcZ86Voj>jFo&r$X`OQVQTaF<0G!I=^uHF&$e_&ayEp zWxJNRzI@Uigh1|is(29shlR0pySzv?_Lhi^A>j?&{V0Lf=Cf-R(>`jTV)X^8R4^ zo5r1Wmqq=ME&Q@XweaW6R1B``h~;0ymb0XYTpX;Vy!2fa(hs_ACg_euwo1@MY2rGY zVC82|R*S&rAy)Z9+R`%GH#9L^f^4YH-TU4s7;EW)csFtfs<;YYMQ6ESGKW2=99uap znZ_q@=~;c0MYs|kRco2;PeIE1;(;M+_(|bGDPeH&_Oq1yNx+sb^puZq@IR$73MdQF zHBUtoQw+GTdU?<%c1>B9oLc%jTH;T>&P!B0%<$l2%D^S#fSXUZdMgD+c#a7e(&aaz zp?jMqe@pDlfNzZxyf~515qrkLvYTMx=jLL<(?a(n4VGI+BSoAJj)Lpp&X$_m3l(_P zN>$0k~H%-0%JWQN@zeAuaLfx zsQuHk^KyW1JyyG3#Jk2hb+{zyXj0TuYX&6gcoe^q(dTsu&BYeBTFR+rY)C)tD8HS* zy$GsSx81F`1gXq|O(t!QAzXH@r zp*vIc(^c;A+9J-%DC1hWBX8LLGMMmVGD*gGit@No!AqMf1Va_S4@kqULy)wW9PBNl zAqPQ=H0qn|ql~w3WhKQ5v>A!|2g&SM9sUktbZN>5d4WE^`|sU~pg=#4`Ya(598e$Q zW-SnNRM1nu^AnMhihPrp$4jL02+7JZvtZ!j+y2R01LKQ>k2|I?@%u1MoE@EF=9GSR{HR2C8HD`mc%gn*B3h@B(0u=B@DSr~Ez>xb3Bw{z=sNZ)R2c_!dP1 zvol$KDs}(4w)36xFc1%l|y$qgyQN~9}^yYZrat0%`a?h2cOc1 zpy%C<290y;!(q=Vpn`$p6S7@kNJm-G2P~Ry^LhyIb`D;JDEglvY1j%i20g>0a?iQZ z!|f#Mn7H}L52er1p1b0k@sh$)$xrQ0@suv+MCc#Uhwcx+*}5g^ zyB@+H+lh-WQWGxgE$7WjXzcg=M??z6VUMD8suhWK580fBuiMFfShPj;g1YBuaHUc= z^uES^;!G;*1T#WK$o7IW>O>{{g%Ba2NKCndQHTw7UWpTT=;JZdpZ;{>sBpm4@WY>( zcVQoO;b^~K4iL!}Fy%n?EJrY0vJaf%PJ(O^ld z(4g#v&zlGbUcA>h9zq14x9&RL`13+GU<5Hp;rgWxa7z2-#W@CAFB^P4-#Xs8!SrEL z7;k52SCvk2qof>mN{0cX0}W3{7_Tx5@nMs z&x#`rVs`9Df`j;?4_Z=8zEY&v(!|WVG+nXMQ;2rqH;gf4fl7$4QocI%B#dw9aagiz zcWv4n$DoKpW7g{ki(s*(rkNWIpq!|66lG@1VFAF$^TGChpT1#L_!=xi^cIVr{NtCm zfG#4!xAM=?69J#SRYSk8)6{Yl3q?jqyhUe}gC#J4f z^(?Q|SqlmZ&XGJyxayXyb+pcFnEKSIzTZ*pRiX>Vnnd&f(Z~<%snXlJ{WNu86G%dLf z#dRRByR~0vXzX%Exgfu%^}F`B3&gjJix5GScB+xkB7$2~#;Wd7EDnYAZ49I)y-67s zwXS0ocKc6hLj(L<&}}yA=3}$O0fF0q-2(${j`u)YOd(pQg`0F7`y3_ca^ z5c&p>xTg%7t%WO&Mad&qpph~PKiU;KtLX=XqQzhsG`ND zW2d3xv5sLJ!H*WtVUYMwqJQrD_SCb-S*EQn4K&I}wAgM<0K{Si2YxI_uicbZM}xuC zant7+=VX$bvzI~(8wL41oloz%E zr0qtvAyMY&|$xG$TPo=xo$Y$oL5|UIU(JS8VZ28B~Y~oGl??b z6!M%Q?xAu8-+=Wdcit*%l;6T}%DKOx?{hcLs&_o9xu7%?omb_cLsw{6Z1%*ANMFAQ z+%+Mxy+qo2I2L8~!&Cllzl@e`9UKO;OwF^h8=r6^?Xs8M5P;LOlZ?-N@ zfQ?iiy;bfKV1>}_KAi8j>po8JGdAtUZs@wMrPf)dFuMq53hmDzmy2JYhKa!isj=JQ zU?B!d-uq*=Zkj82;vkYF!Fi-J{?MT&l^B4-pptBqLLniR9{1lzRmEaW*cI>@rna0f zD|LQ-Izvq`-*6kqlsuk=>f^&y#oaBV5!Vo`(p%Cn-brLLirEPo}-cF30tOL8`- zUy94PCYwi~3I5H-Hmx<%Nv7{<%PAM(>yy<`ba@)`WME+WiBLX989V?CF zPMLxkoc2H_4a~ujq?(1jjg1#Px5pzYh&U-2;CrP$`NEBi){{?dF^NkVitI3)7fMqB z=)&_nscv|F2quG7@gDQBJAk)!>ru&kR9&jLK`Wz+h=KKaIo36mbAE10%7qL%e64B>O34R&OT*c_X$?Hj z2;^?{-S%xVLj}7q-zK6P-3*Wx1;9yHfT$wNHREB_G{deNI=^N|1O_pVKTsvLq~ObM z;~d&?z>rkJbYhn%OIgnEK9k=IBl(ixR&|e-+zcF^8=LG(1*C}+wmu-hV0Ql1Z`s`J zhIp&0X)%kBS+s8vHFc$5U$lv35xq7)>OOB{3SNdP1hzhc&1zM%k$X4|Qx|S_!AC9T z1t>8X`xl3}ccQuMEt$@6^9|sr+Tq*=)r6F|0ydT28>$Ck%3<&vY+P_3z=u$OcXPZjoch__bpjCi zTwmFU=$6fgS7bRzSd>qu4{L7t33#4y*R=6Vg225OS%PSE?p6-=$$D!fTWY>(Q#tKx zO$LRll~ve}#GLEP0YZg-RXr0Gfztah+KNrCtoa@eud3<1sC7LGe19+tx#2qTtYxFZ z!*@OcD~kt`HiCy=M&a*3`_~u-df=UAs+CG85l--)YXz#F=`&+V(&0J{bM=>#Ym%Jb3G@Xx!=EP8yx0#J7J~y>v3HsHXj_d@I`z@OD** zDtLeXu6v~Blu`S@;pAd%!$Ezo@SZXm8dSugEgW*Q>1wws$N7SEUrvJSA^k&~jNJ+I z>b@a`r>+6?D0E2VF%y&gMC)pO=j~L{h246IY8eXqQM&Y!pEv;@Wc$ zaW@OGv0dy#RDz0~Y(@e6D%s6e{506-%ZKdIIxk+yYD2Q7Xd~vsYo^y-SLgwYP(G_e zf3UNt2uE*xVf5uX-KFht9AC(I^Uuw=j4*{qj6Pj#LY3&99VK%KDqnHApFB2ejtNoe z{s_qmOcg6;h{UYr!k6y_?FcEQrw|ZgkIPvxvQtwTnOp^_m1?i~QWhnorDK55#R?|J z27*IiW*G3CCXp+Zhmors&|D1h+{ZJePYDum3Y{SbAQw4i$V>~u@(Euqo@IPJrUdzS zG;O_h05Idok`+?JPbV@de$o|Nsnv7e8*GH+oyW(EoMtjBd^e!aUuH62cS*vC!`Ypg zeuqv2wPK)3a~)9O`-Bs|C^9~(NvhL+gHHhE9~RGvh`6qL^b81IopZiyxlRZkQAJpI z`Pb3M4>}W@uuE)1l`TV4DY-JYs(wH5kpYvcaX=PPNHK_~=}j@IBsEvMB-LmTyv{~M zmYXb-XoCgE>}~X$(q*uxfbmR1K#}m5RK2D|6Ux887!0>}9lr{4E2QI#W(_WqIf)(x zSOkw^iGCp`V9-yB06G|Rc1dbmSuOPQZ9YbRDY=dt zJ;r3UyB_y_EcE5M+z^2B42bNC4S&FfWMbh741jS6HRzQrpwM1WFvGq@A>sPT{8h2N zq2W9}gx5^fG~F4)#ec$Q4+MK*%}K!(nYsK@HGqYsU_UKI`RDNK>HQS!*zseUWoHw? ze1yC1nk44ZB{$@08g#K1t3DsQ%P)r=pdvxgIM;*>*v`|& zo6qb96YiBFiAC+iN!q#nPD1UIp`#Z(1}m$ij1L@+q)*4GZ?JL^rOl9zLi!_g>)Lk{TbW zvM|zrDz~3lcPCJB>}&06ZQ||zbSeoAZV2`h`8%iWFTSp?csB>vj}E+NH9l+0DTl=? zZyqbS8~5U^D7^JHQUkzFha(4}LU?hIpl!KT7fqE89w!%GTZ<;dmA)`EeQ*~+k56zG zA3OK!+IiYthHIOnSGRFF9FKPe-YC6VfN)cexdhOohl(;%;wtTV5-t>p+hfan3s%<- z&C{bl&R_B@mOzVq@_HL;`m<4|ttsUuwr zxLXwF+Abr6>|Zm5aj(yDK=FCrA?&y*ALznmf@B#o8fRYltr&oQy4YMkv!ajXS)*|G ziST*zGjwfQO4>c3;{iY>rnpkLj!^Q}nt%hF@;i(tpJqr8ndRU;)BKi1m_YO3LFA?i zQz|h~Z?!H7_9y+*_FqWbpp-@gx}=!qxV-}Um2zMDzqPd1{VtWt8Fw9YFB8xg%6wTh zeGkmihZNL~gh<40qKu1`Yp@A~)+F@f$_T*r+Rqiz%sH|qb3y+1QNX;8qcznni>SI1uQc%+t5@w2e&MJU~ z8E%TVyExW;K@rM`=zN~dbM;dnE)m+Sq6ZCqg-D{PG92I-wYrX_FpOWAed9gsc^is7 zZ@bIjt+&oh$xtQMD&n@G_kR-e+KPX_ZPDEi7LekE^)~n(*40&dNB;CpvDTB%%cU2> zfdlnxRnKG%%9dRrGlSai*3MsdDFqL^%WJu}4<~KhVXVm02@qwtW26+$R_!kl85{S8 zH>jO0R_4cAX#J&1^bM)>X!3BWsh8dvH84J!tl4_xD?+Y(X6ZRs4C&U^ZuBcB&E{jB zG@Wk#ns0O?Y3YcSB@&Nt$NYgZvo1eKD z1%^kf5rm7@X;r32`5f>1reBH}9VMwB1Z1+6Y+C10ejQHg@VZ?xZT?As`M?tT2Ing@ zu&}-m^o9KKKtldotw%&nRnW6jIZ*1^R-dlwby{KHG=7h`l3vL{;LZ@`@bz}Zwnta; zC-U*Z@N>b8Z_|SUrSEg@MHc=_eZDu;&7*U#xa@gYehE|G(euKqD9Dz3S8q=jc$IJh zg|ElTn~GEzBj$4%E+Zik4C$Q?E~V5D^?g;NCS1Dkp$pNcGs?DWo>4%0n+Q{AlHgVY zJQb!+*X3lff{u^{L!SQ>Eh+>Ng|xw;Fsxu+Ln(OKSvP+RvGHe@{UJjHxbR0%t`nZS zAxsRk@%m#j?D#9I@o53YWmcAOlN#uzXIIEiKIqx9AgS(qoyVycnZp#0g?+zT8X?wb zi-EOG+{t2lg6I{a&m}DGc{41@h%RxNNI;YH9DkZ`ZVyr&{dW#`%iT#CohuZ7(p!y& zb=b1%R8_+mQEtGw7ro+z#dVb`jT(=i!l+%)K)v-^i5CqDh5|n@{_Q>*b?b6Grp5Y+ z|8A{tXN0iH?8vnZbPD&Qc$16jJGB%<*~mFA-}#TqA5?8fEp;-?*Edd8?ZTfMm*hCt zE`}6*y7Ore;Co)9eOFfMZPxktxJ22#){SU1xcbCJoq4MA_RRw8N)-oua0?g8z8}L* za_L9y8&56C9CMZ#$E-BiYS&u8RniOK6zHS$SoO1YizUaRZIKjmx{$3avDX!JE))-b znWx0S{fS`8<@9!F9vCIBLEQdWH>1{}u~hdHHjGKR8m_4hvzpmNiFRnS4C0nq9jtF* zTS@LC6bUpgHQ^vpld~8=95e6B=PHf(ls>OhNf~0o*sk|EELt$G>vd z)1cEJNh8M8u>dgwK}|C_R)`7ZTDVzXAG&hjB?!9KQY@T_IUI$Ynk4s`2kLl}W_ z9h7Mz-r!~q@7>D2*DGu$&_1j1NMDr!Q)ItNm0d_@mt8H6kx+Ukq#iPxDpgwUp|o9CS40?7(f+H=z;L6;R;e<9NfwOTVV+wg^dn+0mfA zDgec~1SLmyCPwh=8=_hZLtyhSpDR}&TJXAa-j89kg46Vp=6D<=A}OUZY=@r61r@Tg<>wmZn<3~|phl3MDtO`cKnVt@w?|Ed zhs#0A!T1i4Rx1r-DF+`pnWwn4nr#pscdErHyxVG3ojJC~T>1o=psl15Y zlhUC}Qsp0*Qv$V+SRsZYNWy!`XQykJ36^e0LRGPS zo2F2-^sF~8tmRsT)Xoc)~^}yqC zp1pZOPx3w==Gps^iBsJ!MX#?P>yXU}go({QVgXe;Y`4|3Wj zy!K|^t6g7PD~^7I)Z|!~MQU0`C=GmL;%5K2?o|eZJ)9U zcuj(d`8xai4QmGQYbNROL{YrbK{1hT`a@W|W$Djnxrg~isRhD8c=t$J0-Wrgazs`s z?k2}*{@drxP|8=|w~gbb<@wVUJnFkLdk+%}Gq;#YEV#mlV)IJ%rv4CiExW|Y07uDS z7;EAU9ShkER$Xl#utpl`MLp`05-qWOzx~G0m>af1X%X=mzqjns> z##UZis-`I6$vcv7Qw5Q(-uutCx=vdVR_e~5;5_PAKc9Yc6(bm&Jt>;+L`q@e%cXe< zo8dxs3C^J{o_f!+>uo}WPzE_zcjl9lF31RCc6vBDiM&*?vr#i#PUD&;j`4oCE3pcr zPlPMn-7RRhSVXv2%<{(oA%i(KMZO`q+KH56=f&k{KyRG6IW!-H1jkCJXqXx*HZU5Y zQOHF)Dc&!B3)4*JPj-OEF95K~qzu4~pgh0d0q*d8eMp9j*P+89DQDAQY=+TJ+TS8! zanKx!R@ZR{wu;wpY9XJ`3UaU52_>k<5SC86#~P>r*EfTZ5F8r z!B^)5-!==q-2;z|)%B48A@Q9&|_7e!Od&0FN##czkIOhN#j6!StDWgpx|Z>&pp7290j4!%xSf zOF)!Xe5570(uD_2Qwf?3*3}1<;RKG}AAV)Z$;3qzYSaqfT#i3n-W>DrvGFhqh{45M zK}w^Dy=J7A2om(4hfDJ~QacLMA*KW=_&(0mresfY$OzZ#;F6|gf(KSP&`A7L=3H#d zc62Q%aQ~pxLndDo*pWz_T(W<-GAoA@d|j{jl87%wPy6ASYZ1*we#{mPLi4N_r!30u!KXj@fl_PXYAqBi@MB0&} znw)PomkRF*=6w&LFhad`yUKvh%V#a5u*?c08L%)e3}j&>srzv8;DKOBiB{HNtO%eC|0uGXHQJm4E^wfKCzU74Cv z;|l5jTE10DRVR{bSUO(;NJ|-yFOcVcFpObJ_^Gm6)zBFqr-hy|Z5)u)Y;Y1eQ$p|s zTpzf20d>71BUWKJb^qz%CrtzX14gi@p0#d=4Pr&XOR!1bLi(c`-DsllbRB`KE|kEj z=fw0Cb@RiXpzj`QCUF!p?|aRomAXAhb&4UsN|7$^4#naJSDfu|K8gcoJWYmcrT3W- zENjV}%E2%8)a0~FN^*rW(qBIobiBvt8`fLxN50l@q_-^8$*UMYz>!d!?Be_~nFp~@ z4sY)tfCAVF)EUG-MF8`@+a;p+?a7~4fpqoMZXu7hgZb|~4nz=h6Q(mf&rW%58#k2& zCr~5yYepCh`7}3rg-tbY-Vbr!!#oyhByGr0;jWu-?K+FGTYQFiOreYHXg4dxPhz8W zLCtaoXVas;gb^+H25ZT~pGQ0|rqw;Vc58k!tDJ~KN!dwdksTI#<_>>52qb~cTh{D^ z%C>D-bAgSblBMlJJ+q>Nz7)+)3bS%u-&RfB%JAy#K@?20v2fzeT>{Wq9SL*|NJchZ z7968PIKJF0=xceeR5l)|bQsRUIR&o0NTI*!#%<*7$G>7gO3nJWlK&VD}%nqwtq1Z8CCe8jo^ z5y&_r-1XU5wqzPYDAZ;FT?XFhLj@nRlH!A$ss?3P!YZ25-eFY_LLnlg*u_GH_Bq`7 z`d4b!9$niG^y!D=_pAKp7WgNd18`jp@nZ_RB~#4t2I|~VrV4q=GHy(ArE~RT`3;-xW~bRx%>Ep zV@sH3kO0t>d56D}^FZp+#Xq{}>fc>?!wrA%Qu`nY4~(3TVJ{sS8?O7jB6P#9sr3X8 zj`fn-K-9kXePYt`2RX50Q2Z}XvHK&R_H;V%JmCHhL|;cKMLp*02qEk=KCo%y#>YMG z3GaBvyRN+Qitm2!I~V`(LiRKEYUt(k)U;~)BnB-;uGU(mQ@Ru1^C<58eEZyU9{JcK ziVBPu+!7CuspF+iY$VaGVNs1{Dr=d*& z6gCyRQrZf>_YYSVg-YzRRgKq@iJ!d)F@m;|hs$*cv{S5F@M+7`$+o6_BOUDM?7Ge4 zlv6&Af=*GJs^*tMFX4Rz&PsQMYV_#}DCpV()Y}UV#|)F%0(rux1!=%x8XQPI_I)UH z-KqaV0hh);70}q@p1VsanG1)cC+4Eh$`s z6yF&*aUNtg7l%+TFKQ%P<O2bdE`d}!pP!j0`RT8JUJ zFr1+gYFKb(&g0(q*!L~3cm@2Pv$l2H_FHbf4RTZ|`d@}cA&zG&xxZ7>wB)1({GGS| z_4LzE|H+SkOQY*MJ}Fb$4*Q)>VW9B7S_zHQ`yl0$!k}_TPVfTKG`yRN*LGdMTFsxnTNhU7q=PK^<^ zyGmxhtzfFGnW-cIl!_CR&gq+E9bLV~)$QDSuYJf>c;`FcdDvkOmyN5kI+?6jL9*KF zeuqygYF~{(=+#HCH1|b`uP^y#b6o9y4}1WFe9{q5=3cATtaU-!>D>6#q;w|NK5W3n z`<#68$v53_HDgx1rL)AQ)0{6`Lk&UXX#zWEL7*VA^Gz?W;Tx!N7ou~oDHaiVbMN!6$?FyOVuVp+( z9{Ch*@w?w$v2N`eChgW+Z(Fy1&FnzdxVq|&_6`mVWu0Aw?Y)Zl)phIEGyeVd|66Jj zF*b$TDWya^q-#C@a|zaB(lS~%HKa26=|V0uaK~ToBuIla9TIeCk{6|lvpw^(GMfFd zbpJ%a*|>2t6wrsa!6XKJoQTWR@v>(Nd4y40eqvYcsLoG0LArL8-Elrs1O)Fvy7cK! zePVC`yJcS6NJ6MzrYNL9RC^z~w5{;S!y~I^@+1J|M#t8?@l9{}#3`pe;UAuaUmUl1 zp&0Y0ROEb5e1Ohsyms_}lYtoo9U4W4#%=}V$HAF={GfvmJ^SnD9&_v)cI?a#4X>S? zA`fY-RE)fqQ&|z;T`jEvG|TwgA|$GORPBY*@vMUz7+GtTT1vi`VJJxz8VF3!5L`#3 zM|Ap^KA)f2Ih;$E^HVw6=%i?fmo2&mni(W<0$GS1CFeRSxxMW=I67jKcGjwid@eh@ zZD;6q8)9s6luJVa`F=JN7xF_m8D=K$zV(m)bHR5q5m|zaGlj8{lzHZsYNPTVDQB*l zW~v-i`X9zzH9asqiFTRFyznJ2`{>6%dFaC)o+8pUlbb1}CT5C-IGr0B%M6UnFeWy4 zlqyg)H+Bv5$`EB*^`-}S5QcN))1Gz8XEXK#*Igi89;xSnwgG#lCbM3 zS<2_I8N``;+-v_2{>MpUYxZED%oNFDRPH8xiwG>YmtFXr6E0JVF?8kipaa?AI5%8~ zGq3-bw><6%Puj$$EZ|FC&lcT{eT3P|x=3b7ZfTmYmFgdZ^ z)ha6HvEQPvyNtzMw6RvxEq5IwTt-buQUl^=C1kE)9-&0zR5bxyF?%?rd)?F~H;*iq z)V99nnrqni4cr^5JEd?KM1EB;VRD#8^bgueV|bxep`s635C}BSh;=BGsA%|j)Zasv zIt8$4@)ft;Z>CY@P!hy+$t9Pl9S76iXuXC8m2eV#+bHN*RN zI+q=k>4kRHHafVZs6rZ0QrO>_> zQvGDyP&oVj?|**|7|c~=zM%r@^{AwBpAFr2m1R^AnP5?7UfWV0*>r+2MVTsmz=xFQ z(G>aoQ7?P}7ExTYOew3z^ujFF^{v_Qup*Cr%;P@xv5%=L9@p7;=bf*5)ob4Ij(7Mn zl{?g~PtHrN7r{5)f9aBTZocKu#3%ccRZeQqsxPY0n$NLe^^0=tvo|ok;+|Nyc71CF z78D>R*B9eF`skx+rsl~)ULvnzp(PzA?zgC{Og@I^{4A+TYhuLw)TAkhM|)tcBo<&JyheaF=JD1J0vO8 zWoWESD~4O-gH5d14QgFN3fYTz!gYlf4j2TEi*Q*V;1EHq|Lx!YE!;wpAW}Z0SoEhx z_@f{Fh`><=4Qd4&2z-3f#gddOeK#L^*u!4?+Sj5{K{({wR@5|;%(OQ<9=YNU*#AIU zUa&@xv#*|cCLO8~l2mtw%c3W7W!FH!TiTVSqgpmZTB^2L1IpHb><5D zOiUei*kS0Yt<^4e@u-_Ni8NK~=asv#=0?{sLIp9h8#t~$^K@GKxWR&@sTNF1`yLR2*ZfS_8A$@o=sP1A!Nc`9YX) z#9}tlt+~D;BRIo~Uh6kT$gKYF|Nbw}u5P7)lsBM#Y*Zz{V92`He)lGhclGKun6hDk zDwF2opVzGmrFWavRD2QYkr%!2#k2yZ{ROd33<;_PYf*3wH*%4pUd;FPx{8WVreYDY zmnX!f2Io#5TG$R!K{O!5F+uHP|^br zIEYBK7EGE->3h(F{*Lpm{lf&#ruOrbeMdWz{iKU`xIv2=(~)sWBQ1fmxsI~eXI zgO#qy!BFwWX2-(`hs8=!5beYEf)fZuQ(8EbS7Ycy($0MT{5#CJT=RNk>SF%yg$I~~b zA$fw%RKT;@x=P7l9+=gC{^yrT$iuJBFoGauz+!!jN!6AWyVsT*Y8s#)xHp#pYRVGyGQ6jcB6?TXhzVO9LE|mCN4(%!uG)p~{)W^#&zr1$v?Kr1S z%OUtkK34`sxdSg5Gnk$i zqgzw=aoE4LbFHkKAS23lWsGos%|lyOR5etMX>1m+Z6vhB`5v7!wWWMNdWFfzK;K{^ z!lFbjRfC345;q9PQZ*y~sX|qQ6ujOu>+@Ux6{=aVGp~8gF=aQyV=9cIULR`rDC24i`x)ml4xV-E zHYl6L7yN_<4JekCA~Fi9()N(}LXw3dey_UvN?)~P&~L)}UCTpmuFAjhvwr;^Xhj%c z#l?NtXGXeR3qB!sK6^|yMg_I19|FPwrdQ_1`O=2R)Z?kaxnMdqNVyx~@ua-h)7V-! zjWSC|9P#9_(N&C%`P5lx_B!f{rdk$iSGmU?dmVJp{aRBbF!@kpK_?x=Rnw#eiLd*`mr#XV*301b(2o7(FMsI^R#mc?MLu?q_6%8!g_6qyqqZlR5)VG~ zP?tHKNmD3UHu$yCNz;;0h3_>x-&izT;=7vy!(Yl2iWZW^MpYujvvvjg*kdZOCG zKk5bhl;;pQW%nyS8G8d^lfEV@gD?3^){-JWJwr`5ONc{JE}4krAQ*7LDN`?c(TmjM zkfcFhSz;iGp!wjtO3`O{;s0HC?VNO!*}JZ3O>xTA4$V>;#^)Q1C^9IM$+ch?Ro6*P zpmrKDpUv-5r%vv_|NdcFYx~U1-?y)(C$E>E@?JG`bxj}mzz4OE_Q2QC2sPnSSDGab zaYL+FMyRtV>&>nPq{*fX&1?uRm3F9d>7kZYFdOzz5Qo;7l4crn=yBR$-U6p*LJ;4U zCOV55M(U(&hX$-zrxmOb1_xfK>ZkpQMhKk^F?*put<=FeRTEs&cgEQv(IKAkjAy## z%Vj4grd8;5S;Sx0M#nq+ZoKJ+if_;P3+vYRs_aky;HfYb)PhM)TWTAfQGOJq+R%6u z_EoJfm`>t7Aw`5>UMK@GU%p~XIqA`wW!O+EO4e(|F*eG5-hw7qTQ=E;?ha87rnN1K zc3#`)c$aT4j^=idjOtBA9MyFBPAjMwTLQNH6??9`lhCjX^jOgLZ5$pQ!j6N_oDY+{ zlB%TUWI|8y#=eB^G zBL^6aFa$bQyDs>ByBn2}z%BNPR{g{$J;~M4q7@v)lj0Wk=X~KFas`E-hQ*{95;+EL z4O68`<@CCQvU*fQIScTF$3KyN5eb!0+6GPlJsI$SAAIoNdA+l?zg@bnpr*y57$%z& zeV2CM^%zx)lmYs**Is-3u%oPak9xZ|f1OH>ogjNPe*G4%!*8Sn@zg3KOwTe$ETG0< zbthH?YwWAC`WPczA_PhbBMf?U<2=H!a6ZNpeA5KHZYCJH-klQzJby#2{<*e$1DA9g7Rp~c7HOsU< zTQ3Z%hI$PWozbYYSUUw}r-CDP9D3-X>S*6Mm`((Q)2}pKr4_MsZ9J!tsY)BvYASQG z<)++#?u8DHa>H|`r~%X(Z^k+8+uS1s#KQ$)3;f5Ph)i%Ei>RQl!^8gpD)#^e7d6Fg_#9s6ah3YK(m~0(`x6 zPWF;aCb%kFICmieAXI>BE%7|~uY+RRxHU}@hZZ>vg-STqyG*Rc2$#k*DzIgdF;!)) z@y4n(g$Hgyd(SyLNpgA*F+yTrLj-1HN9mjokZYy*pe100D&?ca3PEK(G|#PBEP^jq zm8J<04boW9;6o5^gQ+9oMA}TboN62twgeCtR{=JjuW`PY!Z^d2$t9gjC_BRrLwiwX zPd9D6-vT`ec0$ZW?X?Vv$cVhmoA2A;B0*|btY&CA(Cm2(Z9zjuSqtOg)}bg*2!C6_ zzW**|gre+_2{FRP8@;wLxAr0nM({!{BdpD)=oR333Wd``QHI76rM5m+Wqp1LHm(R0 z>O00U#k;TUgnAhR#Q+r1fbvb|K+ugO88q&`n^G62I3}~rU8G!6gZ2E(Vyg0&7jwq7 ze>iTWhl=pInz8Zh^HO6S-KF)qPChQ{zkc!ScJo4YKX%!BGerbGO*VQXUK3B0o{q|Hb-7+IoA=y+VC_S@+Ej`_rM_8UA z{EC)Zv<-Xg;~tkUlqM$0g&K^bIp!UCn~Scon3JZnp{|OfL7B`?fATXry-ki!(DH4_ z_v0l#d~9%dF=OjUgT^?s8?o@M?ua2Q{_%H&SJeG!`nXUuC!#}Qjxo9%*&NLs^EA>- zm$l-Rvk>l1XVY}b6+5M+V&wYnrN>4_>3CWRPASn0Fy~VDNL(qwPBYDO9fssCT`JA^ z#vND@3pEU*8WgijhNvG;t2T-ZlOpU)DZky2n~x&qp}c094pyQ{By*}7BbPwWoC zu91ZB;!(N^Pwb!G?^`q*H~oCLwlPg`bk!mqZ287qyUzDug16 zGf|xDAf@dsMi`oXpk$C1id53jMAN9!%2&VoRYLM)YEZ*4WXT@VJVGi@Fn2VzYe{ib zK!0r(ZAYOy&MD-*l1(Tz=65BiwRT*C%Sz=yRZh(5Hp>KwP%1Q&s-{L%e0~Y*E=zzS zJ2+^(BV$^p$Enaa3QdOoL^qm7WzysYUCpTB>xf13E+?!yKl#Z| zR7gc(R8*j7p|4w)Fv58qe!)Yd*;9<%5*&DWHB?MY$ftVPLo{)hU?`!h6ZhN!l`hh zF+xMPGeT`~WK8vXeavGXGhf@F`e{I1hipPTg=)1dj@UziqqSOKNR(_a{!n^`2ghI) zLTr|RFiCq5S(Jpnf;Gax|K!ZB&8cr{+O(AF3c}K<0NyeR5ISO^sj_y?uKlwcFZ5>N zp=w-PhSS=(adaDZoRn9U^psGNFmLHvU9dJR)9AjT+Zmxu=OQ=IRv2MLf{W*m|DJ=L zX5*b%EBMfaawJvHu)WW6U9F+S~ z6#{jWrZn_2?l&<%;y**HlUWR7wj`>)OFmN6WgJq}&~+V2FAXDiTHv;ATkg8+&g8KE zUFu*cRwX9dT9&%O7~xV&rZTm8oyHlRp7WgN)DZM}2VAa|+#%4PJoOVCTEb+cxb(`x zFc16evrkA-QY=j4TBEA|z-MT>7Eb;Wj8m&~%{AAAqOU7h;;vN$#Ryp)oY~1=sJ~;C z%yaFv*Gj+PXL&X0kN^jtkY-;EQcKr;7MEP%?UOQ)ZrQRKL*){jF9qcSO2N4%O)gy1 zTZ|G4qJ}9AMO&!d*o7U^4aNv(K}DSP!aD;})Q_GWHM$^Ps8(=XgadQd*=MX?GZs8& zy}}4x4MTr)gGSiEbQUTgBc!7dDp+O-2$L8vw?-!jC74%mLVaNNwc0QuOGDzFt~YP4 zVKPP4$H#Z3Ds4F9d|`Td8N6>?uwJ{ard?OM?&p2aDYr9WNYvKBAg}g;oo3kH09(HK zmcM9tjiW$=${2!6QQs|C#xR$DwyV!myRM`5d!alB@6b}@k&k?2NTTr0FxPgg;l;~J z>sJ5R&wu{&@CJAxgob0I>;lECV>NA@u;2W%IQETj&kq(RUc2^W8Msu@7a?KrM5Vhx zdA%8Gl!$dHY%eGWRIcJ3hN>QN4erX<`K0o=kh_B9q-un9Alj~5cP*oxoN7JtH>rja}D$5GuqW;?4OZu*!ix3`# z!^rH(2b71PlPU6a&_M?&Xm;NEaMd7!3RA!E!V7sof)e?4vaYTY%^c&Hkx_TutJhdL%nN+Ey3 ztca9$*=3iFts3P(pqWaw%}ST12}8q~4$Q8P};e*EJfzyJORa-5(rDmytKpRarK>}5l7AF6_=8k?aF@|9O! zDdC;fk<+MG4ggys__`#N=qW;>IPD)TS%c_M6e$I~h&{}kT&Vfv78hT9Aw|EYrY9N3 z>eXXyd@e@LbFnU=Ls{ryGAG7F`mXBJ9&o?`K~_s35$PZsxm}l5zs!g-_h@OHP-&cO zcx~O}l4Fg)?m_|%hn>r6X@-Ud*Q{O5ai*>f*N11XUF*(K0|71^Lk0uyq+i$yPVX_S z5Jo6*mE*(q72A^-{>oRra(w5c61GAsxY--JJV!H=MNqa@xDKflC@$g$iXfG;#hGuT zFHK=SDrY0(q>5Q^7ine)xzGsFCf-y=hx9oyLf#aKJJ!E2n+R831Bc7fMokK!{mO~HD z!!{6@V1!yY&A-;7{yh60c-38zMP51o{PW!gh)ZoG&`^Asgl{ELma4}Zg(g7?cCF2k z&^BZIsBsT`-~$8aNTL#4cmcg&ct5atz9lV020F1)UJ$~En{Xi^pkk%Lufx@_9VvV?wD8tr zD(bxjleLn*fgG6^4fY9}6Qg2C z<(y>~txKQ_)AF%#*|X0+^VVB$<)+)Wx2+yDk`VeUMu=23f*RHccadZkT%p0OD`ume z_NYfaN{U^L;FX?3E^4*RVCntMZ+`QOU;Ki*NGIl$$&As0g+TTm=3ll}U4c=-ZqGXF ztXpomMXj&o?M@VAU_cQ7PKpOB%i!9L-;4|&K#LUOm% zQd*`)4RNB1z z!^K?TtD^+@$3FHk!bh1mrct75Yo-3!v>K}BpTRRW&i)^+zwW&A&gHTjHmvh>z@2M_ zh$o##9Vi}(ONWY=08KDdvGKqL^e_osnKg!dd_B6P{sxjB@M-K1?COBo6 zE98^dK7(6n{Raad_;tf-WvYYZT7Yf-(x(+Po zv5$Q$5>E)oOf@WVr}h+%UlwN&$S%C_f^U86TWt)MQEZ5q1X0t+AAkJEKmKvqFst=B zch!a!%h3$*8r2q?yhN)8^A^gmQP)@O3Le8g|Jl!erkq0QbFEn*BzZ{V6e(cv9G??U zJdukBNhRgIp$gziV%Ulo<@KW<{b((aRarB1J0nyCnodH9$YAec>r$7Dam%j9+Fd-Nul&*~$VQ5w-D!{P0{zVsEbj&fw za683qDu#^*dGnj!eD1mDqUNHWhL#b*$SKB#S3`LbjT=zNKosb2NIE>}q?71Ttk6L* zGbxVEK9{^Kp~58h;&DFt$xpHblKtgN69Z2b#$_rxbY@ySnq zGJE4qZ+g>1Ve2Q+*Au8mX&o&nKVy)Ti>A zId2eUYA{CF6&M~wXq|wP!vtN+((n$Bmk14F~3(=+HF7Ad(K#}G3lzkS&bd{+p_>EWS)smbxdf$SZ(-}b!cKKl!w|7@j`TkM)- z6$>+&wCm+IJvC9LVnwvesv`~;Xoubp8kqj%xJ5ZXkc&#i=}fwu%cicm`ighI>-c~B zz#w0fnr!50?v~4_WjVsI>+{C%=O6W= z!J$=?Q^hDt1R!$)PbyU7YI)6E;gd^O8|b7;<(W8D$YkkCN8h#lhyU|~H(Y<+@KA1g zY8>iCj{w&Y9Z{Q>XJtB!Wm2U~xj^(^ZZJENjs{XOBT4fUyZ&vJezQD1PBg@lnHi!O z%D?~pwQqjQ>u|Yp~|M8o9 zxJ+5!WUZ2XT_W3(;3(Q16-vd4Ot!dcD1FXZr@!)_UvR=Z-+K8ke-`J*hcZmZG{1AH zJU@5c_NOm?<`h0~(udy9>(N{~o|(>N(wRt+pGEdvIx|ox#@V5<$N$4qQW1^Z2$YJ) zM%S|-HH`qPt@8Yk1_p;GCZ^y|w8_k-*fleQnbapf@?Ssy`TtB$PmqOy9>r2(!j#d{ zxgO~VM|ZtwfIIU$9SugQ0b0ZrT}!vfJtmLkMId*VQhsRWT{?#vkt}wkbRhSwb zNd4*hYu@y_V{gCtPpgIo3sd6}pUI_%vQd6!s^G$~2L^}6Xw^5FkDm3sf6iydN&{lQXrAmY3%g#)5 znzEIIjmVwTY_3?2^7ue9Ir{HmVc=Ty;3)gGOJl!?OEkkP`d?lzsvBYlAxLc&$`OVUbGhI%nis|go6pN9`?z`W84tl@? z$Ux2v4o%YL)HNuKt6eG)Kg&tw`{y$$lvBH!U6!jmdAII^@oo(W7ng6{*utS>7}7>t z(11flI_dNwWUq6SzjhwxOiLT@TaN1g^h|{XwS7Tw#=uDY2*wZ;M9GY@z3^9gL=f<+ z`CfIN4!a5;Kq{1#?-QT+1ko$l50&67x!4D?uU5`O2s&3_p_C!j#|qhI3I~-xLuOmq z332@36vY17V51|?a`)H2{`G`Q@GZuGsYk|D`3rgHxzBwrgHd+8Qa-{9->o-poc@6x ze9SSgMovfX936AP^oZ?>l@*1h=%t)POiKu>@gBfRlk1>Rk~+<2$en-JyWc_JPY+3a zupD`bP>E1n6v~0+^NnwO=I&;KryQzjv-)`g0OKwh|L1bB3Gqsv_J&|ra*SEgNf|~87SKXD3+9M>|hBY z*;ZU=-`+BB_L+P0Kj+_lU%#2YJ#%NeXL_c8znXe#&2-;B_uO;NcfRj@i~LIBiQSBk z{iMgsy7)JL^MCeCj(c^}31uA8)zW| zM?NAQG=!f{AG1~21Ia>Sc|jFfGX+pCEY3@4hjG{_t6Q-xR6|;Ao}8F)%8QTx&hJVu zU%UEBmZT@6C#A+8Jv#NOSG_7)Rw`SQdJxbEdx8eK=Ccml^jfDKk{>qx+reE!d~$N1 zgBCi+p6ku}tmd{euIVGn_juJ+SH0#nuaVz}%r8C%(tNgP)Wnp?Q1sG>w8*;DBW+0+ zVp=S%JNlN)(R41j9g1E@la5TU&wu{&6|JELTRa!FM-x?FBwvO%z3EL7(t53Bx6%yE z_l`_!fg&kgU&-Nyx%c~e;??Y$c+E9e>2OqUo|z4sfa&Su(J4uLh`DgH>Eg)f(B$3; zE1J=vXI`F-`h8p+>qi0)=z?-JI{$)~`kZD5rK(cv?U$>Y)kaD;sm{FOiYvbT?Qd%f zV&|q@5PBS0>V zFV4+Nl!aT9ialH(4R)RMh8zCz4Hv!s>tFx6BzI9l(MmI&O1P1zaObH>TysW2^yi#& zj%P~eRE2;b>}#}y=`2k$ObfJ1QoP{}Z_vimv4+k-VaIo`CN1^h+SE~#D}=M`+fh^3 zk+{;vS>jN;;7c#PRBOF<_pMQJQF2V%`9)JpJ6s=K=sI>umyp&KB>AKQKH&*Z`0Qst z8@IG`4kheR5ZLOeR#ml1n^v7gInGJ{>Q8#1^-B|2_bUMzucJv`vmw;(wVmSvy))2u zUM>90XMXS8bI<+W_r9m)Pq!e^ZnaW+p0i|0cMJkQoMF}_7wt$XVXwP5=yx4vc9A-jq+<93EzGS^{+HaXa%Qhg=; z>1}Vj^xnJgR$uuLI?sydBi;)yyih%(Cp9}iH##z2WaG`9dt38JS^w6M{_{Wo(<`oc z|Mk~jA2KWL-4jzuOiNV6LWaTe8J_*}g0?AY>rl`>r>CZl>ZUYE{`wn#C7)!uR{G>j zs$Q5B%S)2eG3S|NZFO>T&(&97b?vp+=*rVGD*Tz)lp?iK#mjsWp~(8zz3z2$3-h(1 z`qcDsC(X(085PYh^^!*H*u?13V~3sp{f%$@6**NMJ$hIzk$qLoo}HUkPwtEcpV5_GtxfhSq6-$)AHb=RE=c6!He{O5oEms^e;eZU9&oegXGbkLcCET#Ha zH+A^%QEhXF>ccHL8Y#mBC(DJM(ooH6jcMyik}=wMYkRK4bFFLki_!97UCN&QCky)% z<36{5dCo|@+4d!inSJWyS|VpleUT!=e(I-w>d}vRj65Kev0gh|Ev8zA^zC}HzW0Ye z{0B)dy}s$Dn)<`E6>i(Jn+DM+PTZpb;FH6RD0B_=r0pC^>M)( z+Zh8rdP!)Z*RxKSmy`rR7HN5fEX^18hPh!x4-+ccYf~C zkACGVU;WglK2-xi&&1BUa&c{#uay@mRjV=$JjwCD{;S}``}cqUcRD2wX7hS7J1tol zOd!%4C4Qth?%TglgH6(?J~SfRsm9M+-tv|_d)&n2-jGs26&`Bgu=%yGef8J>=byy5 zi=iV&j;U1=#+oAz4LVsCrcKLDIG2@p8yy`F7KJeEvMa9mO{Jha=SAnqOd@%_l*aVp zi{Tc=MuzOnk}TD9#F^>vd}ik8k;8xRh0lNW`oH+eZU3tF1j}lz>hjvl1*1Lh&)gf< za&c;E`s}mMmO(`$d~8C|!wd2**u_cVVV4U+EjQBJG$p*X*Fb;Y``)LD2jBh9w=K!+ zY%RlNy92XoOL-<87mdfHO=0QZw{J2=i75@~r2YO~)kVs~%{Sk4ZtWa2|q^@Ytd1IJDi7G=hGy2^r03VxndrjIwqGz2}pauD;JF} zE~W4@zyJI5Ge;}UY---yc0Tj4^J>$7J@bj?;luY|e)(l*obh1ELOII++~Xeq@P|KK z5^-*B_OwHX)CNg$DY!S^a+B=FH{5W;?YI9V7@sHh+;`8t_45<%bomj%jPH2?yHG&MeL>&HVVsH{B7BNKZfY zw0rNrPs2~|)PvTJVRU%tg)e;JU;pJ_e)F6EI|$TuoJO_QvY0x0Z2#U6AS8FKy^|An z+Lz*&#|IYl)*?s1RPlR9wVpilBb(T_WC(~<4p{q5f>-sArJ?+yN+ayiMC zD$8L&?%%ghN9Ve5NPTe6y$?R~j3Y-iASdLfqufop$4`IyQ+m*szxO@bv`rsZ!lUIq zdn7G_Hhes8JvHjCyz)vX#f<_+qHZWUL$xOyCy$Qo9T|@2d<)J7tOlr&SN-nqTz$<| zx{Iei zTm}1pDsWnMG4VGv|XJ{NSZ; zy+nyF^Xy*|Kbm{4o!!m|98{=ax)~eOCR!`L-ll8nfA!jViktHCMDaqeP_1sFmV$Xz z2Tv;JtF#f-;fbzYuJu=|aErBe?*SM_^eC2crDgyRWM2I->Yawb&LGqF1&7o!FB)R%T8Yz;yo6Za(6gCT$_tEx-=ea{dV|G zGajHmF*3few5Uj)|Mo|Jr0e*&+NaJjP9XH;C2xJJtlU#m$5eo%?Lk`W-lcVuB#la2 zH0|d+o})c{&#`02|Kb1s;72#z@U^f1g)Z2dTySP_HaA)ZO+DmkTedy_z`nir+;g{W z-5TfGwJgLS2engIU6!Ngg7PknfB*a6l_^zWHX$4JVi*8%uI&vTt!aAx1+VzdcfT(S z=h*n3U{eULRYUVJd-cgamW{n;eOM+C8S{sHc(1D+J9_xE)6a++XU!#ibrSzafBZ!s zUF$%vH7jqu7V{#F2G1HD97^Zan`M&GoE@JC2lX=r8!vmxB(XGbIIhncr}Zy>@k`G> z`zcdW2gfFYt*G9}W6aVIJMxUsF-gwS>3pOb z2rzi)Nfq`lV}+f1be(9dP*E5xyPA)65ueU#+rERwx-&t`y8DA4{6O0aYwEd=jK`RF zBtB+#Ry;c>>7iII>KW-2O>evHHf=zXR&jRRnQClC?-R z*N+}Vi1a;vRDL}nGsb*jV1AmNqBmcXCzKLFivWSQK89 zC$RSKoxwyC^`{^9h-(tf z*oj!_JQr7VO7`sj~w-bdQ!*0&U<2~ zm`d(^e{##Mqj)5^L+r;JJ~bc0b?&_V_DZw1yWMcY<{)%+=-ZdS{N)$C;uY$DX(8$; z{kZ=<8l%XoB&*1)CAjLkH+)U&cj}{AKchxc7=^>>&C;TxFHh4d7~(6Oq}OkA@=t{! z${WRFCZ3-i4eqkzBQc2LZh4e^_q*TmcX^Nm`^wPJ{(}d9{WpGNe2*00kh4vaDcDYj zhoejCnByX%u~Ge#Uf(NhT!!LaP5B|;LpP;o# z+ES(IDyj9zIVrRal#jb)WFNXsI~82+E@;Oe-O&d=@B#T3cvbQ$tVymv=~Pc=9?|__ zd3A*#3gFs#BZR1OApe((+nDm!*Y zSZHLX)8lY@JR4o3GS`r%vN|3=9uC;#`XmkB)`j5oR2_18hi;@&>kt5*CRwNXB7x+T z-)f12M`POFpz+^u!;KQ;a#=Q+=lT$jvIT%UHF`m1hGy0vsH`+51| z9g?lDY*H-2QAyI(H;-uUHPMT>+xUw#b+ruo3pqn_l^%^;{LDJT4OzKme;(g_4a@kpL{dkT0+|8j@4|% zX}SDihw8Q4InX0&yv9Ro-hlEOm&Ks5$VgbL%fCN#_;Q-3;gH7C*b0OcJc~MYyYkB4 ze#9dlX`NnMu><>0@#$m1>}!Os{b$>e_dKm5wkV^iy1=&*|-2zN4Gi#1WT`h4RX z-+0p--)ye|PYg}+0|!pgv6tS^)Ckj3KW%4kho7=YayL$%rl!mHdLNDxmt1#{yX~TO2HB8_HJmld+ULYZcH?rzpRj9*TN6pX>Dih448=9G$eb>9*b>4X|SqR~ni}9Uv-bvW1KQc7j-PX4i{6P!Mmh)r#{45}(#i&$%J@P{gZe=hSo^G)wIa^jTTnd_3mmIG#?8dS~E+ zKuaCgl<`6|hrQWDmN8a7_-J~ec0*pPHDa*ggHdG+aLior%bDTU&z%{l>j z%dcLnh+91ZH@js7BL!ji3PP{Ba(Fus(M-^yteUh+95vF!c_uZhb+XWJrctex+Inj- z()yy*ajpp!>`u`x>MgmePuTbGzWbD6b<|F(GGs{{I&87h44-O7P-QLU#kXS8ur z8@ol=Q$eez&MZ}*-RH7S$kK=P2rc<+mXz9^=O!*a-l8HR)z=P9x_vv~1TAwZJhIyG zkk(w+zif`a34NoorakOo539tadhO0o8(XxMrqQ(wq{XI;4=a6LU8H=bS0hKk1d{r;l+_XQK~CUJDJMBb$A&h2+GYP(fgk%5~Nz zKQg}lDQ!;vzI(Mmv02v=I(Q?=9ocPUFVJu^6MzJ7m$~qWXgAgL*e0)9!p>#ll<#hBt|j_FhI9^DEMJ@>7SV-vpT)Rc7r! zn+s)CFXeS9c``k?&29$;L8e`27_kk1)7@Xlf_<3!}oOhn^Hfk^(dSpgvJCp)|u>4nMPF38HfYt{vo?n zyeb!Me3+pWU>o!WW{g(&Y_^^=amzS-rb!kzpH^DWFkvq{`$P15Q1th^N3BO~H4nFl z%uZeF<4Tq)~$JH(^lBsKXZw#n*4Xi9C4 zZchynwORnJ{>Ez-zR0c(lQ#+M(>d?ih^i&i$>WFlt2)j^XH$M1~ zBfCvHgI_*h4DKG0IddTJvc8Ugf3uKYxiN&HVERz+gMbkRMjOB#jX`D5sKF=ae76QakXW(z-y_f-lvSSo7?EG@ zN0@&w4&PG#9;H=}{J0`BaPx^pd8uVqXYm@?VtADdSNQ`O!il?gwfb409WlSFkP7!@ z{>zlw8+fuficKxdHO*2PVe$IQZ$BL5PXY+U3~Z~+rHk=;WXa1RuNcAh^s>u=NgPwq z)kct!94)m^u3qD+9k&ZqwSOuEIWE!feUz=->w~B8Osgdw;C@mhs3ohLYGZmcnWNC^ zK{;>C>hGQCbPwyx&+BsQH`uxYQc{(G*XuvO&J*J7uSvYY&2H4+fAoF0Us;vTxvSP` zH#+Ac%|UiAvzsr?(7skFUuU z`sgziIbxVVYtDsR`>8Yag@EMA{6?((r;`wNIWwUQ8G)OF0>2l0tz__ucT&tj_Vg3)t^k{>|uA5w; z8bufVrt@nB&K=fbkJK|L%clLijJW2qC2aCh_`?CO1dkD$qGw_UX=quDEB$dnpUt0hvpQanv+wpk`G~N}7OQ8x&3|}?W zdWFd0`3a)XtN%sg7>en!Z!EmJWwra+_=$z2qmPEIz|Gz_7I-h>Kw*xdR0>fTi z`1l;A*$R`6;0q#@voiUXt^#%WnAuAC;-+Ys=(GfQz_WU8`gg9-Wzvyu+St*}xg)zg zU)BRc_QW~?+DEqddJ2EBXBO~FL}fU4l#|@6T$QItCwpApEpn&gPKbAXJrDG3v`wPS z7cw;PPD#qqHCE6YtCus?^tHe{ax2V8MACS!sVgEqR&kh)MYP|^obkc4{cn2ymq5v- z*pCHs)oJ`?pksvbX|#h(7fNMwI`&iIlHn*c-5kb%d;Yy^pw}WB z|A~Q*oKse+Si3S@qT}|u-Kb*jJW_h-jY}5HxWi~)i^C`}Xf{4+{8H?$!KRGn0TG<= z(uBV_>97iaX2sqhiKc6gf8$hYqjNIh84(dQZ9spg)z`}kmtrJd{JDrCUTaIK1fB7m z$0>A6m)?C`51gmR)0pH1aBkV~8dYACFPo-(FQt%qr)kgnEW{ZXq>>(J zPpjVMG*%8I(2rblUgEWa&m63#@98_lMwi+MYtN;vmen3!t>{bn)&LxJvM4^NALf(r z4eU|&gg<^>!n5l(y1ceVmy+@jUmG{|^Sp58cMU1(ha!FvkW6!JQ5(*NLpsQ6o1MM%tQ)M3>_|8_h`IsW~xZ?f@0JH;JX zjK7`$yFY=7at4^Pt&1Skb%K>1pL+=Dn(evYX<0AGbQ7e}wb+qnLVoe2b*wd;-dY54 z5WeWt;y~xy_xsOSj)s&=fET|}rSFA%cWit-whP-%I9ATDFw1th9<{iZPuyAlNiquD z_E~r-kC?!ovPcT?h>hg=!^mtRDFvV>VVq_+$ZcLqGj$jemS5FM;K*snX}Vn}%He;X z9|$-R7|kUcv?r4=WK}57Cb{eL+8r2s>f^bzc+Gn?u5|vRFF-=#>sO8CUxdO0hygvs zyI)h4FMR`%*!dNmS1UA2iWFMfJOIz>JRNN|a;I*@J9a&v6XK>7BWL|*3R66p+E6Im z%}3_f(}BPzP40Wur5g%4jx^a(tIy{@Fk0SLJc}%&Z<_a*Rjw1&>FdeAs-RvxX?yUN z#C*jL&Q#_Ej=Ly>y?rO*0Cp7bE4S_S;?Q*|vzlE%*c<7D&p>ciNgpBv^S5Q{)Xf6F7to(CQqo23= zer+Y`jgA|$_7I!r4WH(pAj4ZEw7wR^^w?Y*#+w$=MFk>CR!dv?%&)On{W#g}5K{i< z1RB9T%j4sl#D1}wDclwd8r^8$zChhMzhvP{NgIF9; zaQkOzN{IO4iDF0w&)3}ART4AL#%PSeG3In+-G*`0kDVPmc*(=7RnhC6-@YPt4F$u6 zD|kYx&LtLyp;sKVT{qt5BT7-fesIdJ(q$i$0e}M?$b(|U;}S~7=~Y1iTAj!qtrS=8 z`bQtm5yt627!Sa_iC0Lb$!U*%>d89Dkw+>&4t+Uc!nPgzxy7MT+NRIIWA)*-ui{5l zV2*LRvc8oS>Grs((QQ^*krGu-HPxWqDA=?CudwRSGieIFLYDhcC@W5CCU4rIrn#_U z*JWL;bNx#QS2U_ny~S=}c%6>1*{JpFQ)1PxXX@mw&(?)kf7ehfJOrGi5 z5a|Zb&v~8ck+T)*jj^WneEn=lF*LN4mneuc8&xu(3v0K!Xr{YpF}*k!zfebiYI~{? zGePHTR{l}6(I$?Zp=Q1+fl7l)TPIu2_s2#%z8TlH21tM zsSl)0t;*C+-O0wTeXz;g_hTPlP&4h^E1g(2sW%;!F1*^lLQ|<%6=-DcJnsuI-J*Rh zN9Tb=xE1eo?xL%6ahA3C)3EjIGP1JEBi)7#L8aUL&~;iL!AfZZz{ZEzym(@9;_lgK zLZzwj%c$6OmzaqoKBJ$b)h=C(;FxYj${XWmTYIEBSx=W*R-+u6im7T|bZ>byOLVW!`NG0Zh%KT|B+^qU@R8YFP(e5ca+pk^{1GD4TN@Zoz< znfh1dhX=&k^PIAqz?$H#`~7NS0(MQIC<)ihahlodgU7XKYfAe)?T8`SU(v$U#U(2T zpzV4=%=7OtI$|(b55%vDg^bsT14LYlu)XI?C-95C%p%5_OWy7d z3;5?Z?sMNDP7%lRSD8C#xf7}Lv7(FRIQS2+{~~6^5d^D;7m*&Gslm8ApUop@Y%vvh z9r?wuMLtUVIBoUGjrx=hzh)C@kWnt1n+Lln)`sj+~eQ06; z=Z@E=tzry;=}q29u9;Bqc1j*%0rs3g9|o3_k=m!sckXoS!~tWBW>*fg&jZsQvO{6i z)zU8a*>OG9CElO6p}XqMulLs?Dxxg-%$xh#y4M`n<2z2*GJFN*BQU@68ITWt7?$eK zxN^&~(zB)9g4dGu(Z7@ePPrMh+*Qwe$~;@yn-}#(#MgK)p!2!eQ_|neQbPh!B*hC8 zeu^A9KRy^@kv^XN;#ex7fxhoB&8SjT^Ti98xBg46{BZ5))CuDx25^g-w4lq+&^9H$ ztTu$2c1<{ui=2w7lkuhFJtSyD8RxZ~OcCh&CAYt%N8|SXg=q65T@m@lMO>>iiNR-_ ze>uxh^G;T!I%4MP5UFmb3HeBPdyxsXSYIUH^pgVQfPB;LrBjWhvNqOf1!wed?A3&D z%BKKF6(~^B!QAj`mSH+Sr>}U=O|$whgC$-}{)IU~K~WYnv{l9`17QwfbeedG*M2W0 z?6s)c;kg$-Lpe9(DaUaA8CQQz9PEg*&59)L*n3zPs_b%
0lx7!|nArDn>Yw14p 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 OL_k#?l?o;6kpBYrs{ { + test("can upload an image", async ({ page, users }) => { + const user = await users.create({}); + await user.apiLogin(); + + await test.step("Can upload an initial picture", async () => { + await page.goto("/settings/my-account/profile"); + + await page.getByTestId("open-upload-avatar-dialog").click(); + + const [fileChooser] = await Promise.all([ + // It is important to call waitForEvent before click to set up waiting. + page.waitForEvent("filechooser"), + // Opens the file chooser. + page.getByTestId("open-upload-image-filechooser").click(), + ]); + + await fileChooser.setFiles(`${path.dirname(__filename)}/../fixtures/cal.png`); + + await page.getByTestId("upload-avatar").click(); + + await page.locator("input[name='name']").fill(user.email); + + await page.getByText("Update").click(); + await page.waitForSelector("text=Settings updated successfully"); + + const response = await prisma.avatar.findUniqueOrThrow({ + where: { + teamId_userId: { + userId: user.id, + teamId: 0, + }, + }, + }); + + // todo: remove this; ideally the organization-avatar is updated the moment + // 'Settings updated succesfully' is saved. + await page.waitForLoadState("networkidle"); + + await expect(await page.getByTestId("organization-avatar").innerHTML()).toContain(response.objectKey); + + const urlResponse = await page.request.get(`/api/avatar/${response.objectKey}.png`, { + maxRedirects: 0, + }); + + await expect(urlResponse?.status()).toBe(200); + }); + }); +}); diff --git a/apps/web/test/handlers/requestReschedule.test.ts b/apps/web/test/handlers/requestReschedule.test.ts index 09e01ce308..b6ac6cfab8 100644 --- a/apps/web/test/handlers/requestReschedule.test.ts +++ b/apps/web/test/handlers/requestReschedule.test.ts @@ -268,6 +268,7 @@ function getTrpcHandlerData({ user: { ...getSampleUserInSession(), ...user, + avatarUrl: user.avatarUrl || null, } satisfies TrpcSessionUser, }, input: input, diff --git a/packages/features/settings/layouts/SettingsLayout.tsx b/packages/features/settings/layouts/SettingsLayout.tsx index 95f217cb15..677c43f5e4 100644 --- a/packages/features/settings/layouts/SettingsLayout.tsx +++ b/packages/features/settings/layouts/SettingsLayout.tsx @@ -10,6 +10,7 @@ import Shell from "@calcom/features/shell/Shell"; import { classNames } from "@calcom/lib"; import { HOSTED_CAL_FEATURES, WEBAPP_URL } from "@calcom/lib/constants"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { IdentityProvider, MembershipRole, UserPermissionRole } from "@calcom/prisma/enums"; @@ -145,7 +146,7 @@ const useTabs = () => { if (tab.href === "/settings/my-account") { tab.name = user?.name || "my_account"; tab.icon = undefined; - tab.avatar = `${orgBranding?.fullDomain ?? WEBAPP_URL}/${session?.data?.user?.username}/avatar.png`; + tab.avatar = getUserAvatarUrl(user); } else if (tab.href === "/settings/organizations") { tab.name = orgBranding?.name || "organization"; tab.avatar = `${orgBranding?.fullDomain}/org/${orgBranding?.slug}/avatar.png`; diff --git a/packages/lib/getAvatarUrl.ts b/packages/lib/getAvatarUrl.ts index 2c971be827..f1d11aba42 100644 --- a/packages/lib/getAvatarUrl.ts +++ b/packages/lib/getAvatarUrl.ts @@ -6,19 +6,28 @@ import type { User, Team } from "@calcom/prisma/client"; * Gives an organization aware avatar url for a user * It ensures that the wrong avatar isn't fetched by ensuring that organizationId is always passed */ -export const getUserAvatarUrl = (user: Pick) => { - if (!user.username) return AVATAR_FALLBACK; +export const getUserAvatarUrl = ( + user: (Pick & { avatarUrl?: string | null }) | undefined +) => { + if (user?.avatarUrl) { + return user.avatarUrl; + } + if (!user?.username) return AVATAR_FALLBACK; // avatar.png automatically redirects to fallback avatar if user doesn't have one return `${WEBAPP_URL}/${user.username}/avatar.png${ user.organizationId ? `?orgId=${user.organizationId}` : "" }`; }; -export const getOrgAvatarUrl = (org: { - id: Team["id"]; - slug: Team["slug"]; - requestedSlug: string | null; -}) => { +export const getOrgAvatarUrl = ( + org: Pick & { + logoUrl?: string | null; + requestedSlug: string | null; + } +) => { + if (org.logoUrl) { + return org.logoUrl; + } const slug = org.slug ?? org.requestedSlug; return `${WEBAPP_URL}/org/${slug}/avatar.png`; }; diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index e9eb598659..4a2f79c891 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -191,6 +191,7 @@ export const buildUser = >(user?: T): UserPayload allowDynamicBooking: true, availability: [], avatar: "", + avatarUrl: "", away: false, backupCodes: null, bio: null, diff --git a/packages/prisma/migrations/20231114090318_add_avatar_url/migration.sql b/packages/prisma/migrations/20231114090318_add_avatar_url/migration.sql new file mode 100644 index 0000000000..2d539596fe --- /dev/null +++ b/packages/prisma/migrations/20231114090318_add_avatar_url/migration.sql @@ -0,0 +1,19 @@ +-- AlterTable +ALTER TABLE "Team" ADD COLUMN "logoUrl" TEXT; + +-- AlterTable +ALTER TABLE "users" ADD COLUMN "avatarUrl" TEXT; + +-- CreateTable +CREATE TABLE "avatars" ( + "teamId" INTEGER NOT NULL DEFAULT 0, + "userId" INTEGER NOT NULL DEFAULT 0, + "data" TEXT NOT NULL, + "objectKey" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "avatars_objectKey_key" ON "avatars"("objectKey"); + +-- CreateIndex +CREATE UNIQUE INDEX "avatars_teamId_userId_key" ON "avatars"("teamId", "userId"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 3130a9d423..8cc61111fa 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -192,6 +192,7 @@ model User { password String? bio String? avatar String? + avatarUrl String? timeZone String @default("Europe/London") weekStart String @default("Sunday") // DEPRECATED - TO BE REMOVED @@ -279,6 +280,7 @@ model Team { /// @zod.min(1) slug String? logo String? + logoUrl String? appLogo String? appIconLogo String? bio String? @@ -1011,3 +1013,17 @@ model TempOrgRedirect { @@unique([from, type, fromOrgId]) } + +model Avatar { + // e.g. NULL(0), organization ID or team logo + teamId Int @default(0) + // Avatar, NULL(0) if team logo + userId Int @default(0) + // base64 string + data String + // different every time to pop the cache. + objectKey String @unique + + @@unique([teamId, userId]) + @@map(name: "avatars") +} diff --git a/packages/trpc/server/middlewares/sessionMiddleware.ts b/packages/trpc/server/middlewares/sessionMiddleware.ts index 46b55b6450..3eae4eb1a8 100644 --- a/packages/trpc/server/middlewares/sessionMiddleware.ts +++ b/packages/trpc/server/middlewares/sessionMiddleware.ts @@ -30,6 +30,7 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe { + const data = await prisma.user.findUnique({ + where: { + id: ctx.user.id, + }, + select: { + avatar: true, + }, + }); return { - avatar: ctx.user.avatar, + avatar: data?.avatar, }; }; diff --git a/packages/trpc/server/routers/loggedInViewer/me.handler.ts b/packages/trpc/server/routers/loggedInViewer/me.handler.ts index 3b53cfa0c6..4c4b72f648 100644 --- a/packages/trpc/server/routers/loggedInViewer/me.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/me.handler.ts @@ -25,6 +25,7 @@ export const meHandler = async ({ ctx }: MeOptions) => { timeFormat: user.timeFormat, timeZone: user.timeZone, avatar: getUserAvatarUrl(user), + avatarUrl: user.avatarUrl, createdDate: user.createdDate, trialEndsAt: user.trialEndsAt, defaultScheduleId: user.defaultScheduleId, diff --git a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts index 4231d45dc2..c790a446ad 100644 --- a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts @@ -1,5 +1,6 @@ import type { Prisma } from "@prisma/client"; import type { GetServerSidePropsContext, NextApiResponse } from "next"; +import { v4 as uuidv4 } from "uuid"; import stripe from "@calcom/app-store/stripepayment/lib/server"; import { getPremiumPlanProductId } from "@calcom/app-store/stripepayment/lib/utils"; @@ -31,12 +32,35 @@ type UpdateProfileOptions = { input: TUpdateProfileInputSchema; }; +const uploadAvatar = async ({ userId, avatar: data }: { userId: number; avatar: string }) => { + const objectKey = uuidv4(); + + await prisma.avatar.upsert({ + where: { + teamId_userId: { + teamId: 0, + userId, + }, + }, + create: { + userId: userId, + data, + objectKey, + }, + update: { + data, + objectKey, + }, + }); + + return `/api/avatar/${objectKey}.png`; +}; + export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) => { const { user } = ctx; const userMetadata = handleUserMetadata({ ctx, input }); const data: Prisma.UserUpdateInput = { ...input, - avatar: input.avatar ? await getAvatarToSet(input.avatar) : null, metadata: userMetadata, }; @@ -114,6 +138,15 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) // when the email changes, the user needs to sign in again. signOutUser = true; } + // don't do anything if avatar is undefined. + if (typeof input.avatar !== "undefined") { + data.avatarUrl = input.avatar + ? await uploadAvatar({ + avatar: await resizeBase64Image(input.avatar), + userId: user.id, + }) + : null; + } const updatedUser = await prisma.user.update({ where: { @@ -129,6 +162,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) metadata: true, name: true, createdDate: true, + avatarUrl: true, locale: true, schedules: { select: { @@ -186,28 +220,11 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) }, }); } - // Revalidate booking pages - // Disabled because the booking pages are currently not using getStaticProps - /*const res = ctx.res as NextApiResponse; - if (typeof res?.revalidate !== "undefined") { - const eventTypes = await prisma.eventType.findMany({ - where: { - userId: user.id, - team: null, - }, - select: { - id: true, - slug: true, - }, - }); - // waiting for this isn't needed - Promise.all( - eventTypes.map((eventType) => res?.revalidate(`/new-booker/${ctx.user.username}/${eventType.slug}`)) - ) - .then(() => console.info("Booking pages revalidated")) - .catch((e) => console.error(e)); - }*/ - return { ...input, signOutUser, passwordReset }; + + // don't return avatar, we don't need it anymore. + delete input.avatar; + + return { ...input, signOutUser, passwordReset, avatarUrl: updatedUser.avatarUrl }; }; const cleanMetadataAllowedUpdateKeys = (metadata: TUpdateProfileInputSchema["metadata"]) => { @@ -230,17 +247,3 @@ const handleUserMetadata = ({ ctx, input }: UpdateProfileOptions) => { // Required so we don't override and delete saved values return { ...userMetadata, ...cleanMetadata }; }; - -async function getAvatarToSet(avatar: string | null | undefined) { - if (avatar === null || avatar === undefined) { - return avatar; - } - - if (!avatar.startsWith("data:image")) { - // Non Base64 avatar currently could only be the dynamic avatar URL(i.e. /{USER}/avatar.png). If we allow setting that URL, we would get infinite redirects on /user/avatar.ts endpoint - log.warn("Non Base64 avatar, ignored it", { avatar }); - // `undefined` would not ignore the avatar, but `null` would remove it. So, we return `undefined` here. - return undefined; - } - return await resizeBase64Image(avatar); -} diff --git a/packages/ui/components/dialog/Dialog.tsx b/packages/ui/components/dialog/Dialog.tsx index 06971f9745..836367155a 100644 --- a/packages/ui/components/dialog/Dialog.tsx +++ b/packages/ui/components/dialog/Dialog.tsx @@ -177,6 +177,7 @@ export const DialogTrigger = DialogPrimitive.Trigger; export function DialogClose( props: { + "data-testid"?: string; dialogCloseProps?: React.ComponentProps<(typeof DialogPrimitive)["Close"]>; children?: ReactNode; onClick?: (e: React.MouseEvent) => void; @@ -188,7 +189,10 @@ export function DialogClose( return ( {/* This will require the i18n string passed in */} - diff --git a/packages/ui/components/image-uploader/ImageUploader.tsx b/packages/ui/components/image-uploader/ImageUploader.tsx index 1f2c5089e5..6f77756f07 100644 --- a/packages/ui/components/image-uploader/ImageUploader.tsx +++ b/packages/ui/components/image-uploader/ImageUploader.tsx @@ -170,7 +170,11 @@ export default function ImageUploader({ } }}> - @@ -190,7 +194,9 @@ export default function ImageUploader({ )} {result && } -