From 7745c5f66bd7ee1b3a560418266aa2b103e3cfa7 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sun, 31 May 2026 04:01:04 +0200 Subject: [PATCH 1/4] feat(chat): 39 nuove chat bubble (253-291) + paginator soundboard - 39 bubble custom con relativo pointer colorato (colore campionato dal fondo di ogni bubble) + regole CSS in Chats.css (resa in-stanza border-image + anteprima nel selettore) - selezionabili via chat.styles dell'ui-config (lato server) - soundboard: paginazione 9/pagina (griglia 3x3) con frecce + indicatore, cosi la card non cresce a dismisura --- .../images/chat/chatbubbles/bubble_253.png | Bin 0 -> 695 bytes .../chat/chatbubbles/bubble_253_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_254.png | Bin 0 -> 1154 bytes .../chat/chatbubbles/bubble_254_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_255.png | Bin 0 -> 806 bytes .../chat/chatbubbles/bubble_255_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_256.png | Bin 0 -> 1151 bytes .../chat/chatbubbles/bubble_256_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_257.png | Bin 0 -> 551 bytes .../chat/chatbubbles/bubble_257_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_258.png | Bin 0 -> 880 bytes .../chat/chatbubbles/bubble_258_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_259.png | Bin 0 -> 1433 bytes .../chat/chatbubbles/bubble_259_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_260.png | Bin 0 -> 381 bytes .../chat/chatbubbles/bubble_260_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_261.png | Bin 0 -> 891 bytes .../chat/chatbubbles/bubble_261_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_262.png | Bin 0 -> 1217 bytes .../chat/chatbubbles/bubble_262_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_263.png | Bin 0 -> 798 bytes .../chat/chatbubbles/bubble_263_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_264.png | Bin 0 -> 645 bytes .../chat/chatbubbles/bubble_264_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_265.png | Bin 0 -> 632 bytes .../chat/chatbubbles/bubble_265_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_266.png | Bin 0 -> 656 bytes .../chat/chatbubbles/bubble_266_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_267.png | Bin 0 -> 534 bytes .../chat/chatbubbles/bubble_267_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_268.png | Bin 0 -> 625 bytes .../chat/chatbubbles/bubble_268_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_269.png | Bin 0 -> 1244 bytes .../chat/chatbubbles/bubble_269_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_270.png | Bin 0 -> 661 bytes .../chat/chatbubbles/bubble_270_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_271.png | Bin 0 -> 1080 bytes .../chat/chatbubbles/bubble_271_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_272.png | Bin 0 -> 1482 bytes .../chat/chatbubbles/bubble_272_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_273.png | Bin 0 -> 1202 bytes .../chat/chatbubbles/bubble_273_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_274.png | Bin 0 -> 325 bytes .../chat/chatbubbles/bubble_274_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_275.png | Bin 0 -> 1763 bytes .../chat/chatbubbles/bubble_275_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_276.png | Bin 0 -> 326 bytes .../chat/chatbubbles/bubble_276_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_277.png | Bin 0 -> 455 bytes .../chat/chatbubbles/bubble_277_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_278.png | Bin 0 -> 533 bytes .../chat/chatbubbles/bubble_278_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_279.png | Bin 0 -> 326 bytes .../chat/chatbubbles/bubble_279_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_280.png | Bin 0 -> 408 bytes .../chat/chatbubbles/bubble_280_pointer.png | Bin 0 -> 112 bytes .../images/chat/chatbubbles/bubble_281.png | Bin 0 -> 825 bytes .../chat/chatbubbles/bubble_281_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_282.png | Bin 0 -> 831 bytes .../chat/chatbubbles/bubble_282_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_283.png | Bin 0 -> 1101 bytes .../chat/chatbubbles/bubble_283_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_284.png | Bin 0 -> 1309 bytes .../chat/chatbubbles/bubble_284_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_285.png | Bin 0 -> 1265 bytes .../chat/chatbubbles/bubble_285_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_286.png | Bin 0 -> 408 bytes .../chat/chatbubbles/bubble_286_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_287.png | Bin 0 -> 906 bytes .../chat/chatbubbles/bubble_287_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_288.png | Bin 0 -> 1079 bytes .../chat/chatbubbles/bubble_288_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_289.png | Bin 0 -> 573 bytes .../chat/chatbubbles/bubble_289_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_290.png | Bin 0 -> 879 bytes .../chat/chatbubbles/bubble_290_pointer.png | Bin 0 -> 113 bytes .../images/chat/chatbubbles/bubble_291.png | Bin 0 -> 548 bytes .../chat/chatbubbles/bubble_291_pointer.png | Bin 0 -> 113 bytes src/components/soundboard/SoundboardView.tsx | 50 +- src/css/chat/Chats.css | 468 ++++++++++++++++++ 80 files changed, 506 insertions(+), 12 deletions(-) create mode 100644 src/assets/images/chat/chatbubbles/bubble_253.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_253_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_254.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_254_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_255.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_255_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_256.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_256_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_257.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_257_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_258.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_258_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_259.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_259_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_260.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_260_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_261.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_261_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_262.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_262_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_263.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_263_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_264.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_264_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_265.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_265_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_266.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_266_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_267.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_267_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_268.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_268_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_269.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_269_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_270.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_270_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_271.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_271_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_272.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_272_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_273.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_273_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_274.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_274_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_275.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_275_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_276.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_276_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_277.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_277_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_278.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_278_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_279.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_279_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_280.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_280_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_281.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_281_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_282.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_282_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_283.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_283_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_284.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_284_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_285.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_285_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_286.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_286_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_287.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_287_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_288.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_288_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_289.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_289_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_290.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_290_pointer.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_291.png create mode 100644 src/assets/images/chat/chatbubbles/bubble_291_pointer.png diff --git a/src/assets/images/chat/chatbubbles/bubble_253.png b/src/assets/images/chat/chatbubbles/bubble_253.png new file mode 100644 index 0000000000000000000000000000000000000000..b727a298f6409076cec0e536ff9e94221c9f590e GIT binary patch literal 695 zcmV;o0!aOdP)g`60_9G{#D8b=QwqWagLzad+d5JF}KB&Ccj=)N{{Q-1SzsAiiBx7WQ5 zD&vz=I0#`Ni&7W_xczk#ipP$SJvTc`^}@ZoLEp>eN00|O>hdsM*!lUJg2x)+xXr%u zxltH&uB0XRmbJq-_ca?qX?Ft%qylgog?$Lf(#@5o1iq&L!eNk4n>+AjYddnl-24L7 zPaZTvPXQqs50}X1O=0ET$I#g4_ZzVE^cl?Gx{(TuFC_s5%Pa~6e{}>E;^k?$GA>81_{B~4uaBt0oVwlg|QW+55{JY5RC00sDZwkZBZfZ8}#my z*20V(AV{h(2Mv-MOco%iz+?rII82rxiNa(Jk{HY|Ab2|DvL6+dvAuJI4rh0+RE#Ia z{ea+?#i^4clw;7H4u{p7wY9*mOibuu5Tw~`299dA5(_j4NUuy!Bo4IYc|9>!LqHCu zXgJS>PS_udEN(ek{$7;#Qai6IAN#rLvY(sf;G zF4pVZox9>{Vr0YFH7Sz@wOS1p>-Ah@^mW1&WQsAgo8jZTP%cBB002ovPDHLkV1nRqJ8J*{ literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_253_pointer.png b/src/assets/images/chat/chatbubbles/bubble_253_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..45955f68ff364c0d5ecae4df4d422ed3ceb03676 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{K!v9J=(xM z$GWVik@MJrr3@?%T@o8LT^=<*O?+T~CCh;K=!{hpR5zS>7y>lm*)36lwZ)e}1~PcM L`njxgN@xNA`lTe{ literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_254.png b/src/assets/images/chat/chatbubbles/bubble_254.png new file mode 100644 index 0000000000000000000000000000000000000000..b4a100da0889ec7cb12a60abdbad8ef7f074ac67 GIT binary patch literal 1154 zcmV-|1bzF7P) z&rXv;5TC7R@YpuKLdyXR7a;NIrG+<;1JB_j=xbmCk;D@i+mkmEKrdo6a1rF7fwX-9 zifd+PcjwQyZ7_t3b7;SBcXodBZ)bKt=e-B#oQHcP@4dTkj1uMyf_@+WfgfWJYqJkv zW8-C-1#pg^CE>Uni|;rRSHR|**MM@*#yA}@u05 z9K@W#@wdBs*^bN0pBV^2lzeGtdU39b7nyc;_p-J0>=;AxCK=J6VRiHrjSEK$_zWec z5J#z)CRUa@P;WL_dPbu6Y^)K;C`qb=;a~uVM}5O2FyfjZQz)O6uE>q^(s*DG;OxL) z2IH2OI^m@mK|l1i;4akdya-y127`cbaM*{HWx}Y}8)2?@M+{5d?s{;DMqmO$RGaE2QPK zQniR;HZL5!|K|U75vN<6J$R$7+xAj`&9Qd?h@%AQ*pth;&Gm5MY%^- z71pUB(~#C^Zs}YZSs%ygU#Deonlw&&C^IUrx)?F$qdh5_y@-fXCX0y8$ht7&NEwo^ z6j@(%hGNfYX{tbOtdUa0(wN<2x{~o;QcjC0S#o5VbjjD5;EMawIy4s)S&L-2Wgnxy z_eB^{D&NMQTUBry3GFOfNkQ?OULyciwd4rUzirTRkFobA?JGVOm8+#SVv-{`LOSZc z%Ab#O1>qzvt ztgxDF>#$h){2{KHr99@baBxoG_QsS9(bxd%4;&4X4D8XWewA|TypFK4i4!E2O4Gxa_@ayOXK5viV;W^eJ z51n_90B`ZOXr*N+H+dSL6XEB8Y*2j2#>Jca?@xp^b4Ge8EYJ>>B)`?T|M}D2UoUZp UWRAh8N&o-=07*qoM6N<$f|>FzEC2ui literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_254_pointer.png b/src/assets/images/chat/chatbubbles/bubble_254_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..ebd039d82321404a4967cd02175b078d50e014e9 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{CNLT@Mr^f z&i#!&jhx30EM;JE=#to|>GG)gY2pL>D_I7-M`x^>pt|A2!w{ee=@lZ57i}$91I=LY MboFyt=akR{06+32hX4Qo literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_255.png b/src/assets/images/chat/chatbubbles/bubble_255.png new file mode 100644 index 0000000000000000000000000000000000000000..f0f9fb6f4675ed854e4bca934818c271c06c06fb GIT binary patch literal 806 zcmV+>1KIqEP)fUa@K;zjNY?cg#A7<1#g{`Bo0L^9p(!x?QMmIibk zYd`0i<5d|vHSsRGw&2sY3bWNkP5*tcIFX`i5aX-_3aE~q;>9{TAAUr3)hso+DJ!|gW~%seE7O#fPyd;Ry{LMH4Iem6%Ns#SiE-PnmO=`~<#u(Z7{<4*>D}l*L zW`_cI0wN$ME5`c~Iu?zj(b&SnXgGW0EVrO&~^)BWN_|E7oL9w288%BXBh&z4Hs_y*T??psfx~gVx$BEU6 zo8`((Qp!#*AiRQ6dj9Hx1fsI5bO}&b;wmdWx9`K;`iVx8webu0V*ONdk|Q@~l)L~s z*>pVT+)HuzNE|*AuZ0u&CG!Q0v6;s7`aCWkZEtKC!vTqfucH>?egW|mW`k|vLa+@} zf9(H#4GnXuDNI08wF2T^8m~1F_u7FQ$ZK)o;Or+e9a?nW;fAtO>UD>8l-aqR&pAmRZ`22>>m#@U? keyd5&5p{hja+l8k0P_UrtTP7nCjbBd07*qoM6N<$f_S`bt^fc4 literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_255_pointer.png b/src/assets/images/chat/chatbubbles/bubble_255_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..147f587ffe969a3e9eaa66bf6f4eeda902c8fe33 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{FtvPdaQxl zr+!;kBj>RLOBq-kx+FGgx;$!rn)tx}N|pid(HW~IsBSp%Fa&5qho4Bf*iRltpcxFF Lu6{1-oD!M<*PYbhQ5;8XmhhF*@wTDo$c=3VpVFI z4!}#u8#Sgi4e1rN5FvvIs{-h$IPg#_L`ZPm>NWP`HDt$@>KcUJIM3=7BxDjvPY03y ztQyc&CX`1c^)o-PAI;XjDI`e}bh);7`tl+tv^?ggP9hP#ZENelI>uy(NGy7U>pJ!v zMQEgr@6WV!+U$>wb}<>+FG2`c`$e+if8B|FA1B@Qb4qj6^!CFXyM&@43XmcVfZtDYLC(7-!w9|A0b~^T{LO$L zsP-qI4i%26()Z%r2MYs*%|O zDR3JNcI5LU+fv_9YmM>6TBT!tGLC(xl9*`SiOZ+>&7mj=b0W-fOW9?{>vW+Pm-@!o zp!3Qj%$+rs5YeUJg|M7xLi!^glDdYMFDSgG3xR#nv}M4ViHMX6Z=dUC0chD}mC40e z`_mjW_qh;g@N=KmfoRvZEjild`YI)n^&ojzmBEtc!qgu+-l<1P zkzO;BSNFvpRwqvx_5R~6IUIax$EN+P39^EF1sN3t85IP%96@NmR0NsXw1jVdTf?in z#r7sMYZ4i5Hj>1)B{N%QBA$}DpV@(TpWBI_{`BOyUtThYo?T-?gmMv*y>wn~LbBHZ zatV1285IN>6$F{i0rbW>FxrFdh;H+Z`T7q-Qbj3KMIUtRw2sY$!>yy(4vG-?lu5_t zy~qDO&sc=ad;H%aHN?2g{-Ck`r=}Y}UgWaz^%GD0U$D(Ud&Ihb<+bBn{sVW}#%li% R4|)Iq002ovPDHLkV1klUAvgd4 literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_256_pointer.png b/src/assets/images/chat/chatbubbles/bubble_256_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..0a95873dcf119226526862030e8c841b425cf260 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{II{>*~=*I zarb+7Bj>RLOBq-kx+FGgx;$!rn)tx}N|pid(HW~IsBSp%Fa&5qiJwShc%@McTD#4jKe2c1Mb zQ5?jfU5&h46R%#bNw0V9#Xl5Mcz5sp?!6>00ZJ(eTL7T*Qi#HLqz({l>*P#nS6rBj z#J^jizMO+S*6)ieEUSTvYJ8NW3@TqJb?xlq{f+cf(2BJaq{ti^ z=N6aPcw}@eki&ihqO5A!7m&fZ!T1AlSME=|GQhU7!$1J#fRk_TH&&CKzuXj?#on=y1gjc1O-(2uv%9k27y>k*E?A6BqO~djXa<9) LtDnm{r-UW|zW*ZP literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_258.png b/src/assets/images/chat/chatbubbles/bubble_258.png new file mode 100644 index 0000000000000000000000000000000000000000..755e5e147960d4fc52fe22fc6082f0171c5e7545 GIT binary patch literal 880 zcmV-$1CRWPP)# zPe>F|9LK*inFo!PH8op^h0dc(rk+AC{ZYC_bP7VEgF<2#LHY;%LGT*btsoXW2wqzw zf-E6&H%#|zh#>wcMd{#fyF2oGZ{PgiT({ksZT)~VZ{9xN%x8YTd9&{=P#9|Vuk4Wy z*`7(Mo9FpIH^vI>$2F=75koY{D>wO})EHCCRaE-RXIcuXY9BBBTmY5;bhh&2$PQ4y z44j2Eq6hWu8d9!jNsAUiQY6t}5F>i(xHLu7AnG9N7LrqiYLYQZ5nDl^eVNNeb`YQ2 zH8oQ@e0EX7Agi6MF$VSRwIaDV^xr$B6^#XzrQp{EU5F47aS#iIeQf{z=B~5olBFM_ zvlWu`T2J><5!n%Zmp(Gp_*tGzg${)3Ikdz+{xOHn6;Z2K~gV5tQyl6(}U zDAg|@KJAB&Jz@x@q)TL7k>%XW8B)C+3GPAsOd`@t1xm{d~@*07y>y*vq+X z0-2^eL;FS==bORQK>_^O6NS@jw^+&DE@d(!Z^(#R!<_ z*l$+$-MP({EPlOuV@^dOcg`F&x`AEeGpzh^>_VxQ3;O_U(ngG6luh#?(GiV$L6 zE*n}=jgo|us0qt+W|8xB0a(gW&!V~btNpK=%Ov9%_FS&r-+$0000i_@% literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_259.png b/src/assets/images/chat/chatbubbles/bubble_259.png new file mode 100644 index 0000000000000000000000000000000000000000..e603611be5a77597d9246bfeecd13f96500ee5cd GIT binary patch literal 1433 zcmV;K1!nq*P)X!lDQH$@vZ_VJmw`B(DEdk9!{*Be zr-CqNi*s!8!@3V!O*gDs)*$PP`=VP*LTfb9z@^=0i-Y#a^W1xG&P{IGG~Qfq`iF8~ zIQKT^m*?#~Cya9rR*Er}^Fq!!1Bc6P^~9rnuhTIx7RedoWFV|qi(uO1zB#@DJgbAy zKjMOmgPF;1-Wb%+#r^Qd1CE8^E*3y&e*;a{vwD`k{fmPxy6!p~hKF~Cz`vmx`iIU# zC=j5(@P!Ki`_<>22*X`00KYa-6k5dQo2o^;{K)OlxHSNY@d$(hEwH=oDH?O8<4d^f zmTpL=);Q8t=5-C09!z>Nr5#L1`&fCC(OyqT^Ww;m=42*6X<3>zQxz^Cu* z)ni92J^=^ciaQ2|Yc2qcF^&K*0OG_@p1KyAyuW^yVCWD4;L31PhmaWWfvV-p2m%Vv zUwr#G!F+7*5cs^+1lKIfIL0*?WBj9IZ_s4LFb~*rwkx=?^;gh+nV+e4AkIv ziM$`cDXw7v!6XQB49Et6{(bi6B?RGe-yb&Pz|caIT<#Zuaai{UZ>3bq`pfL+jrw7)TBXlng;GlG3S3 z-3r=Iw1U+dI5z;#A66?a1GcP2BqpcRY|J~(-v;)8IDMGqpKnuHZA?@+(} zD5g#IDDK-KOr8IyT$oaN!=rj;kcAM;?prM^qJ}$1Ei0soks$zVs!{YTfwIFQ1O$xg zj#zyvxOiH(luGDLHR?TIV>QI$3WOIC1Sy3Y)>&i`oO6c760RX+A(T?8VMhb3-?s)L zAKJGZzjw@=Vw)q<~oMTf8O=lHciW#k=lrUP_ znxLnnmtd4SU>-Z#K`^>%!-n-oPPN1}%P{LRluC)KzY`2Qbf0gQU|^@cDU@j>o0g57 zb7nUX<*!W3uhe>S`%)?*5k^7ne6E>|QyQ#R>OgBVc6miQzUMmqeZPNb=i=Wan18rQ7b?corgm~mM zfUhD00q?uZ#Tr&*L6B>l%YEwZn(%i4B%NBKyGnLnMTSroAcQrTK`<-&w)oX1FiJ_f zAQo5S2Y|XonNN<65lpiNlo^D4&xmtReKiFA%ivN>bqjg9ko}&r0HG8J!R&eYdamak n?;1*x5Q^Rn2M5Z|-&*|(0WE5Seb%}p00000NkvXXu0mjf`=^`~ literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_259_pointer.png b/src/assets/images/chat/chatbubbles/bubble_259_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..b370fe85856e63f0bde96041eb9c1a335a8f552c GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{E)Z2JDE{D zV&2ZVjhx30EM;JE=#to|>GG)gY2pL>D_I7-M`x^>pt|A2!w{ee*Io#R%fu~v0W^cb M)78&qol`;+0PXN3`~Uy| literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_260.png b/src/assets/images/chat/chatbubbles/bubble_260.png new file mode 100644 index 0000000000000000000000000000000000000000..706583d16e3ffd6631e211105e36740b2c8cff4a GIT binary patch literal 381 zcmV-@0fPRCP)ijIpR`AI8A!_>>O2HhRd#(IP8%(&BO2mvSJkI(|FgSr#y?*GW1K~XSNz&bSdGQncSGkzMw?AbFJ zh7y=PdnVZ3u-IY2rhpb8$R-O+@7#2dVfNaqG_iw;fldJA30eathG7Jl2-PP;0fs72TIHg zH4kk^b1$qp1BxARzXqhh#H)!c!(YFC180khiV&raTySPv9Y(i#KnFA^F<+j*fCe%c bl0!`ZN>Y*G>m%}m00000NkvXXu0mjfHDRFA literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_260_pointer.png b/src/assets/images/chat/chatbubbles/bubble_260_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..7ed9701ec1870dd53f847b0f7ffc610d4709a292 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{8+zq>A?o> znB8T~jhx30EM;JE=#to|>GG)gY2pL>D_I7-M`x^>pt|A2!w{eeCaI!*^M#`Ifo3px My85}Sb4q9e037HfYybcN literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_261.png b/src/assets/images/chat/chatbubbles/bubble_261.png new file mode 100644 index 0000000000000000000000000000000000000000..e6881b4858de3bdbbfe4a9699699d0bb54e026c6 GIT binary patch literal 891 zcmV->1BCpEP)X1^@s6oR$`P0009*NklT}ZQ}5Q`iHdo3(d99Y|#E|n;Xl~$L=|G`4ARXBpTkSKb$ z5Dt=nzYt9D7oy=pK(ogich<>flhxY|-hIv6o!J@YoA13h^HzdAbo~2}oEjL3>FMc) zk)cossZiFCeSzTSV?cdzoL@X9F36VOVr4HtfZlD>9FB`*BcKR?gZ5g`)c zn3@e&?1_nG^yx&p+5ZER2 zrG$Ph&e{#yMX!sqCF8ct!O2Lfsxoy(Mn-H;`u+WVw#$8eeYn29cC-{Tfq>`)-eAc> zfU6>sF$t-vTK9K`iZ61%p`jrh9v-rIpd!r45_rvHyNEXYospV+`d|Jx`vYix@#)s? R!3+QZ002ovPDHLkV1mUSs)GOk literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_261_pointer.png b/src/assets/images/chat/chatbubbles/bubble_261_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..11d961572172b361f719ce9aeb1b617651c9dc9e GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{HXu&-<~bz zz_f{-jhx30EM;JEP*FB(^{Clk!}F&-ig(#UiD1!9EMk^(xDGNfu-fn)TC=Oq6KDp5 Mr>mdKI;Vst0JI<>)Bpeg literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_262.png b/src/assets/images/chat/chatbubbles/bubble_262.png new file mode 100644 index 0000000000000000000000000000000000000000..42c68e6ca4d81e2d46a01034ee4ff09fa9a5b527 GIT binary patch literal 1217 zcmV;y1U~zTP)$ zPfQa*6vp3F&L)H?h#;b3lnBJAkpoFn4;A#Nn23L3i1DB?O)zp5FCJ_{Lc9_+)!;$B zX}})TjV5BmgEVOsq=2B7s4+2udeAX%c4xc0o!ZjQZoBnMpu4lX)16&(JLH*jf!efP0Sw0t2b7QD~G`SycCAg!u+9k>hhE?H-1$`UAxD7}~ z7(}zQNLHJBhvxO|aF+^%kR%X(z9qDZMjpE=_72T6r)Z&5T*<*{7;XfT5++2oH4~*& zijO%EYsgr`tk7X&at0xU-h1*Gep3p8t9MLb@^Ob-9OAKp=xFo8SS$uzt&LK@$PGYJ za>i^TVcVA(fgpSg3*%p*MGV~>#n9l6*tvBptWa-Ibe9=_*@mWI#xI|YoHyNzB*zdb zFaUt%iut*BaQ8wFOQ4n^H_f)8%N2sT`QU!u^&H*1Kj-9M#vA!tYjDrCyE+KtiFl5; zeGKffnS>^aj(tm04t6tGFhtMo?5QZ7(f zoN`x;#bWGpcVnY|vHmm~vM;eBjBkJBb}-ZNNz;DIrp+0FFc|TVAK}y2udHBbRCUQf z_d)$6sq64->}|eRe3op#8AP|;f`^$>Nh0wP(`I5Fz?}? zXh6xFfjtO!142M3byZa?Y%pZp79t|muh|RRwqG)pDI+^gE-BNxPii~~LEspJC)QGt z(p95x`!`pDzmLMNzL(4;$bogOMj%6@QRarOL!GKC0Kj)^&bAwT1cecwi1PRC02ndk z3=ZPJ#eO8i4m7tk!-o2L_#eb`hzI{6gy+#H1Wz4i2lt!|z_AnEP`7@)tvB&#c$Xvj z!ukYlnvpY1RkASY?QCbg397&QAoM_K>ce|AD67}{bRfn&k}|pTA{vFrv!`$)@fv1l zX5fF43C5T<0!59FM{NIcU&jkr>9KFXmB}Hfh9E$EX(kEd^6e4m?F4;%@o;bed_EtP zs(xyX=UVzc1*UOEP3=1Mh+)(jpPpu)@20QYdUMw22&eUBVFZNZ4CB6NKvpV_;BFA+ z3~G6Bku@`(jUrR_J7XXefWYx9DULu@RJIe_*G;m~#w};`*(v+=#{>c)a1dFqwf0pRt10d%xp~*>N>5E9R z#x(g133AFA)Uvp=oOKm^O2kat2 literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_262_pointer.png b/src/assets/images/chat/chatbubbles/bubble_262_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..ad0803df886767e3102f7cfe71593733b19606bc GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{MeuJJd7~Tjhx30EM;JE=#to|>GG)gY2pL>D_I7-M`x^>pt|A2!w{eeZr%cC!Xxzd1I=LY MboFyt=akR{02P5G$p8QV literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_263.png b/src/assets/images/chat/chatbubbles/bubble_263.png new file mode 100644 index 0000000000000000000000000000000000000000..eea056ff76165c420e3e88f6f4380e3284945ed7 GIT binary patch literal 798 zcmV+(1L6FMP) zziSjx5S~5GLNHjQv9K`3t%Zfvu2{tC#J@nftbH)zkANuJTw$Xqg1-p<1Gyad4=AKc z(7*+WDQxyYEi5e3h(bh>mD%jP%$t4h?XKKyf?spD?|n1#eKY&^j`flx0S?de^aX|_ zNj#_knM=kO?U%`$7-E`dPaRhm%Y!)Su74{nvK{(H5ts)*WiA=-+LyVPvz#)HVmRMx zhrSBx^w#10^aH_&b0dt!Sv*{96r_hNof0m(irFA1SteFCN5;w~7J-b>wA7?<0bs|NZ88X&2o>W+k!QPI zr8q7GflZZMnOoj+w)5D;dk{y4)8e-gC6UQcL_CD_{K@We4hI)ZP@R4DIKA#%o!6Rb z;^%Yk=H=_!nR>bUL6Kz_D2PM=%xhvfZ5a(OAFw7!9zFe(sijZf)8aNrp(M8Z zbirweb2+3TiLfL;!cnld@eRyZAQ*xS;D4Loph8r4GxV^_>s^gfBZBMT6l_8@k5@!={ znw%*Hc`FZY!}QI2aAV;))QN^obd}rA7DL7mbsB4o`W%_o>o(S?qX;+l-2Z-Oh4s2!kj4re|flH cVSAbS7sx^PF?uBbHvj+t07*qoM6N<$f_=q<+yDRo literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_263_pointer.png b/src/assets/images/chat/chatbubbles/bubble_263_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..16225fe6711615d8190004692f6fbaf7b39410c6 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{HTBRFP$ys zfX~F~jhx30EM;JE=#to|>GG)gY2pL>D_I7-M`x^>pt|A2!w{ee?p^$P@>5eAfo3px My85}Sb4q9e03sM9tN;K2 literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_264.png b/src/assets/images/chat/chatbubbles/bubble_264.png new file mode 100644 index 0000000000000000000000000000000000000000..da3f3c29b1dfa55f84b31a912e54bd85f349cd4e GIT binary patch literal 645 zcmV;00($+4P)m#| zKOpjAUbv)}yGt5ty$__9>-~7|d++5)&KcbILB-Slp-*MA+i_qu1|c~U0}S}`wY<=FkS z8^K=hLo2A+?Ko^Vi_KIq_06{5zPkasob{hc>7M@2f1-sd2?AKdp-4ba$W+d+Y9Y-(iQ9@T7kTLKN7Y9^2)WGorMq!2F4OLb++$xvlS zrmeYBTM5dv=uqUuQe^!88jCX3CC^eYcAV&InjI675j(N-GH!e4vA*I)LgPe(rzc0L zB4tihYAe!=&^@!U-HawCCyDCfqEgKB<84r`ZMe}Yi~Yrwzqq`Do$allJsA<15FI4S zaelH>Ow&>_%qEku2u3z0`5)I!=T|{dqi2p5M%?N#kC1txl1S&m-_8CKL5>ECRtI5G(@0A`mPB!6J}8Z0ehB7>-8p z9~q8D^u`=~amL=7e^byb77OWzRJpqc@E>t^52AB`C^BKX1)raMs4>sD;o^%28mVW^ fvvn;yvctn~2=sv_mZI{G00000NkvXXu0mjfV^1qP literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_264_pointer.png b/src/assets/images/chat/chatbubbles/bubble_264_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..b8c24f914819af521599523b87d30c98c4533e26 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{HXta!;dY- z;pVBijhx30EM;JE=#to|>GG)gY2pL>D_I7-M`x^>pt|A2!w{eedmi(@%RX1Q6KDp5 Mr>mdKI;Vst08V))9RL6T literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_265.png b/src/assets/images/chat/chatbubbles/bubble_265.png new file mode 100644 index 0000000000000000000000000000000000000000..7a709b0398164d730ed8747f267b71f379cc64f1 GIT binary patch literal 632 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$>^XV6yjgaSW-L^LCbf_H74=_WJky zxsrQII-V~55?t~+&#BGnHR4oS2aawH2JQ|NJ&Xhs#77$U5+X_H0S zInFjM9i_bQ&-D}HKd-$%kD;1UQX&- zee>$(vj!{EGp0Yp#Jto*{b_R^`7 zcTdJ!-#!wbG39#I>!n-VZq??m-#pEH)2D{q;vcU*e6-sXw>A7$fk#y8@#%$CFC#Wx zP17)axz*46*V`|ZNIpH+Qf~J?;Y(f1WB=n9-#?9iWn)nP!YAT*;nc8Q6ZGD{ zoO5NVb?)KY6TYpecs@5Q*2-+toVco+Yk9n5A2DgG70<5V**o>^40#o|A@2&&WMA?WZrMaUC;RSleivl_Ex(LOi>J;u6{1-oD!M$ zF;Buk6oB7d>Z&n@C^|qiCXgUHI~rr6yMwbMF)oh&2NR70n;V0>qd&k!BMc5|+#oRy zsDl${-j%DzaXk)fX=%R%Ny~A)@7=pcTT2*YkTwV*T$)0PgvE>1%Y2^`WasiWV~jw= z$YT=1GENYK#y+yythe24HX%xclTKL1!9ViAd0Gq+098CLaPds#Z^!#(UmL3KKoK>=~>S zF`Px)wQmYm)W~C#Cx`a5lEiWLhwVJ-S=VKxpE%-u!qb0*Vigy09VYR8PEdbYW!~!r*8A9*PI2 zf%V5)Zk-b6BXZ{2K*uE>8kvfFa-}S}^TSvEeo~n7U5*qIh!DRzdxqTNl5aaI&pPt- z;(l>rkWa0<>3@9p&G)V5^8Ec`E3ch>N7r8%0q~ZS=F&0@gt!R`3FhU)&48oZH*LkI zkTAp&9weSfdBjFMk@8sFY{U~OZ!4Zid0X*B$`5m8Ng>5hNHG*r422X!A;nNpA@{Bu zFv;NEr(y3QQhB5O{RL@)$WLbAML}&h7uvzrcRH}NFc)~gqux0t>SHP5qCc!41Q`I7 qMn=75PW+z98TW1IQTXrw34Q^Zvr`;59`P9f0000GG)gY2pL>D_I7-M`x^>pt|A2!w{eeDbEF&`aiWh0L@_V MboFyt=akR{05(1)u>b%7 literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_267.png b/src/assets/images/chat/chatbubbles/bubble_267.png new file mode 100644 index 0000000000000000000000000000000000000000..805d7a8a507755e394f9e82e61180d6f9beeba72 GIT binary patch literal 534 zcmV+x0_pvUP)$RAP9+%$ znoNfQxlG_)<_;qqM;*=z%tS6l<*CdMV@$42PgS>Dy`J3P)5Vp)J zK#8Apnuzp%xo_!b>X{AnGVI*s@4sSlw=P;}+K~M`Z|HrV-Obg>nQkzn*eyxJ-E%(n Y8-d^Ac&dK&Z~y=R07*qoM6N<$f|XzO)c^nh literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_267_pointer.png b/src/assets/images/chat/chatbubbles/bubble_267_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..5b1bb693f1ab9f46b473ec6d0de6101e775990ae GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{P@3r+GIxY zgq(lp4zMIY2x4GTaP7GvqV)0LCxZ|2S*A1El6*o>2t_<8&|+a`a6ctf%snya4A2Y) MPgg&ebxsLQ0ROHd#sB~S literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_268.png b/src/assets/images/chat/chatbubbles/bubble_268.png new file mode 100644 index 0000000000000000000000000000000000000000..997541255592ecc7ca5841ee2c9a01b50865cece GIT binary patch literal 625 zcmV-%0*?KOP){oU;uYMpY6#(&bffvZnqgoblMb7okgvQ zpr=8?pF!$NgM_nc7Uct<0;fy;w5bt@yt6-a#vDdV=}AiG|#xsdu2 zQpT;xK`0MSg)|wqRm(}X7TDo%cnYZw+gD+$mZMgl!I|V})nYgxit@d%-ENnHeB|Tt zIEBQ0o6Y9OL2kDj#c@oGG0`6C;Cr~~00000 LNkvXXu0mjf2{0-M literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_268_pointer.png b/src/assets/images/chat/chatbubbles/bubble_268_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..5a4dac22c3ab24aa990af795d3bd18dcc464119c GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{K!vBJJ!HG z$Eviek@MJrr3@?%T@o8LT^=<*O?+T~CCh;K=!{hpR5zS>7y>lmotmgjmk3)s&$ zU1(fI6vzLwStY3u(@mmF&~prsBuQnVWMvJNMq)O}d-C);|n0 zbMD;D{C3WnuZ_U$l3H2VMyZ=`Tte6%U2ui{8l2f1QF;B^l$26t^-KuiYU8r`EC>qw zHCPs1t@1N_k7WT;Ilj5fVcXNWNVQ+raTP&SIHyMC%dsqa9hz*Lp0b`XG?kBhagVZ1 zPK_g|3gz0+f?jRAIO)Q~v|T1x2Cc4I!ses7e0IL)gt-7qBY8sa+Hcw3-#MKH%XvH= zr|eMxF#t6|fexDna#0q+utf9&t79YWwtLsFP1$bDaZ540=&m=h^|e+ucLTT`z%~TY zr7vi1s>6O63E^6eRW{w5MwMXcIpx0%zJt-hANcjQpul>@FhQH(EoJ{8G3_1lG_s`^ zt(8~rNac<-o&R?!@%*;G#J^uRW4Xwq6 z_!7bDj;VwOR@M34l{u>1w+8<7Q)@SU`dXzQoGw9x1gG><5aG?(3E!jj}r zJ2F$}Rk>o-Ze)I1kCCBXRe$j1Uvc7_yYcOzF?4*mUD+bhR-{Hgcl~^?XS%(zE+%IK z7u+v>Y2n$HAqbjxTm;STOk$VTLG@jhT5g87+R&ytKI=jESA)o>QWM7sT<5dJvyh;8 z>X9_P@+-gM6jfas--Ha@~Yj=UY4(L?|rUztsc4aVO$3wT2(e@ScisOY#xf<_Xr9i4_L3`p@ODD zf5L)f>5#6l{{e+O?5=BAz9DHz3giVqAw-*v?cal!kKkPZ+zhZq0WKzNQGg$~w;Hi$ zfzJ2T`grpLuZZ5Jph?Am4lVd!EHZQmMUoC(Xh|s-7ae$k_16CbSd^fzua8P?Qpz2E z8xPUYGA4O@>9%+F+)ukG*S2oA8m5ive)nBXO-<

Ow=qN;bVBrA+(XlF6O#`p4O( zkABClwmW=nZqI)?JE^XBX1m&P6CL|@ydA;H`tq#9NJVv(<+vfbUk~A0>0>CZc?ij5 z5{-?Gx8`y!=z#Wv@q%*`0k3(w#bJN)g|Bh=yA7W9H7_RA;ijQw%+BTT7Kpk}?VOZO zHsg<~jaXux%tS6REH-~U2}&%l&J&oM^!8>OnyC-%OE}(fgY~j|9W&F(W-M;(!KG6w zeCH(@q|~3NRe3tuta3TLB`e2Fn**QRZ~s;{*V9sve&Ck+D3TkqGLus(?@!cXJY9y1 z6BB_1n5$@HXC^6R!Sb!lxCZ-bSBkO){O5}NYd*hdh<^dv;&v#ulg|(U0000RLOBq-kx+FGgx;$!rn)tx}N|pid(HW~IsBSp%Fa&6Vv$9Z@sntyzpcxFF Lu6{1-oD!M<%{n70 literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_270.png b/src/assets/images/chat/chatbubbles/bubble_270.png new file mode 100644 index 0000000000000000000000000000000000000000..cebc39cc3fe53c661f96a45d76de9c9544f8e5a9 GIT binary patch literal 661 zcmV;G0&4wI7{{NKI`j*;YSmIiD7c7&;2?r7tq$EBbP)~|ih>B@;tRM41&4x*9oz(=PA(2k zB6Jc&N`3tXx7~R1#Cy(|CikLN%>Cg=a+loY*MCT#JOD|P|2sJ!Q>>J>d1*Cvc^TDFKDw}U9QcjWOc5HF7uvQ?<2_B7?wyA?nwp$~ ztLv*T_sUx{K48@Y40AOt)LjG|$qg$z4h`nu@u|)0Sc?7RwrWhSc>~4SwMcLS{R8m) z`poNCX8W>Vo{Du_F5UmCv;9jbH+K1wZD}BtyDq~1w&=%ud3*WsyKp0t z+ajcG+vfj|%21@+=56TM6c{3%bGUTt*GGQ8I@U9c8SSX-970lACfH0Tq*0D4ofDN| zT1ev@6`dnDqbd!CDK4OKOZTTrLDJ+T{opOAhJn2V`zN6HXTKk9#f42lLS@mB&4W!W zD8VjHu5wreWp^jbAZ80p5oRAy<-J)fw84VXQB_Jr2Osx-T!659a*74KWlHfdE?tU! z#b3cKjDF^dN}EM!9E9hAf+W6UE~`F1kt$XNsCLZO!EEzTmoUV@tsD!j_yWqZ#sf>$F=U~V7)wS7r)^yj;-)Z v6&ZCusCQgH1!*BH9;dpY3}bZyR5mdKI;Vst0M=_GRsaA1 literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_271.png b/src/assets/images/chat/chatbubbles/bubble_271.png new file mode 100644 index 0000000000000000000000000000000000000000..96166c74015988f3b145ec669a67d906e8b98a4d GIT binary patch literal 1080 zcmV-81jqY{P)4W2)d+vGs-~avRDWQ}iRS-gw zDQ6X0&Vkb5MM^tg#z@l9D5Xok`P1%*y7F@B zQb%uyDoJ+xyd0aCNFg`O*EIr1_KAw4QQ*U8U}OsL`9XUJV(m2)`0@Aqila^j!5lAa zf;T6-I*AZB)z$0L_8v-uRdn*tVexx3N+U(}a(j-p6)Vj9quRLWUmNGPIr>bbkcT4# zQI=S}V3lY?YDadWn^RR2HXP5L9;|X3VsQ&muQ6*GyNq1#fT=WVLvr96PF0$?4Tkfz zaR=Fa{@3RR4y__kF$lEX1t|%DjRP#rvftDgf7@Sl@#3?WZp9j4Ww}Shm;rvQ0_QIR z2lw#Tp{6_h8R~BX!fz36dno$p?y$7g`0NR^Pp^?Kh_4WlqI!h-JA^H|V8-WI23J5= zi+jpL4cZ-cB%aR{mh0=G-caE8^t@63fK4pD3l;=oyEmz^{>Kd$&#h!T*OUqBkTxqV`5>XGijho zFtU-NdJ(A~-+^l5JW})HY~BgmFUvD>t|4S!8T7*g)sy95kmN~dW5UOl+aLlJTlriR z+e_D#mqVV$c_?J*RHJJBwS!L2@s)w^1N2pffyl?Vg^5XA*i?#8XN%Z4j7;&k&s11v zimVJ`o28A@61g|@oRj5XWN(TPU~2mx+{j6?hSIXQ@<3%fAd_r#6bO_6HMKmCg@-Do zvqjdXGO?eB1$45}Hw%JvWKc})3ek180o4bNBO4&rkap~}gvyMhp>TV%QgySbG+`T$ z;YHy61V~dQ&vGivLb|3FL(7{*I4f2++e**dZdqLS()Nr@Z$2`qG%-WkV+@NAM#?zZ zEY8yKBK%(?Z;kK7z6P^*Bjo~!v?L|0@V6g7`IC`kmjmn?mOXQYv<$D~p#{gZ9`h#T z{s5v~5dOtBnFRvPMM!= yUR-aYYlxFJ99~>+qRA$b3Tgex>&A(O4T*nk44s~8Nixs?0000g%XCItl27Ofp@=61S}e>A7YhV8RK?$%3N(Yk M)78&qol`;+0IsYfDgXcg literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_272.png b/src/assets/images/chat/chatbubbles/bubble_272.png new file mode 100644 index 0000000000000000000000000000000000000000..80033eb42d53e35cf821b8f096e7d7e1f91a973c GIT binary patch literal 1482 zcmV;*1vUDKP)# zdrVtZ9LK-6l!n)0X#|uF7d9d|kW4_tAQIfdU;#59;80w^VU|tsMKauy860tj80G{M zvw_S%$Yzq^1j;CI%sKbLZA+0tt7U$ZbI-5m+&1^q z-+BDb?*hvgF|)<8CZt&AeS{eGtQKveD+FoSA-3H!B1Bh6(l#irRsj{q1dWj2>|k+yPHyuPMH$etK!E|L>!qD{1zn^OG`o_1=HzD)~|orgV`9h0?{ zgl2?^bCp^`{Qxs0Wbn??88F7$jA3vHt5a3+~p zwJNFrokp{&-#DgcPLQUTe2&RQZus-Y=08k3QmUR~e^1@+T6C7LN6)!o1n=4a1*d=# zZrHNF4@9@Zr#r$!Muhmm9;&%%q`=hVl-3}#J^2qJu&B+MwUX^RX+-)?HMj#q77uK* ziePuW;OmNG`Xz=g^&x0r&Peo_a493gUwGPX<8O zJ^=2cR{Uy%S0bWD;3(y(R4AL7Ay((;-EEyXd0_y}2T~a@7u(3ZoAH1pgU54y^)+69t6puMG}Y`GesEPpT1+l{=EFIWfJ69WW?O2CK10V>^@ zZhRarYBet04>a8cqJ;s!F)j$BV-vUo15_h_;roM4Hbb(7sAyqpS?&I5P3)0Bqf4;h zK??j^Ya6=8c~7$b&)X!}7yW?;T>y*$x{CvF-QWgSg$pM22G}ZdNV+)TA$<4BdA0zFwX?!17HgDw7+e+a;q>>D(cS~RF$WAz1C<&;cWq39TgjHP z7_6hDlz(U`F19$)#1#~f>V1U{djJX>afF|+Rv=eD&*ED;e5vFC!yo_} z_*LooFp&lpqU0IGm3Y)1uv#9FLCV{yS1B;7k9rG<>(*qJooC7E)@1T-0Lu`P%Nmfc zH6WQ9)1)!&lNb71&XT~Ei-DTIau8u@`7{b9u$2M`{T})A5(ATiW_xjbKOy+93aqTO z77G$Pnr%DC<|wW=BmaXbY54X7cuh91sl@p%pYwzeYl(7)i4$S~TePg9B6`()WF$Aw kf|h$p#Oy7bQ@v698yb9OrIIbV2mk;807*qoM6N<$g6Ao^;s5{u literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_272_pointer.png b/src/assets/images/chat/chatbubbles/bubble_272_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..e38a4d7d7a85057913a79bea92a6282fa9856964 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{HPCep2H|^ zuf%`XJ=>UH_tOO&kUfi zBqUKL9qX2y)#WV3e+DP*eo~KQ%RGd#Y8DV;fi>Q_jh+vmF%sY;IBe|3S+Z2gbXU|! zYJh>hJ_eRLH3R=ToW#L)9@~#VAb?kIUgLXr7h+jWd@QmD_tgd@Q>MXy<>b!6?vqDQ zf8h!mF1avk#>}`4|J3S{ttNV6xvt@oqAr$~Q?`n%p`>)dVUsWQaX8R>!z6??C}K>x zD{AED>T))IR%}>{>rJ=t(sh$tWGM9~MFIy~vc3c}r`yoy_u=h_c6fjFGQga&RUxp- z)YP%~+0z5R-_M{(9E?C|EODz2 zD2Gk)1nL9=4}Kf+nEvh29`0vHC&px^V{u+S%oYm-A@rZU_GO@1kWw}Ab&UX;?ld7K zB?V3AulKVYFBjf9!6;_%9TuNOeJMzrk^!eZ7xy1MM9ak+O2g>qYGB^k2j%ZxZdbr4 z&37)uD3b}jejnUaK=g#l20O`((0HcruAMo@p4WeD!EjZVRUkz}nt87+I$DV%m3!pe zAlPZ^a%`j?t(LPlr#Ue%I~!S@;~A9iKr^r-w4N0e6f$>iYionmVqq@n7Co@8p3mO< z&+r*#hS6vLL{v(E)*XfHzEP~ggUoamg8lmPU51Ym56j46q(rSYl<NgQ{Eg1}ubnoCfydk^#+Ebz(3m2Z zBtTNliQ{URWPeRjwJk@0t=z7c$SG3E}TPFO+G8z3ta!HCzDB{t}C5Ll{q6rue<@5&A9WH#f zB^e2(D;$CGEfS9*S@7+!v6;*WK$Bh-({e)L6~&Hx*TA+n6uSliNA!f^4FjZwo9`aN zj|WHegyN-0r*^w)Y^k@*y8j_6T#KG$jCM;w?*{NgB)wE8k7psKlxG>BVSnf?U?icTcEXQa^ag)0;D4j+FH?`abs2iimxO#7F{b@=PAbwhyw_^ zv{dWk=PV^40^^o^`uj6Gvk5kC$a1YLj{qCYgu)7v6`_^Fj{5?q literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_273_pointer.png b/src/assets/images/chat/chatbubbles/bubble_273_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..84b8587427adfad36b547b4b3d2d0afd10dc415f GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{P^#=eKMo? zgWWOb4zMIY2x4GTaP7GvqV)0LCxZ|2S*A1El6*o>2t_<8&|+a`;5a74#U~-E0W^cb M)78&qol`;+0M_3jn*aa+ literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_274.png b/src/assets/images/chat/chatbubbles/bubble_274.png new file mode 100644 index 0000000000000000000000000000000000000000..4e511a63c1c6dbab5430651b9ee09ded546eded4 GIT binary patch literal 325 zcmeAS@N?(olHy`uVBq!ia0vp^#y~8?!3HGHM7N)0U|{6&ba4!+m~;1rAzzb$2-}1C z;(QGoc1J;iWy*wYU%qZtF*!Kf zO~!LY-{tU?OQ+athhLTQ-8}n`(B*S0Uh6t}#WH{NXg^i(%d%K+*%jI1*k3buEDEiX zm=c?I@yXH;m-_Ws^NvsEUB%sDkX)&mv#fN!a4<;)5WQg&B7em5} zCO#NrG~OV*XdqEuc#ud12?VHETS_mq-FCOz?)^GDWB#*yW@j%&V;aMs?9QBX{+a#0 z|NH-QSpm&W!=rD)KXot?@BjP?Ym8pLs}C$Mh0*2f#NKDOv$nih)yJ|kVuQcYkB@(U z@*+O@^n(py^1{xy8aNoqX!}m=(m^yM(hMjk!~O5yo4S~VPl!YFK-wb`?qHr`iP9?eWpbRuutuS+;7ER zY{%&3#+Ps+M1ZTqlmSoA+Q7&lRmy(xyNm2rG|9gJMd- zgNX(9vAE3W9r9pyd=-U6^+lPLJnS7+qDEhC%S+{FR_`TI7$`DQ!+Qf58?FBMrD7H% zDHTYi(W+?`&4BN63D^VyZ%Q^CbGD&4Xb95ppV;JDQjNrf_y z%mS>fmKq--%?a^mJOeEa@8C9Kss<#!5C$|AUeScN<;o->0EGcBL`l+#^d=W@-RWm- zSrA|z>4v6MCm?Q2|HOj~15Cz&4L;qalOshsN7_&-RwF`V6SJdyVk44)6Ce{)$7uqt5<^BL}cdI2(E<7ktg6(51%_a@tMPmWF=*+K_6Vh zTOjxLL&+=1PCckm@-TYU>;UC)JKFkVIy&)e`!%= zL>31J^*#b)^97KPEMx8FtlpoW4Z+mvgc8eOZEO-BE-k?=3izng4-T0E;q&>Ssf{V7 zlQDvI_d8i??Ajm1YgX1g7bOvutC zAPVqm0-FS)nu^TC93-a#G}R2RF{3Ztzj*2?bZmApLP{!!Vmb{~6HrK`U@fT#nFX9yl1NAb(BX!? z+lSnI7}mZZOs#I1+#baLnt>~oM|-A-_Cg8mnF733*p13i3dQcH^jhFR$?8+BzGjFB zUq`5)6DGTHX%6y`6&-Nl`1~?XT?gbIKTP2$63}oqx(ahi!E>?|X;pzXJ&!^p3awZ` z=FdFaad|}%5miNCa#BxOPh}_2R2fRM`n97O5%S|;$&U!zT^4rbg>S4a>Z95tLRWjt zFe@tBW<$tja*zrNx?~B*+B`UHbz&uzLNOhOn$1E@Rfy1TA+dlxsgO=Y(^QlyyM=Yr ztl6|{TxVz@G}aD{kt&%)89Yq}ENT@$vRwyyYg8tCcVQk`RfPlr2h2`5L=l2Tg1y~^ z@;E1*6xNa{*xosLsx7e$?o0V zEj8y9n{0u&=43wn9c24o9n$Z!?IOm12(kP3{C*snnL$WZaea6@l8S{#Ww@oD%E?$O_){BjG?qJq}P0!_*A!w^qv0}#V0lJ;6J9onal8BdYT ze2j-TI%WRRQG0aQh$Fy-sdM@POY4xUztkl zQ1e1x&$zcRcYa0Ry7TQjkGk>HX*|#Ht8ttkqki`uz086`VZr4*rinG*D0K5w`~He*;0tfU1mz2{Hfx002ovPDHLk FV1n*(OyvLo literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_275_pointer.png b/src/assets/images/chat/chatbubbles/bubble_275_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..8a6135903b36241fecb5ce51556afb895ca2f59e GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{8%reb*_Q? zPVC=Fjhx30EM;JE=#to|>GG)gY2pL>D_I7-M`x^>pt|A2!w{eef;yt1KSC}G1I=LY MboFyt=akR{01W0Nj{pDw literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_276.png b/src/assets/images/chat/chatbubbles/bubble_276.png new file mode 100644 index 0000000000000000000000000000000000000000..b4de1a76d97404de6ed58eb13dea5173abd83d9c GIT binary patch literal 326 zcmV-M0lEH(P)kfk;4264$PiEp%oA_#7BsN3GO*WTS{Ql4tw{SS6Yy;K7ksQbvsCb5%KeYldC)l+J z-6s4%ikYTqH&TVzRS;)lGYmo8x^NAGapoaToy$}i%e*pBn>8bv`!Mo`rug&vRC= zlVDxEI2Fd0*fu}uT&Wv-!cCA-o2xaTri`r!+sar#wn4?trD4!i#&-eA?#o|-QsQoc zcN)7bV>8a?ZjTVW8>|l=XRSM$a=0sJ2QtX<%Z@mn$_rgN>!>3fbp#?gwRi2)jZc6? zp2u2XTxTznE(I)07*qoM6N<$f}ALl8~^|S literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_276_pointer.png b/src/assets/images/chat/chatbubbles/bubble_276_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..6683f6ea06fd2a5df50af84e3866f0aa28ee8f66 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{CNN5$&m)` zH@COt1f2+#}$ MPgg&ebxsLQ0Hfk3>Hq)$ literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_277.png b/src/assets/images/chat/chatbubbles/bubble_277.png new file mode 100644 index 0000000000000000000000000000000000000000..df7317ee6c0d48d05d0a6abfa511101bdb8b4940 GIT binary patch literal 455 zcmV;&0XY7NP)! zF-yZh7>3_FIt6q!dvU1XQrt2Z6uM^>(SKmU!O@|h_y;WF8V9F>f^*g`0W(PVxGHo# zx$9kXxn5hbq~w}sXwu8|<9qUcfwlyzVQ>TT--|+!g{N=<5cCbex{XlO1d5tKQ4=U?0!3X# zlaDc6C$TuxH_=U`T4w!ZL2bI}W>noW>L*KTnqTVKIe^gf3yX&b%~G3Kx4v0Bnq(Ll zesiMy#U|?tQ7&2VYiN5I8wU7EW9;?-Ofs?PiZB=CaLMmmV5b#;bYrGubx}OO#Iy5z zx%r}-uT_}`8PcE!yruxsV&5uS#_xhi62QCX<4)e8w_*OXjTfpCJP5_H4CK=D(UHAt zPK2nDy}FLkZUf+}I0005pNklvj2<6xeoW|Dr2WDh7w!;&)2n56{(c@8FyD3I<@EdYlCmD28$M9?0!m!obPPPZ>0X z*uiX&M?r05YIqn{n>f4cVhw1Fm_;wBV2&ad+<5mBuUE{TnrE{~d@CO)vgl4ZbqbjGR)svAx`3;~+Z^Ip)AIni=1&EaktG3V|KMSjOXiMIQ{ z->Nk#2&$a)*>BeRN_>XAQb*H&1r0TBt)m+{)6_U_7#wWtO-a7LvHGT~mHP+Vmv#5= z-h001qYO8H-j8~n?(Qw#l|9d+7oS{N!SeLjf!W$ew-g>-lyOG$neP0W&eJrZ|eY|>~&r7R)J3}PnHY~U~H({5hW4nvVicM;(ZLhUIzgPMp z?)t}*Pp2Ac78+Z8Ha%Y=_#;L__rQANo{r_qto7PUOAhgFb_a$JgQu&X%Q~loCIEx4 Bjw}EG literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_279_pointer.png b/src/assets/images/chat/chatbubbles/bubble_279_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..1d7880813ab076b09773be33ae057ba6dfb418e1 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{E$ywcA$Y< z$NFnSBj>RLOBq-kx+FGgx;$!rn)tx}N|pid(HW~IsBSp%Fa&7AFVdQ&MBb@0R2fNTmS$7 literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_280.png b/src/assets/images/chat/chatbubbles/bubble_280.png new file mode 100644 index 0000000000000000000000000000000000000000..9132a2f3ad9c52eb41bd519b8861e7dc5792b5c2 GIT binary patch literal 408 zcmeAS@N?(olHy`uVBq!ia0vp^dO$4C!3HE}%>QrCz`z*q>EaktG3V+GL%+iYB5n2d zmHjumW+XlkXl>OtNJ&ades_j3!orDt-2o<<#d8C9EBbwyurXHhc;8wz-OBq(s$Fm2 zZ=ZY6-_UqjQ_!pMwwa+XqkB0y_us$H{v*8bDT_Jx?>)aZ>F%G({Nh7%t!8d+-k-CJ z4D$RLa^pT#>Bld~+*lix7M}L~9!Jc!1K&US?~e*%bhFf|d-`bVSK&TZc1Abb$7&7s z%RfiUh%^OoI4N?pD&S1$j!HJ~{}E^V-Ct;$qMqNbLtC3Ht*jGj9^XpTYWSI%rS^V`Ivdybh2Af{ nt}Y9?pS-|Z$oc&Lg+CZ|p3Re$66Lf2hB$+#tDnm{r-UW|Qqro( literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_280_pointer.png b/src/assets/images/chat/chatbubbles/bubble_280_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..ef49fe289de4a5169d8ebfbcff99ed035f841051 GIT binary patch literal 112 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q|7~C978JRB&Q@K{P@3T7Nh!v zoYaJbCc^?v2F3|WZaG{NePF)Oe=*`r^rIIiIJhcl=sdyuRpOsBN#kg L{an^LB{Ts5L~0;( literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_281.png b/src/assets/images/chat/chatbubbles/bubble_281.png new file mode 100644 index 0000000000000000000000000000000000000000..1545615d77c9db485b05ee31050e1ef8e5a5b402 GIT binary patch literal 825 zcmV-91IGM`P)>yZO*Sce3k+lbf38J#W~j5L&n@UjWb}pSei$3zp)Ne$+KjQ?X{*!$xda&C-e0ke+Wme3{AKyI zG13bbZTOBy9z-1#Bd`T+l{z5jC*Vd3Ob17Jgsy3CY(di~Q|J(Utk4#;l{z27t&2cL(arC=^lUdhp*klM~ib#*ip!RIdM6b4XWlsVqo zw9?#EbE69R$*9BF+Su)Tp}%1Y7~mqs6|=H9=fCgi*( z&+`lpQvEapkQ$7qfb|W1Jic8uK_KJsSeK7AtE$5fjFgjCj<{a$)AU`?rl(88V+w5P z{ORekf;XuSQKuW3&_0-pZh9z;712)%HF$8(a)@MXiE5iJs3|ndJQ=6414F^VGcksR zc6`R`8`^jp$K~t0Ewpd`^SjroV@Pq!mV-m{c&&;XjXGT5f{zg{8>}A)51OiOV>b!s zfjPkm08gO{VC6kGp7PWIO{vc71aBtBjx7_^;00000NkvXXu0mjf DpDBaz literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_281_pointer.png b/src/assets/images/chat/chatbubbles/bubble_281_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..0859bf6b6e1a96039d8961630a3099382413ff22 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{5W6zOO9FZ z!_AP+M$TggmNKw7bV+Q~ba~YLH1UD`l`I3^qcc`bP~C9iVF=KKPwj#amU;($1e(F% M>FVdQ&MBb@063*4b^rhX literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_282.png b/src/assets/images/chat/chatbubbles/bubble_282.png new file mode 100644 index 0000000000000000000000000000000000000000..132af17b9457b2f1c4dcc8fec17be924b172157d GIT binary patch literal 831 zcmV-F1Hk-=P)1gh5XWbalaQmJSSS>cgQ1|H2q00SyZ~ihBFYPdCM6wD0C@>Io*->fU;7|du0KBkeYs&B%isVN zJI2xA$@^Ee&;0oL*ZjVCI|bMs&9Z@p1GV+&>dxwc3IVT+4n0H1#ALg(OoKD{NhxK>46IyXez7F3xv zi|R)n4DISfMB6J@bfrvNqW#+l?e34O*~15)*FuMaxjLeC~u! zVE_jv8G}lvurx1bhH3LYO&?@?J$z=r)C?Nu@9D>y(h(0g?s=bS zO($K$g9Wy7{z&z9=-&g;k_hwu_6kKhGNG!UrrtmEDWt^hV zQVSft6=PW4if?yRd?I4oWxF)>1+^U_(LauD5~et}WpK1*rq}8XE9#i?F8COg!=@@M z2EMq}fp$d!G=**NqJU@68d+;MB2e4`3sPOy3Aq^%-FAq0|DL{iHP>{_wb>Z7*S0{u z9gv_}(m<49(OXYV9T3uS3di22U*OL--CmZ8+2&38Pled+GG)gY2pL>D_I7-M`x^>pt|A2!w{eehrSBVc4`pg2b#g) M>FVdQ&MBb@02@{$@&Et; literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_283.png b/src/assets/images/chat/chatbubbles/bubble_283.png new file mode 100644 index 0000000000000000000000000000000000000000..5d887d9e190d7860956585689c5f20bfeb10e5c9 GIT binary patch literal 1101 zcmV-T1hV^yP)4Tx0C=30kvmAkP!xv0q!vXK2MdZg1W_jiQ4vR}Vi7DztgBy!V2i%`L{+>ig>|8UNKK`xuRL4k=l%8|)AguutoB5pJ zJs_m7s|HACz>t$jE`#(Sbgja#f-^qLqG!m7$EHCJ0ZOKhwK~?<9Sh`fpx>};1LQ=7 z4Z%ghmRZoNbBYbyQuflK(1Bhe%n@Y)1&sn(O!5qq#YVv;Mucz!e~<59pn!%+79E8d zHn32AKjIJgm93i|8?AI|3q$bP^WF!bc?Z5rp7(X+dEdtnc!Ep2=HDp8; zI=0~as-|hXaIp>Dk2%#*eJKFQ=`_5ZLVFTAZ=iLhl3VqjlY594Vg4HS4`46@9jguY zZmG}h&o|ZlegNnsa@Iz;*JJWXmsz7jF)kN|qy|Dc-V#4)xL@E*L{5l_&hfi>fvY zJH30K-luocSwI{VYYX_#AtL&!<-;%YqnaGX`YRkVHlr^OQ>XnMq1bbI`yW&wT%snU zNc6|IYdD5Jkgb{=D*JwiAF{`VQ5`~K)WNXBZ7M4{8^tR1cKc&z)NkKbQ!;Axt| z$L}q8zPy8jL6$qVOli7>D$ws%^^$uz{ z9Gc*0^eLlUDWPPkk1IYtAhv&O+Xc!@dUSC5b7YT}H~7GCA84nd5(8I!eBfS~%yVBU ze177bvV#u+W!U8duZ3ooV&jsgHUh~t24_a*kL?e7P`Hd$mgIa2(k8&Uk7PrNlMj&S zGDGGSK1Z`EXHkTU!~x2DFw)hTAYWC^X{u9?=S}Bo*}x=KSxsQ=@?<=l&0sQ_yt#}W z?_^0aqR2gInM7muvm2q=Y`$9G?ljrOVgbEguQbB) zy&pKA&r2gLp#&h|O_!frN46|T*ds{TBS_dINZ2FD47;FB`?jKCZ35P27GDv&gi6je zk*29?wOWM+P{h8un(@Dr&!S)Ngo+?tuh-2t=C=U*h$mdvLv}c2{Am@6Tt0sRfwjZB Tr9$E<00000NkvXXu0mjfoCgdy literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_283_pointer.png b/src/assets/images/chat/chatbubbles/bubble_283_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..bfc3fdbb9561f17fe13120475cb5791b2320fb7f GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{CNNN?b!zI zoV&ZGHgX<2u#|ztp-W<;rpu$|r-={juVfkU9-Xmjg6f774?}<^RPGmvFKnIX3^aqm M)78&qol`;+0E4V1+W-In literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_284.png b/src/assets/images/chat/chatbubbles/bubble_284.png new file mode 100644 index 0000000000000000000000000000000000000000..e359c05958a4a21ce29c7645ce165ae203702a65 GIT binary patch literal 1309 zcmV+&1>*XNP)LN@=vWo!A2u0X zr9~)3#D@;`r4tcfbT9s?5}k~~)NCs4mZT=JNlCLbxl429ob=w@f4RA76Ky_la?bso zlgs($_dCCH?hV89JW9bZ4A1jCgBpT0Br0@()~F#&B}H*_>sP%sfWZKDq&hSrqj2jL=i~-&L;|`xH3fj-|!kgs<@BQP%7rRA_$_MJ~p+X2`wyK z!^HS0&AuG@umEhvz&x0ILX!&2Cl8)yXs4~88o9(JAQ1*PUsy-xT|+9{M(En_LE!RV z#s+7N4g2Ni6s@HP2-(Ra`2$lBA%P?ZDpp|wytKWruw-loim|a+nAX>?$507An+b^G zSmk5|QBu#4Fg3S5BC{puyb!YK_p>JoV_AcT86BMV23n+;S}b52Bz|B4>#nAv_n@;N zD#;|tWR=(-$nVFD`S7}zBz=l&BC4a;qq@hy+sDNAHmjwiuxN6X7Ae_y2Dw#9Z03N} ze4?n=SUqo%O_yO9ndhWpq+TQNxg0{5|InSZMd68RVG>)3ahTgqiNb`F#G$^^;t&#Y zV)k4|%gah8M?Nl$k1T7BF}tkbBKagx(X%2$Q}lg3s>!C%7-PNp{ z?W9{kS)y3=@iSaZl)~#pHyvN_SCwsU)=!HPk`QjCw9h_TTR^{e0AGFcJ8d^P3*jSp zMEtI4vhFg;RauB$y&4v4hvRm--l_#F zy%-r)uOC8gEr)E{xG24KCy3LdrNv6iW?-dR(f8fg;yNp}4^@+RJVncjJ*0#$w}P( zXg?Ko?M5f|?FeD?wK15Yt(s$rD6$1eH*2kpnzgX1$s2*3I_Mh#t$$mHxS4=z)ga4MH#FKYh<$G<;O T${}7300000NkvXXu0mjfN2`Dn literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_284_pointer.png b/src/assets/images/chat/chatbubbles/bubble_284_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..dae09b2ecab6c127b2ae6131e5948f1f0d4b6df8 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{K(I}dai+6 z#y0Ha0hZ(kK@3a^u01zIls+E(Wbi>g%XCItl27Ofp@=61S}e>A9tTADW=?Q;rQWUa{7G?=$lA0-Nrz`E4j6_?dleQ<%oAAmz>IBQ{5JechC4_0dO*qxVu$NF?{vgQk{5%?J?f|Y{6)?i3)_u?cM4$Ui zz=ZS~>@8U%!mXzMQN&^~ltRN8j1{?}zH|27>e^i2)j6VqPAQ3yBgk-kMtrdy4~ofj z$E#JgUn0z4>kH!C=f4d;zaJOGLkge4&smJdYO~LW_v15YFY}@MR6m0HHsKD% zkRwb)x>MV*iGvV++3$Q~|F*^?z?_6*=$SwqNMb;3eFPX!iO_2=13C_UjSh=D(*Q=j ztD$eKhj-N~jKpGCU0@L8c!58_z#rx(nYIbUP}>~1Zs`Js(|4f{m(0)Ws~^Uf$s{%a z_*Q0|Lc3#iX-GW z>5u|pquSG18c&G_&o?&Oez_RsQF9R8P)+H=;eDcgweUOY7yiVxsVVGfZkDOn_4+iY zyeh*{ObE)Ij>FqT9VgWF8b(?|Xt+Fq$#s=@Y0DOTc=jy(Fp6dj*+NdtL-X?{*wZ_o z$iRdfL}RtU$+mOx1|FFD8J7|Xyxi0TcgoU?_#zdIHOmR0k?|B17_-`wPgTGvmO+nw z@sT*6O{MW$DutwFPAH_tV2H+qaLrN*+2$dRBL&8?N7Qmd$IV)fYa^<&^#&rw0w(XT z!0x6d96fW!HggwRjnHsZ6h#uqmbNV-R0uTyveh;hc?ViPMr30fewm&TjW;ZWOH1fTMu!s0VlIJaKoK*mSRYVNp1pC8Bgt;5-g-w zm|%#Wz`-0hEtb7GZ@vrF-Zp_4f(lGzz9QZ|_2%m^=cf25a0(#YQu!{ta{EIlW@-Po b{}25Ij!iwF+=d2`00000NkvXXu0mjfdo@x+ literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_285_pointer.png b/src/assets/images/chat/chatbubbles/bubble_285_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..11d961572172b361f719ce9aeb1b617651c9dc9e GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{HXu&-<~bz zz_f{-jhx30EM;JEP*FB(^{Clk!}F&-ig(#UiD1!9EMk^(xDGNfu-fn)TC=Oq6KDp5 Mr>mdKI;Vst0JI<>)Bpeg literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_286.png b/src/assets/images/chat/chatbubbles/bubble_286.png new file mode 100644 index 0000000000000000000000000000000000000000..ee5c7063f519f0914c85b66e423b6324054bc907 GIT binary patch literal 408 zcmV;J0cZY+P)2qgxGSDLSkO3@K;K^n%11F?d03{n_Pk^G}KXRtHe(4O^7HR64prl<;sL`S@xB*Q* zObkN|+(1hAL(1_t2xVAY$pASy;S#5(!-rJJYv>wd)VM>&@z^|oH`(Ay67)(c_&hKq zOB-Yl3`xh|KyH{}tLl-P2cXggy|h8*!}QPt+!(gy{FY7gNN$+H^w2Z2G7PnlM=vgM z7M1Afm=J#;JitP1QlMATAO(OOFCom!Kq@$MbTyofP5-~2pK!_%0}dQG0Hz%r9I(pY zpmmxDSqyUoDd50?1K?5xgz@PIcSb05BTWD?!yf=5%(N3=z0g|#00004Tx0C=30kvmAkP!xv0q>2v|2MdZgWKbssU#O#0u?PyKtL4k=q@HzhAgusn8@a6D zJs_yBsRl@AK);hnE`szRbgjg%g3~_BqG!;FN2fpz0!q4ywJO%v91G-8pvSOm1LQ=B z4Z)j&EiZtlA4Pp#$B7nPr{`3L1GbnB*8BgN=eqlrW(h{x07iA&-Vh1|5ZI z*0E51KjIJgm93i^87_5d2|@j{=e_qr!w!5GJn!qs^S+NE@C283#lK#Ji4O=ADq7DW zv~9upRYlWw;bI#)AG4~X`ceRpQz>{mh1Mjr-$3(HDYx=HC-)HFgt=?jKY+e8v@O@z zyRkaAKi`z|`vLD;a@pnf_*nn|0mn&1K~#90?U=tx!%!55f4L1Js0}92p;($B{#ooK z843=f2)=;3PvYp}=;9lQFVIEMsc4}fxL6UaVvE|QSPL-$F_IJ16f)e9HkjbI{4RlW z(y!+ZH&?(f4#tZRM{`Hlb*_R^sf?4e3Uac<{gPU(hH?KPqARhxyU*RF)q14d+S=x> z(e-s49PFW<)u7Jj5Cx@Pb%KOk5mYM2+>P9cgspK!h}9K{GyIbbqCdY(DIFa;ei?D- zMI2bxJHpm{Gu=H$)|?1k=f`5Rl+M}$gwvZJ84_tUuE9&OSVY*G#l$VgFS7|@_R-qKVUl7`U>e4IQHzl=MTIn7-;$rK6TU*xaB zkP;YD0z*n*NC^xnfgvR@BoRgS(&)KoEq5i*p)pRHZ_#^5(_Z#{y~W;^NdP6bf98%H<7oFQ3tF)BfZ_RaMFz!<)=F gu7d>MI?)$&Uy+P`m+$Kb3IG5A07*qoM6N<$f*szcb^rhX literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_287_pointer.png b/src/assets/images/chat/chatbubbles/bubble_287_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..be1db004d770d9e2c951e6963380d6b94dcdc505 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{8+ze(V+(J zJ3EWp8##|1SjxcS&?T`^)8$d~)5Hh%SF#LvkIq;%L3P84hao@{0)LC{3y9vz2sDGi M)78&qol`;+084Ep?*IS* literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_288.png b/src/assets/images/chat/chatbubbles/bubble_288.png new file mode 100644 index 0000000000000000000000000000000000000000..62f7236236065e0ec22800d5947d38c6de9a94cd GIT binary patch literal 1079 zcmV-71jze|P)-Chuq5DP#tYy+zTp$2y@X;ZR zTs;q6p)V4ObmQ<)h9?KSaA|N5Ud7We5DGxwLQbYn0>HC)SjIc!X|??$lg0rN?&jD% z__MVn0rQa{tj+v{c@Ir^S;@UU$c6$El=Lv3hAAENaU=+*dM~#Xs1Tdu@+vO$i`k@C znp-{+6w`Vb_4&l2x14?u4-4`6xnY)=Hwhr~F%QdQ9K&`qTfr~h(QGIn;}{|kha-3_ zM}Sm@28A9F?K?VP%_0puC{6V4l`eR@T9oFArkl&}N)!FH@l_V2d(@Mud0Fn@-3#a( zoiMeJfQbto5L+)nG7DNOfs@&4rp{V`*kM>LuRnZ~;u>3O#=bNKnSRUw@Y&&MwA9 zst2^OZ5f($y|)}@HUJPX?61zpJg1U_1m$+rx@onw+<8smIdm8kyJpp zV0@rdVY@G{S?8w<2B_tl#La*4(+^mF>XCaRnlFZErf8ZbFzkeXsd8T!HUp~A%@RTJ z_A~5xnGJ{~wrfHfW=rc6g)bokkQ%B5M|nI=y>BLE$xYc>b)ClPKGh@awkuz*V?J{4I!<})mmihWC~WM*)H;%`&1V)(0!u%?bVK8s^}%;1kzOLPOkj{ z3;GL=_0u`zago1z68=9(3pt0x_1LV{*D6)M}E?J0yDSE!NweGofC_8;E$KH-`<>+1I^itpptKbvn?l*W%%bVxQ->ttaE z(QH6&O+4MAZ?@*jy>#fE(@wvyWb%(-9;XLmnwxW@8_|r>RnUQzV;-F+RriNFn*#9g zyjql|i=F@uDpIVp#i3OmXw`JLhi1Dvl%8QGN^$ZcIP{4cejfzAc>An$d2ol780!En zRpP!KNW^-qT^`L4cZ1MMu!Imb-1(Doy|Z002ovPDHLkV1kL*{A~aL literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_288_pointer.png b/src/assets/images/chat/chatbubbles/bubble_288_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..565a77f0b3d6125f2ebdd9275b8d41fb9c07466f GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{P_QWt1h$N z0gb8|jhx30EM;JE=#to|>GG)gY2pL>D_I7-M`x^>pt|A2!w{eevaABV0oh+K0nK3W MboFyt=akR{02LS{>;M1& literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_289.png b/src/assets/images/chat/chatbubbles/bubble_289.png new file mode 100644 index 0000000000000000000000000000000000000000..f3bf1ff0e90555d409e91e52c548de65342833e4 GIT binary patch literal 573 zcmV-D0>b@?P)# zU5dgm6or#i7-j_n!@vlFn{WliXX#Sh!GKHY0!mllCImrWTtPwR!DcvvH>8@zMC+8> zIZ%xKq0Ps+_vS~6=Sa7lmttV*yhBPUeL9_-*ODX=bzO@g@fvmHG>NuSN&sFynL2OA zH7TW-&*y@pilVUl+iXP2z#?Z+yA2+cZ8ye+S(XWos;UYg4ZC{)YOQqxR<_-E!P&yS z5I`;#3&9=Y=pJ~Xut+vw`lWE@>O_)yHawq@1~|G8I_Ggc>Z`N)WXis%Kq_Wv(xv-I zaCKRhVz2;1;N_F4DGrzN6&Rc+Cvt^ohp@h^$}!H%TOfu$i}#>1r)etK0VGgKlIXgw z<-K8>KM>+vbK-9Z)AR}*064f2?bZ8d-`~drAXQiZ12`mhg10+<_xptmLb%~Jm+M>* z$FDC@FS7IFGpc!?03~wY*V~8p#^qt(_z%$IK!`C@J+@UCfZB4N>l&RKM>=t6q9Otp zUb)OCz*`>n|2{_=2c9817eoJ|8fWLFE>1(3tCPwQr_k!{auHZkx(U z>j>8FL5SRz`a{EzY*zOasY`4IF2)Q=^bMgrF7k*24@;ko}i$kY|lzT`=S|f zTh?qcmAW_-;>$N79>zkRk5t}R(imI1bN!DD0D7G)k_ORiCi+p7^j{Z_u}RGb1F`jW zGyb#$OpQbPuL8H6H7Wy`11WvNfwmA3+IfJh1V;7w_SQLgT{x!y6i*en7z~Ag3>ckJ z3QGZ-z}V^QdN^zo}$?-h9Z*< zPB6(Xc3O_-Qd9i1)6uWr_xEcVbM^EjP97gcUCEstJxQRDcn^gWj%yIO0vA*&GZ7Dg zk)Up;!w!N2*&c=VzYY$>$@8=D;kD2j`_J!xnRWpe*S5Qe4(Mg?GH0Ci~Y?$!=X0DK$iHI-=d=tx63 zw_nLKq~JM*Ymf`OJg`g%F(K7Nec~;e1vi{tssOf1ed=~gw@rnmHhglU-&2?-VBErA z6<|O=PzW2OEnro&QKr#4g7IPLL2Urbc#axHcPGBUrY|pzwqT9JGBU=kYbbJX63P;M zR@E``KZ7aqCkjiRDqE^DV7@{UsspN25e>%zl=8?h1t>oY7hF_=|f2V zxV{!X^=dX6d5*}eIRcmzt@f9y2qp;NKyTA&rM;mox;^BCCvI3Jun7eL;k^xs_sfDo zLF>zFT#}27Eu2lc*%D+!nrm!Z)JGJqi{TJ!H)k8H#Nl;$N+f0Omu!P6D2O{x1o`wD z3IIWzz4Vx&==94e8-@#Ab6xn>VmOq4&(4ygLWIP%*1NwKmCh|~*+z5g6WYM_;Ph1Z zR-Ysd0-5npEzh}xo-@x%1a`Ln=%3GITYsNhS+OeCRPTPEFE=c|wbuXu002ovPDHLk FV1o0Zjv)X5 literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_290_pointer.png b/src/assets/images/chat/chatbubbles/bubble_290_pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..5077cd2efa0a1a4e36282538980c657f52eed990 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRR!3HD+k8e2&q%1sL978JRB&Q@K{P_R>Yc^X< zLtx01M$TggmNKw7bV+Q~ba~YLH1UD`l`I3^qcc`bP~C9iVF=KKyTyFXvP&!u0nK3W MboFyt=akR{06Xj@wEzGB literal 0 HcmV?d00001 diff --git a/src/assets/images/chat/chatbubbles/bubble_291.png b/src/assets/images/chat/chatbubbles/bubble_291.png new file mode 100644 index 0000000000000000000000000000000000000000..6a7e2a34563590a7925e1db9d6eba08856444a55 GIT binary patch literal 548 zcmV+<0^9wGP)! zF-yZh6vy9{h@@Z&x`-J>NI?@Dz%Y>$*G%1iv&HxHUVM+l0Dahx()u z0SCcA8sqRq%CanQ90wDC6;8C&3k87p!+`Y-1nz+}#uK1YTGDm*^(D8#d1-SYupxkp z%5&bVvPU^X_fyX2DO}H(01;ROH;4{{UO%?(`43c~TG10A7y=@wC>gkF)$$&&@pa5T zSqUfuxDw}O2`B@2q3~H1PzG=*JZ^Q#?M@c^t;zz9iv8Oo;FLMdp2uby@N{z*i;dcn zP9~0E7xthk()a_1j1C_}hlSM@*76unf-C_H{IfdXnQNXMNCfaKRS`VqJa3j4Z0&`UnJaXRiJk~)l1e6^}MDQvDqkP|Itwt#&2j)fCftIuaMma9_t0LRBu_4i# zNB3nFFv{^NRRB89T~A$RwCN^@+FIX$^Q%jcsk?jz_Wd^GsWB!MFFA!0000 = () => const [ isVisible, setIsVisible ] = useState(false); const { enabled, sounds, lastPlayed, play } = useSoundboard(); + const PAGE_SIZE = 9; + const [ page, setPage ] = useState(0); + const totalPages = Math.max(1, Math.ceil(sounds.length / PAGE_SIZE)); + + // Clamp the page if the sound list shrinks (or on first load). + useEffect(() => + { + if(page > (totalPages - 1)) setPage(0); + }, [ totalPages, page ]); + + const pageSounds = sounds.slice(page * PAGE_SIZE, (page * PAGE_SIZE) + PAGE_SIZE); + useEffect(() => { const linkTracker: ILinkEventTracker = { @@ -49,18 +61,32 @@ export const SoundboardView: FC<{}> = () => { !sounds.length && { LocalizeText('soundboard.empty') } } { !!sounds.length && -

- { sounds.map(sound => ( - - )) } -
} + <> +
+ { pageSounds.map(sound => ( + + )) } +
+ { totalPages > 1 && + + + { page + 1 } / { totalPages } + + } + } { lastPlayed && diff --git a/src/css/chat/Chats.css b/src/css/chat/Chats.css index 43f093f..62773c9 100644 --- a/src/css/chat/Chats.css +++ b/src/css/chat/Chats.css @@ -807,6 +807,318 @@ } } + &.bubble-253 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_253.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_253_pointer.png'); + } + } + + &.bubble-254 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_254.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_254_pointer.png'); + } + } + + &.bubble-255 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_255.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_255_pointer.png'); + } + } + + &.bubble-256 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_256.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_256_pointer.png'); + } + } + + &.bubble-257 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_257.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_257_pointer.png'); + } + } + + &.bubble-258 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_258.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_258_pointer.png'); + } + } + + &.bubble-259 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_259.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_259_pointer.png'); + } + } + + &.bubble-260 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_260.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_260_pointer.png'); + } + } + + &.bubble-261 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_261.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_261_pointer.png'); + } + } + + &.bubble-262 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_262.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_262_pointer.png'); + } + } + + &.bubble-263 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_263.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_263_pointer.png'); + } + } + + &.bubble-264 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_264.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_264_pointer.png'); + } + } + + &.bubble-265 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_265.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_265_pointer.png'); + } + } + + &.bubble-266 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_266.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_266_pointer.png'); + } + } + + &.bubble-267 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_267.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_267_pointer.png'); + } + } + + &.bubble-268 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_268.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_268_pointer.png'); + } + } + + &.bubble-269 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_269.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_269_pointer.png'); + } + } + + &.bubble-270 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_270.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_270_pointer.png'); + } + } + + &.bubble-271 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_271.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_271_pointer.png'); + } + } + + &.bubble-272 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_272.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_272_pointer.png'); + } + } + + &.bubble-273 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_273.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_273_pointer.png'); + } + } + + &.bubble-274 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_274.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_274_pointer.png'); + } + } + + &.bubble-275 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_275.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_275_pointer.png'); + } + } + + &.bubble-276 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_276.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_276_pointer.png'); + } + } + + &.bubble-277 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_277.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_277_pointer.png'); + } + } + + &.bubble-278 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_278.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_278_pointer.png'); + } + } + + &.bubble-279 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_279.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_279_pointer.png'); + } + } + + &.bubble-280 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_280.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_280_pointer.png'); + } + } + + &.bubble-281 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_281.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_281_pointer.png'); + } + } + + &.bubble-282 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_282.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_282_pointer.png'); + } + } + + &.bubble-283 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_283.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_283_pointer.png'); + } + } + + &.bubble-284 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_284.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_284_pointer.png'); + } + } + + &.bubble-285 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_285.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_285_pointer.png'); + } + } + + &.bubble-286 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_286.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_286_pointer.png'); + } + } + + &.bubble-287 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_287.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_287_pointer.png'); + } + } + + &.bubble-288 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_288.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_288_pointer.png'); + } + } + + &.bubble-289 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_289.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_289_pointer.png'); + } + } + + &.bubble-290 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_290.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_290_pointer.png'); + } + } + + &.bubble-291 { + border-image-source: url('@/assets/images/chat/chatbubbles/bubble_291.png'); + + .pointer { + background: url('@/assets/images/chat/chatbubbles/bubble_291_pointer.png'); + } + } + &.bubble-200, &.bubble-201, &.bubble-202, @@ -1810,4 +2122,160 @@ background: center / contain no-repeat url('@/assets/images/chat/chatbubbles/bubble_252_extra.png'); } } + &.bubble-253 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_253.png'); + } + + &.bubble-254 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_254.png'); + } + + &.bubble-255 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_255.png'); + } + + &.bubble-256 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_256.png'); + } + + &.bubble-257 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_257.png'); + } + + &.bubble-258 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_258.png'); + } + + &.bubble-259 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_259.png'); + } + + &.bubble-260 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_260.png'); + } + + &.bubble-261 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_261.png'); + } + + &.bubble-262 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_262.png'); + } + + &.bubble-263 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_263.png'); + } + + &.bubble-264 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_264.png'); + } + + &.bubble-265 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_265.png'); + } + + &.bubble-266 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_266.png'); + } + + &.bubble-267 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_267.png'); + } + + &.bubble-268 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_268.png'); + } + + &.bubble-269 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_269.png'); + } + + &.bubble-270 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_270.png'); + } + + &.bubble-271 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_271.png'); + } + + &.bubble-272 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_272.png'); + } + + &.bubble-273 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_273.png'); + } + + &.bubble-274 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_274.png'); + } + + &.bubble-275 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_275.png'); + } + + &.bubble-276 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_276.png'); + } + + &.bubble-277 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_277.png'); + } + + &.bubble-278 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_278.png'); + } + + &.bubble-279 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_279.png'); + } + + &.bubble-280 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_280.png'); + } + + &.bubble-281 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_281.png'); + } + + &.bubble-282 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_282.png'); + } + + &.bubble-283 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_283.png'); + } + + &.bubble-284 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_284.png'); + } + + &.bubble-285 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_285.png'); + } + + &.bubble-286 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_286.png'); + } + + &.bubble-287 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_287.png'); + } + + &.bubble-288 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_288.png'); + } + + &.bubble-289 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_289.png'); + } + + &.bubble-290 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_290.png'); + } + + &.bubble-291 { + background-image: url('@/assets/images/chat/chatbubbles/bubble_291.png'); + } + } From bd24002b9388d97d346cd67bc815510b87fcf8ad Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sun, 31 May 2026 05:23:40 +0200 Subject: [PATCH 2/4] fix(chat): stretch (non repeat) sulle bubble custom 253-291 Ereditavano border-image-repeat: repeat dalla regola base, quindi il centro delle nuove bubble (con grafica) si duplicava invece di allungarsi. Override border-image-repeat: stretch per le 253-291 cosi il corpo si stira come le originali (che avevano il centro a tinta unita). --- src/css/chat/Chats.css | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/css/chat/Chats.css b/src/css/chat/Chats.css index 62773c9..4e2f071 100644 --- a/src/css/chat/Chats.css +++ b/src/css/chat/Chats.css @@ -1119,6 +1119,48 @@ } } + &.bubble-253, + &.bubble-254, + &.bubble-255, + &.bubble-256, + &.bubble-257, + &.bubble-258, + &.bubble-259, + &.bubble-260, + &.bubble-261, + &.bubble-262, + &.bubble-263, + &.bubble-264, + &.bubble-265, + &.bubble-266, + &.bubble-267, + &.bubble-268, + &.bubble-269, + &.bubble-270, + &.bubble-271, + &.bubble-272, + &.bubble-273, + &.bubble-274, + &.bubble-275, + &.bubble-276, + &.bubble-277, + &.bubble-278, + &.bubble-279, + &.bubble-280, + &.bubble-281, + &.bubble-282, + &.bubble-283, + &.bubble-284, + &.bubble-285, + &.bubble-286, + &.bubble-287, + &.bubble-288, + &.bubble-289, + &.bubble-290, + &.bubble-291 { + border-image-repeat: stretch stretch; + } + &.bubble-200, &.bubble-201, &.bubble-202, From cbd63220bdd6697ef921d8cbc469d3e440b74593 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sun, 31 May 2026 06:07:30 +0200 Subject: [PATCH 3/4] fix(chat): slice border-image per-bubble (253-291) per stretch pulito Le bubble custom ereditavano la slice base (centro largo) -> con repeat si duplicavano, con stretch si allungavano troppo/storte. Ora ogni bubble ha border-image-slice/width calcolata dalla sua immagine: il taglio cade nella zona centrale del corpo, sulla colonna piu uniforme (colore pieno), con un filo stirabile di ~2px e border-image-repeat: stretch. Cosi il corpo si stira pulito e i cap restano intatti. (Le bubble con decoro su tutto il corpo, senza una fascia piena, restano un compromesso: limite intrinseco del 9-slice.) --- src/css/chat/Chats.css | 159 ++++++++++++++++++++++++++++++----------- 1 file changed, 117 insertions(+), 42 deletions(-) diff --git a/src/css/chat/Chats.css b/src/css/chat/Chats.css index 4e2f071..13187d2 100644 --- a/src/css/chat/Chats.css +++ b/src/css/chat/Chats.css @@ -809,6 +809,9 @@ &.bubble-253 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_253.png'); + border-image-slice: 16 22 15 27 fill; + border-image-width: 16px 22px 15px 27px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_253_pointer.png'); @@ -817,6 +820,9 @@ &.bubble-254 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_254.png'); + border-image-slice: 7 28 15 25 fill; + border-image-width: 7px 28px 15px 25px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_254_pointer.png'); @@ -825,6 +831,9 @@ &.bubble-255 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_255.png'); + border-image-slice: 12 19 22 30 fill; + border-image-width: 12px 19px 22px 30px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_255_pointer.png'); @@ -833,6 +842,9 @@ &.bubble-256 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_256.png'); + border-image-slice: 24 18 10 31 fill; + border-image-width: 24px 18px 10px 31px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_256_pointer.png'); @@ -841,6 +853,9 @@ &.bubble-257 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_257.png'); + border-image-slice: 6 17 19 36 fill; + border-image-width: 6px 17px 19px 36px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_257_pointer.png'); @@ -849,6 +864,9 @@ &.bubble-258 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_258.png'); + border-image-slice: 22 27 10 27 fill; + border-image-width: 22px 27px 10px 27px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_258_pointer.png'); @@ -857,6 +875,9 @@ &.bubble-259 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_259.png'); + border-image-slice: 21 27 18 37 fill; + border-image-width: 21px 27px 18px 37px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_259_pointer.png'); @@ -865,6 +886,9 @@ &.bubble-260 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_260.png'); + border-image-slice: 6 22 16 27 fill; + border-image-width: 6px 22px 16px 27px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_260_pointer.png'); @@ -873,6 +897,9 @@ &.bubble-261 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_261.png'); + border-image-slice: 18 27 5 22 fill; + border-image-width: 18px 27px 5px 22px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_261_pointer.png'); @@ -881,6 +908,9 @@ &.bubble-262 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_262.png'); + border-image-slice: 33 31 11 34 fill; + border-image-width: 33px 31px 11px 34px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_262_pointer.png'); @@ -889,6 +919,9 @@ &.bubble-263 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_263.png'); + border-image-slice: 15 19 10 32 fill; + border-image-width: 15px 19px 10px 32px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_263_pointer.png'); @@ -897,6 +930,9 @@ &.bubble-264 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_264.png'); + border-image-slice: 18 24 16 25 fill; + border-image-width: 18px 24px 16px 25px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_264_pointer.png'); @@ -905,6 +941,9 @@ &.bubble-265 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_265.png'); + border-image-slice: 41 40 17 18 fill; + border-image-width: 41px 40px 17px 18px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_265_pointer.png'); @@ -913,6 +952,9 @@ &.bubble-266 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_266.png'); + border-image-slice: 13 34 22 27 fill; + border-image-width: 13px 34px 22px 27px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_266_pointer.png'); @@ -921,6 +963,9 @@ &.bubble-267 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_267.png'); + border-image-slice: 17 30 22 25 fill; + border-image-width: 17px 30px 22px 25px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_267_pointer.png'); @@ -929,6 +974,9 @@ &.bubble-268 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_268.png'); + border-image-slice: 7 30 21 24 fill; + border-image-width: 7px 30px 21px 24px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_268_pointer.png'); @@ -937,6 +985,9 @@ &.bubble-269 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_269.png'); + border-image-slice: 10 23 25 35 fill; + border-image-width: 10px 23px 25px 35px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_269_pointer.png'); @@ -945,6 +996,9 @@ &.bubble-270 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_270.png'); + border-image-slice: 13 30 14 26 fill; + border-image-width: 13px 30px 14px 26px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_270_pointer.png'); @@ -953,6 +1007,9 @@ &.bubble-271 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_271.png'); + border-image-slice: 23 23 9 35 fill; + border-image-width: 23px 23px 9px 35px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_271_pointer.png'); @@ -961,6 +1018,9 @@ &.bubble-272 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_272.png'); + border-image-slice: 9 31 24 25 fill; + border-image-width: 9px 31px 24px 25px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_272_pointer.png'); @@ -969,6 +1029,9 @@ &.bubble-273 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_273.png'); + border-image-slice: 11 16 25 37 fill; + border-image-width: 11px 16px 25px 37px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_273_pointer.png'); @@ -977,6 +1040,9 @@ &.bubble-274 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_274.png'); + border-image-slice: 7 22 19 27 fill; + border-image-width: 7px 22px 19px 27px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_274_pointer.png'); @@ -985,6 +1051,9 @@ &.bubble-275 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_275.png'); + border-image-slice: 8 23 14 26 fill; + border-image-width: 8px 23px 14px 26px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_275_pointer.png'); @@ -993,6 +1062,9 @@ &.bubble-276 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_276.png'); + border-image-slice: 12 40 17 17 fill; + border-image-width: 12px 40px 17px 17px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_276_pointer.png'); @@ -1001,6 +1073,9 @@ &.bubble-277 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_277.png'); + border-image-slice: 6 39 18 17 fill; + border-image-width: 6px 39px 18px 17px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_277_pointer.png'); @@ -1009,6 +1084,9 @@ &.bubble-278 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_278.png'); + border-image-slice: 16 38 6 19 fill; + border-image-width: 16px 38px 6px 19px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_278_pointer.png'); @@ -1017,6 +1095,9 @@ &.bubble-279 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_279.png'); + border-image-slice: 6 26 16 23 fill; + border-image-width: 6px 26px 16px 23px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_279_pointer.png'); @@ -1025,6 +1106,9 @@ &.bubble-280 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_280.png'); + border-image-slice: 23 29 6 15 fill; + border-image-width: 23px 29px 6px 15px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_280_pointer.png'); @@ -1033,6 +1117,9 @@ &.bubble-281 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_281.png'); + border-image-slice: 18 42 9 18 fill; + border-image-width: 18px 42px 9px 18px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_281_pointer.png'); @@ -1041,6 +1128,9 @@ &.bubble-282 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_282.png'); + border-image-slice: 18 42 9 18 fill; + border-image-width: 18px 42px 9px 18px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_282_pointer.png'); @@ -1049,6 +1139,9 @@ &.bubble-283 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_283.png'); + border-image-slice: 17 26 13 31 fill; + border-image-width: 17px 26px 13px 31px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_283_pointer.png'); @@ -1057,6 +1150,9 @@ &.bubble-284 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_284.png'); + border-image-slice: 9 26 23 26 fill; + border-image-width: 9px 26px 23px 26px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_284_pointer.png'); @@ -1065,6 +1161,9 @@ &.bubble-285 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_285.png'); + border-image-slice: 16 35 15 15 fill; + border-image-width: 16px 35px 15px 15px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_285_pointer.png'); @@ -1073,6 +1172,9 @@ &.bubble-286 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_286.png'); + border-image-slice: 18 22 4 23 fill; + border-image-width: 18px 22px 4px 23px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_286_pointer.png'); @@ -1081,6 +1183,9 @@ &.bubble-287 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_287.png'); + border-image-slice: 6 22 18 26 fill; + border-image-width: 6px 22px 18px 26px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_287_pointer.png'); @@ -1089,6 +1194,9 @@ &.bubble-288 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_288.png'); + border-image-slice: 18 31 11 24 fill; + border-image-width: 18px 31px 11px 24px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_288_pointer.png'); @@ -1097,6 +1205,9 @@ &.bubble-289 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_289.png'); + border-image-slice: 7 54 17 24 fill; + border-image-width: 7px 54px 17px 24px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_289_pointer.png'); @@ -1105,6 +1216,9 @@ &.bubble-290 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_290.png'); + border-image-slice: 18 24 14 29 fill; + border-image-width: 18px 24px 14px 29px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_290_pointer.png'); @@ -1113,54 +1227,15 @@ &.bubble-291 { border-image-source: url('@/assets/images/chat/chatbubbles/bubble_291.png'); + border-image-slice: 9 26 11 35 fill; + border-image-width: 9px 26px 11px 35px; + border-image-repeat: stretch stretch; .pointer { background: url('@/assets/images/chat/chatbubbles/bubble_291_pointer.png'); } } - &.bubble-253, - &.bubble-254, - &.bubble-255, - &.bubble-256, - &.bubble-257, - &.bubble-258, - &.bubble-259, - &.bubble-260, - &.bubble-261, - &.bubble-262, - &.bubble-263, - &.bubble-264, - &.bubble-265, - &.bubble-266, - &.bubble-267, - &.bubble-268, - &.bubble-269, - &.bubble-270, - &.bubble-271, - &.bubble-272, - &.bubble-273, - &.bubble-274, - &.bubble-275, - &.bubble-276, - &.bubble-277, - &.bubble-278, - &.bubble-279, - &.bubble-280, - &.bubble-281, - &.bubble-282, - &.bubble-283, - &.bubble-284, - &.bubble-285, - &.bubble-286, - &.bubble-287, - &.bubble-288, - &.bubble-289, - &.bubble-290, - &.bubble-291 { - border-image-repeat: stretch stretch; - } - &.bubble-200, &.bubble-201, &.bubble-202, From 809734456153b0a55414655f8ae7fc47774a58e0 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Sun, 31 May 2026 14:39:59 +0200 Subject: [PATCH 4/4] feat(theme): runtime custom theme ecosystem (graphics-only) Runtime-loaded visual re-skin system (no client rebuild, real themes never hit git). A theme = a folder on the server (theme.base.url) with a manifest + CSS "pieces"; each piece is toggled from Settings > Themes (checkboxes). A broken/404 piece auto-falls back to the default (per piece). Hotel-wide default via ui-config theme.default (+ theme.default.pieces), per-user override in localStorage (same pattern as the catalog style toggle). - api/theme/ThemeManager: fetch index/manifest + inject/remove + fallback - hooks/theme/useThemes: state + persist + default-from-config + live apply - components/theme/ThemeApplier: applies on boot (mounted in MainView) - UserSettings: General/Themes tabs with theme selector + per-piece checkboxes - custom-themes/: reference template (demo theme "Neon Viola" + README) - .gitignore: public/custom-themes/ (real themes are never committed) --- .gitignore | 3 + custom-themes/README.md | 40 ++++++ custom-themes/index.example.json | 5 + custom-themes/neon-viola/cards.css | 24 ++++ custom-themes/neon-viola/catalog.css | 10 ++ custom-themes/neon-viola/chat.css | 12 ++ custom-themes/neon-viola/theme.json | 10 ++ custom-themes/neon-viola/toolbar.css | 9 ++ src/api/index.ts | 1 + src/api/theme/ThemeManager.ts | 122 ++++++++++++++++++ src/api/theme/index.ts | 1 + src/api/utils/LocalStorageKeys.ts | 2 + src/components/MainView.tsx | 2 + src/components/theme/ThemeApplier.tsx | 12 ++ .../user-settings/UserSettingsView.tsx | 38 +++++- src/hooks/index.ts | 1 + src/hooks/theme/index.ts | 1 + src/hooks/theme/useThemes.ts | 108 ++++++++++++++++ 18 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 custom-themes/README.md create mode 100644 custom-themes/index.example.json create mode 100644 custom-themes/neon-viola/cards.css create mode 100644 custom-themes/neon-viola/catalog.css create mode 100644 custom-themes/neon-viola/chat.css create mode 100644 custom-themes/neon-viola/theme.json create mode 100644 custom-themes/neon-viola/toolbar.css create mode 100644 src/api/theme/ThemeManager.ts create mode 100644 src/api/theme/index.ts create mode 100644 src/components/theme/ThemeApplier.tsx create mode 100644 src/hooks/theme/index.ts create mode 100644 src/hooks/theme/useThemes.ts diff --git a/.gitignore b/.gitignore index e7bee96..d80f3e8 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ Thumbs.db # the dev server takes minutes to start with 100k+ files under public/. /public/nitro-assets /public/swf + +# Temi custom locali di test (i temi veri stanno sul server, mai su git) +public/custom-themes/ diff --git a/custom-themes/README.md b/custom-themes/README.md new file mode 100644 index 0000000..e9f7436 --- /dev/null +++ b/custom-themes/README.md @@ -0,0 +1,40 @@ +# Custom themes (graphics-only) + +Ecosistema temi caricati a **runtime** (niente rebuild del client). Un tema = +una cartella con un manifest + "pezzi" CSS. Ogni pezzo รจ attivabile/disattivabile +dall'utente da **Impostazioni โ†’ Temi** (checkbox). Se un pezzo รจ rotto/404 โ†’ +fallback automatico al default (solo quel pezzo). + +## Dove vivono +- **Questa cartella (`custom-themes/`) รจ solo il TEMPLATE di riferimento**, versionata su git. +- I temi **veri** stanno sul server in `public/nitro/custom-themes/` (serviti via + l'url configurato in ui-config `theme.base.url`, es. `/client/nitro/custom-themes`). + NON vanno su git โ†’ vedi `.gitignore` (`public/custom-themes/`). + +## Struttura +``` +custom-themes/ + index.json # { "themes": [ { "id", "name", "author?" } ] } + / + theme.json # { "name", "pieces": [ { "id", "name", "file" } ] } + cards.css chat.css ... # un file per "pezzo" + assets/... # immagini referenziate dai CSS (url assoluti) +``` + +## Creare un tema +1. Copia `neon-viola/` in una nuova cartella `/`. +2. Modifica `theme.json` (nome + elenco pezzi). +3. Scrivi i CSS dei pezzi (override con `!important`, caricati dopo il base). +4. Aggiungi `{ "id": "", "name": "..." }` a `index.json`. +5. Carica la cartella in `public/nitro/custom-themes/` sul server. **Nessun rebuild.** + +## Default hotel-wide (admin) +In `ui-config.json`: +- `theme.base.url` โ†’ dove sono serviti i temi +- `theme.default` โ†’ id del tema attivo di default (vuoto = nessuno) +- `theme.default.pieces` โ†’ array di id pezzi attivi di default + +Ogni utente puรฒ comunque sovrascrivere da Impostazioni โ†’ Temi (salvato in localStorage). + +> Nota: i temi ri-skinnano solo la **grafica** (CSS). Non cambiano la struttura +> dei componenti nรฉ il comportamento. diff --git a/custom-themes/index.example.json b/custom-themes/index.example.json new file mode 100644 index 0000000..46fd0ea --- /dev/null +++ b/custom-themes/index.example.json @@ -0,0 +1,5 @@ +{ + "themes": [ + { "id": "neon-viola", "name": "Neon Viola", "author": "infinityhotel" } + ] +} diff --git a/custom-themes/neon-viola/cards.css b/custom-themes/neon-viola/cards.css new file mode 100644 index 0000000..5608fb7 --- /dev/null +++ b/custom-themes/neon-viola/cards.css @@ -0,0 +1,24 @@ +/* Tema Neon Viola โ€” pezzo "cards" (finestre / NitroCard). + Ricolora header + cornice delle finestre. Caricato DOPO il CSS base, quindi + usa !important per vincere. Tocca solo la cornice/header (non lo sfondo del + contenuto) per non rovinare la leggibilita' del testo. */ + +.nitro-card-shell:not(.nitro-wired) { + border-color: #7c3aed !important; + box-shadow: 0 0 14px rgba(124, 58, 237, .55), 0 8px 22px rgba(0, 0, 0, .4) !important; +} + +.nitro-card-shell:not(.nitro-wired) .nitro-card-header-shell { + background: linear-gradient(180deg, #9333ea 0%, #6d28d9 100%) !important; + border-color: #a855f7 !important; + border-bottom-color: #2a0a4a !important; +} + +.nitro-card-shell:not(.nitro-wired) .nitro-card-title { + color: #fff !important; + text-shadow: 0 0 6px #c084fc, 0 1px 0 #3b0764 !important; +} + +.nitro-card-shell:not(.nitro-wired) .nitro-card-tabs-shell .nitro-card-tab-item-active { + box-shadow: inset 0 -2px 0 #a855f7 !important; +} diff --git a/custom-themes/neon-viola/catalog.css b/custom-themes/neon-viola/catalog.css new file mode 100644 index 0000000..1f8973d --- /dev/null +++ b/custom-themes/neon-viola/catalog.css @@ -0,0 +1,10 @@ +/* Tema Neon Viola โ€” pezzo "catalog" (catalogo Hippiehotel, .nitro-catalog). */ + +.nitro-catalog .nitro-card-header-shell { + background: linear-gradient(180deg, #9333ea 0%, #6d28d9 100%) !important; +} + +.nitro-catalog .group\/rail { + background: #1a1030 !important; + border-right-color: #7c3aed !important; +} diff --git a/custom-themes/neon-viola/chat.css b/custom-themes/neon-viola/chat.css new file mode 100644 index 0000000..c0f767e --- /dev/null +++ b/custom-themes/neon-viola/chat.css @@ -0,0 +1,12 @@ +/* Tema Neon Viola โ€” pezzo "chat". + Accento viola sulla bubble di default (bubble-0) e sull'input chat. + (Le bubble custom hanno la loro grafica; qui tocchiamo solo l'accento base.) */ + +.chat-bubble.bubble-0 { + filter: drop-shadow(0 0 5px rgba(168, 85, 247, .8)); +} + +.nitro-chat-input-container, +.chat-input-container { + box-shadow: inset 0 0 0 1px #7c3aed !important; +} diff --git a/custom-themes/neon-viola/theme.json b/custom-themes/neon-viola/theme.json new file mode 100644 index 0000000..199fd09 --- /dev/null +++ b/custom-themes/neon-viola/theme.json @@ -0,0 +1,10 @@ +{ + "name": "Neon Viola", + "author": "infinityhotel", + "pieces": [ + { "id": "cards", "name": "Finestre / Card", "file": "cards.css" }, + { "id": "chat", "name": "Chat", "file": "chat.css" }, + { "id": "toolbar", "name": "Toolbar", "file": "toolbar.css" }, + { "id": "catalog", "name": "Catalogo", "file": "catalog.css" } + ] +} diff --git a/custom-themes/neon-viola/toolbar.css b/custom-themes/neon-viola/toolbar.css new file mode 100644 index 0000000..ed74f7f --- /dev/null +++ b/custom-themes/neon-viola/toolbar.css @@ -0,0 +1,9 @@ +/* Tema Neon Viola โ€” pezzo "toolbar". + Best-effort: ricolora la barra strumenti in basso. Se i selettori non + matchano nella tua build, il pezzo non ha effetto (fallback sicuro). */ + +.nitro-toolbar, +[class*="toolbar-container"] { + background: linear-gradient(180deg, #2a0a4a 0%, #1a0730 100%) !important; + box-shadow: 0 -2px 10px rgba(124, 58, 237, .4) !important; +} diff --git a/src/api/index.ts b/src/api/index.ts index 424bb4f..ae86f5d 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -28,6 +28,7 @@ export * from './room'; export * from './room/events'; export * from './room/widgets'; export * from './soundboard'; +export * from './theme'; export * from './ui-settings'; export * from './user'; export * from './utils'; diff --git a/src/api/theme/ThemeManager.ts b/src/api/theme/ThemeManager.ts new file mode 100644 index 0000000..cbd9e80 --- /dev/null +++ b/src/api/theme/ThemeManager.ts @@ -0,0 +1,122 @@ +import { NitroLogger } from '@nitrots/nitro-renderer'; +import { GetConfigurationValue } from '../nitro'; + +// --------------------------------------------------------------------------- +// Custom theme ecosystem (graphics-only, runtime-loaded). +// +// A "theme" is a folder on the server (NOT bundled in the build) made of: +// /index.json -> { "themes": [ { id, name, author? } ] } +// //theme.json -> { name, pieces: [ { id, name, file } ] } +// //.css -> one CSS "piece" (cards, chat, catalog, ...) +// +// Each enabled piece is injected as a in . If a piece fails to +// load (404 / network) the link removes itself, so the UI falls back to the +// default look for that piece (per-piece fallback, never breaks the client). +// +// The base url is configurable via ui-config ("theme.base.url") so themes can +// live anywhere (and never need a client rebuild to add/change them). +// --------------------------------------------------------------------------- + +export interface ThemeInfo +{ + id: string; + name: string; + author?: string; +} + +export interface ThemePiece +{ + id: string; + name: string; + file: string; +} + +export interface ThemeManifest +{ + name: string; + pieces: ThemePiece[]; +} + +const LINK_ATTR = 'data-nitro-theme'; + +export const GetThemeBaseUrl = (): string => + GetConfigurationValue('theme.base.url', 'custom-themes').replace(/\/+$/, ''); + +export const FetchThemeIndex = async (): Promise => +{ + try + { + const response = await fetch(`${ GetThemeBaseUrl() }/index.json`, { cache: 'no-cache' }); + + if(!response.ok) return []; + + const data = await response.json(); + + return Array.isArray(data?.themes) ? data.themes.filter((t: any) => t && t.id) : []; + } + catch(error) + { + NitroLogger.warn('[ThemeManager] index.json non caricabile, nessun tema custom', error); + + return []; + } +}; + +export const FetchThemeManifest = async (themeId: string): Promise => +{ + if(!themeId) return null; + + try + { + const response = await fetch(`${ GetThemeBaseUrl() }/${ themeId }/theme.json`, { cache: 'no-cache' }); + + if(!response.ok) return null; + + const data = await response.json(); + + if(!data || !Array.isArray(data.pieces)) return null; + + return { + name: data.name ?? themeId, + pieces: data.pieces.filter((p: any) => p && p.id && p.file) + }; + } + catch(error) + { + NitroLogger.warn(`[ThemeManager] manifest non valido per tema "${ themeId }" -> fallback default`, error); + + return null; + } +}; + +export const ClearTheme = (): void => +{ + document.head.querySelectorAll(`link[${ LINK_ATTR }]`).forEach(node => node.remove()); +}; + +export const ApplyThemePieces = (themeId: string, pieces: ThemePiece[]): void => +{ + ClearTheme(); + + if(!themeId || !pieces || !pieces.length) return; + + const base = GetThemeBaseUrl(); + + for(const piece of pieces) + { + const link = document.createElement('link'); + + link.rel = 'stylesheet'; + link.setAttribute(LINK_ATTR, piece.id); + link.href = `${ base }/${ themeId }/${ piece.file }`; + + // Per-piece fallback: a broken piece removes itself, leaving the default. + link.onerror = () => + { + NitroLogger.warn(`[ThemeManager] pezzo tema rotto "${ themeId }/${ piece.file }" -> fallback default`); + link.remove(); + }; + + document.head.appendChild(link); + } +}; diff --git a/src/api/theme/index.ts b/src/api/theme/index.ts new file mode 100644 index 0000000..ee14d93 --- /dev/null +++ b/src/api/theme/index.ts @@ -0,0 +1 @@ +export * from './ThemeManager'; diff --git a/src/api/utils/LocalStorageKeys.ts b/src/api/utils/LocalStorageKeys.ts index 75ecfe8..c1ee611 100644 --- a/src/api/utils/LocalStorageKeys.ts +++ b/src/api/utils/LocalStorageKeys.ts @@ -5,4 +5,6 @@ export class LocalStorageKeys public static CHAT_WINDOW_ENABLED: string = 'chatWindowEnabled'; public static CHAT_TRANSLATION_SETTINGS: string = 'chatTranslationSettings'; public static CATALOG_CLASSIC_STYLE: string = 'catalogClassicStyle'; + public static THEME_ACTIVE: string = 'nitroThemeActive'; + public static THEME_PIECES: string = 'nitroThemePieces'; } diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index e155f05..d94bbc5 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -28,6 +28,7 @@ import { HousekeepingView } from './housekeeping/HousekeepingView'; import { RareValuesView } from './rare-values/RareValuesView'; import { FortuneWheelView } from './fortune-wheel/FortuneWheelView'; import { SoundboardView } from './soundboard/SoundboardView'; +import { ThemeApplier } from './theme/ThemeApplier'; import { RadioView } from './radio/RadioView'; import { InventoryView } from './inventory/InventoryView'; import { ModToolsView } from './mod-tools/ModToolsView'; @@ -134,6 +135,7 @@ export const MainView: FC<{}> = props => return ( <> +
{ landingViewVisible && diff --git a/src/components/theme/ThemeApplier.tsx b/src/components/theme/ThemeApplier.tsx new file mode 100644 index 0000000..1f02ed1 --- /dev/null +++ b/src/components/theme/ThemeApplier.tsx @@ -0,0 +1,12 @@ +import { FC } from 'react'; +import { useThemes } from '../../hooks'; + +// Mounted once at app level: subscribing to the shared theme store triggers the +// load + apply effects, so the saved/default custom theme is applied on boot +// and kept in sync when the user changes it from Settings. Renders nothing. +export const ThemeApplier: FC<{}> = () => +{ + useThemes(); + + return null; +}; diff --git a/src/components/user-settings/UserSettingsView.tsx b/src/components/user-settings/UserSettingsView.tsx index 999ac3b..77adc46 100644 --- a/src/components/user-settings/UserSettingsView.tsx +++ b/src/components/user-settings/UserSettingsView.tsx @@ -3,13 +3,15 @@ import { FC, useEffect, useState } from 'react'; import { FaUserCog, FaVolumeDown, FaVolumeMute, FaVolumeUp } from 'react-icons/fa'; import { DispatchMainEvent, DispatchUiEvent, LocalizeText, SendMessageComposer } from '../../api'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; -import { useCatalogClassicStyle, useCatalogPlaceMultipleItems, useCatalogSkipPurchaseConfirmation, useChatWindow, useMessageEvent } from '../../hooks'; +import { useCatalogClassicStyle, useCatalogPlaceMultipleItems, useCatalogSkipPurchaseConfirmation, useChatWindow, useMessageEvent, useThemes } from '../../hooks'; import { classNames } from '../../layout'; export const UserSettingsView: FC<{}> = props => { const [ isVisible, setIsVisible ] = useState(false); + const [ activeTab, setActiveTab ] = useState<'general' | 'themes'>('general'); const [ userSettings, setUserSettings ] = useState(null); + const { themes, activeThemeId, manifest, activeEnabled, selectTheme, togglePiece } = useThemes(); const [ catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects ] = useCatalogPlaceMultipleItems(); const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useCatalogSkipPurchaseConfirmation(); const [ chatWindowEnabled, setChatWindowEnabled ] = useChatWindow(); @@ -132,6 +134,11 @@ export const UserSettingsView: FC<{}> = props => processAction('close_view') } /> +
+ + +
+ { activeTab === 'general' && <>
processAction('oldchat', event.target.checked) } /> @@ -207,6 +214,35 @@ export const UserSettingsView: FC<{}> = props => โ€บ
+ } + { activeTab === 'themes' &&
+
+ Tema custom + +
+ { activeThemeId && manifest && manifest.pieces.length > 0 && +
+ Pezzi attivi + { manifest.pieces.map(piece => ( +
+ togglePiece(piece.id) } /> + { piece.name } +
+ )) } +
} + { activeThemeId && !manifest && + Tema non valido o non raggiungibile โ€” uso il default. } + { !themes.length && + Nessun tema disponibile. Aggiungi una cartella in custom-themes/ sul server. } +
} ); diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 8a85f2a..46a0287 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -24,6 +24,7 @@ export * from './rooms/widgets'; export * from './rooms/widgets/furniture'; export * from './session'; export * from './soundboard/useSoundboard'; +export * from './theme'; export * from './translation'; export * from './useLocalStorage'; export * from './useSharedVisibility'; diff --git a/src/hooks/theme/index.ts b/src/hooks/theme/index.ts new file mode 100644 index 0000000..effac86 --- /dev/null +++ b/src/hooks/theme/index.ts @@ -0,0 +1 @@ +export * from './useThemes'; diff --git a/src/hooks/theme/useThemes.ts b/src/hooks/theme/useThemes.ts new file mode 100644 index 0000000..c2ef444 --- /dev/null +++ b/src/hooks/theme/useThemes.ts @@ -0,0 +1,108 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useBetween } from 'use-between'; +import { ApplyThemePieces, ClearTheme, FetchThemeIndex, FetchThemeManifest, GetConfigurationValue, LocalStorageKeys, ThemeInfo, ThemeManifest } from '../../api'; +import { useLocalStorage } from '../useLocalStorage'; + +// Per-user custom theme selection. +// - activeThemeId: '' = default (no custom theme). Default for new users comes +// from ui-config `theme.default` so the admin can set a hotel-wide default +// (like catalog.classic.style), while each user can override from Settings. +// - enabledPieces[themeId]: which graphic pieces of that theme are active +// (checkboxes). If absent, defaults to ui-config `theme.default.pieces` +// (when on the default theme) or ALL pieces. +const useThemesState = () => +{ + const [ activeThemeId, setActiveThemeId ] = useLocalStorage(LocalStorageKeys.THEME_ACTIVE, GetConfigurationValue('theme.default', '')); + const [ enabledPieces, setEnabledPieces ] = useLocalStorage>(LocalStorageKeys.THEME_PIECES, {}); + const [ themes, setThemes ] = useState([]); + const [ manifest, setManifest ] = useState(null); + const [ loaded, setLoaded ] = useState(false); + + // Load the theme index once. + useEffect(() => + { + let alive = true; + + FetchThemeIndex().then(list => + { + if(alive) setThemes(list); + }).finally(() => + { + if(alive) setLoaded(true); + }); + + return () => { alive = false; }; + }, []); + + // Load the manifest whenever the active theme changes. + useEffect(() => + { + let alive = true; + + if(!activeThemeId) + { + setManifest(null); + ClearTheme(); + return; + } + + FetchThemeManifest(activeThemeId).then(m => + { + if(!alive) return; + + setManifest(m); + + if(!m) ClearTheme(); // broken/missing manifest -> full fallback to default + }); + + return () => { alive = false; }; + }, [ activeThemeId ]); + + // Which pieces are enabled for the current theme. + const activeEnabled = useMemo(() => + { + if(!manifest) return [] as string[]; + + const stored = enabledPieces[activeThemeId]; + + if(stored) return stored; + + const fromConfig = GetConfigurationValue('theme.default.pieces', null); + + // Default: config list (if this is the default theme) else every piece on. + if(fromConfig && activeThemeId === GetConfigurationValue('theme.default', '')) return fromConfig; + + return manifest.pieces.map(p => p.id); + }, [ manifest, enabledPieces, activeThemeId ]); + + // Apply (inject/remove s) whenever theme or enabled pieces change. + useEffect(() => + { + if(!activeThemeId || !manifest) + { + ClearTheme(); + return; + } + + ApplyThemePieces(activeThemeId, manifest.pieces.filter(p => activeEnabled.includes(p.id))); + }, [ activeThemeId, manifest, activeEnabled ]); + + const selectTheme = (id: string) => setActiveThemeId(id || ''); + + const togglePiece = (pieceId: string) => + { + if(!activeThemeId || !manifest) return; + + setEnabledPieces(prev => + { + const current = prev[activeThemeId] ?? manifest.pieces.map(p => p.id); + const next = current.includes(pieceId) ? current.filter(x => x !== pieceId) : [ ...current, pieceId ]; + + return { ...prev, [activeThemeId]: next }; + }); + }; + + return { themes, activeThemeId, manifest, activeEnabled, loaded, selectTheme, togglePiece }; +}; + +export const useThemes = () => useBetween(useThemesState);