From 170c19594e4739b35fbecdb159818b584cdc32e9 Mon Sep 17 00:00:00 2001 From: lionarius Date: Tue, 21 May 2024 05:44:31 +0300 Subject: [PATCH] semifinished --- bun.lockb | Bin 150028 -> 151763 bytes package.json | 99 +++++----- src/lib/api/channel.ts | 47 +++-- src/lib/api/notification.ts | 26 +++ src/lib/api/secret.ts | 52 ++++++ src/lib/api/user.ts | 46 ++++- src/lib/api/utils.ts | 2 +- src/lib/components/channel-context.svelte | 171 ++++++++++++++++++ src/lib/components/notifications.svelte | 65 +++++++ .../alert-dialog/alert-dialog-action.svelte | 21 +++ .../alert-dialog/alert-dialog-cancel.svelte | 21 +++ .../alert-dialog/alert-dialog-content.svelte | 28 +++ .../alert-dialog-description.svelte | 16 ++ .../alert-dialog/alert-dialog-footer.svelte | 16 ++ .../alert-dialog/alert-dialog-header.svelte | 13 ++ .../alert-dialog/alert-dialog-overlay.svelte | 21 +++ .../alert-dialog/alert-dialog-portal.svelte | 9 + .../ui/alert-dialog/alert-dialog-title.svelte | 14 ++ src/lib/components/ui/alert-dialog/index.ts | 40 ++++ .../components/ui/checkbox/checkbox.svelte | 35 ++++ src/lib/components/ui/checkbox/index.ts | 6 + .../ui/command/command-dialog.svelte | 23 +++ .../ui/command/command-empty.svelte | 12 ++ .../ui/command/command-group.svelte | 18 ++ .../ui/command/command-input.svelte | 23 +++ .../components/ui/command/command-item.svelte | 24 +++ .../components/ui/command/command-list.svelte | 15 ++ .../ui/command/command-separator.svelte | 10 + .../ui/command/command-shortcut.svelte | 16 ++ src/lib/components/ui/command/command.svelte | 22 +++ src/lib/components/ui/command/index.ts | 37 ++++ .../context-menu-checkbox-item.svelte | 35 ++++ .../context-menu/context-menu-content.svelte | 24 +++ .../ui/context-menu/context-menu-item.svelte | 31 ++++ .../ui/context-menu/context-menu-label.svelte | 19 ++ .../context-menu-radio-group.svelte | 11 ++ .../context-menu-radio-item.svelte | 35 ++++ .../context-menu-separator.svelte | 14 ++ .../context-menu/context-menu-shortcut.svelte | 16 ++ .../context-menu-sub-content.svelte | 29 +++ .../context-menu-sub-trigger.svelte | 32 ++++ src/lib/components/ui/context-menu/index.ts | 49 +++++ .../ui/dialog/dialog-content.svelte | 36 ++++ .../ui/dialog/dialog-description.svelte | 16 ++ .../components/ui/dialog/dialog-footer.svelte | 16 ++ .../components/ui/dialog/dialog-header.svelte | 13 ++ .../ui/dialog/dialog-overlay.svelte | 21 +++ .../components/ui/dialog/dialog-portal.svelte | 8 + .../components/ui/dialog/dialog-title.svelte | 16 ++ src/lib/components/ui/dialog/index.ts | 37 ++++ src/lib/components/user-context.svelte | 68 +++++++ src/lib/components/user-search.svelte | 68 +++++++ src/lib/event.ts | 20 +- src/lib/stores/cache/index.ts | 48 +++-- src/lib/stores/cache/utils.ts | 53 +++++- src/lib/stores/websocket.ts | 89 ++++++++- src/lib/types.ts | 32 ++++ src/routes/(auth)/+layout.svelte | 11 ++ .../(channel)/channel-area.svelte | 14 +- .../(components)/(channel)/message.svelte | 60 +++--- .../(sidebar)/channel-list-item.svelte | 78 ++++++++ .../{ => (sidebar)}/channel-list.svelte | 8 + .../(sidebar)/profile-dropdown.svelte | 131 ++++++++++++++ .../(sidebar)/sidebar-header.svelte | 60 ++++++ .../{ => (sidebar)}/sidebar.svelte | 0 .../(components)/channel-list-item.svelte | 48 ----- .../(components)/sidebar-header.svelte | 53 ------ .../add-user-to-channel/+page.server.ts | 0 .../add-user-to-channel-dialog.svelte | 26 +++ .../add-user-to-channel-form.svelte | 41 +++++ .../(forms)/create-channel/+page.server.ts | 36 ++++ .../create-channel-dialog.svelte | 26 +++ .../create-channel/create-channel-form.svelte | 52 ++++++ src/routes/(auth)/channels/+layout.server.ts | 12 ++ src/routes/(auth)/channels/+layout.svelte | 68 ++----- src/routes/(auth)/channels/+layout.ts | 4 +- .../[channel_id=integer]/+page.svelte | 12 +- .../channels/[channel_id=integer]/+page.ts | 3 + .../(components)/secrets-list-item.svelte | 18 ++ .../secrets/(components)/secrets-list.svelte | 38 ++++ .../(auth)/secrets/(forms)/secret-form.svelte | 114 ++++++++++++ src/routes/(auth)/secrets/+layout.server.ts | 16 ++ src/routes/(auth)/secrets/+layout.svelte | 37 ++++ src/routes/(auth)/secrets/+page.svelte | 7 + .../[secret_id=integer]/+page.server.ts | 28 +++ .../secrets/[secret_id=integer]/+page.svelte | 65 +++++++ .../secrets/[secret_id=integer]/+page.ts | 26 +++ src/routes/(auth)/secrets/new/+page.server.ts | 28 +++ src/routes/(auth)/secrets/new/+page.svelte | 27 +++ src/routes/(auth)/secrets/new/+page.ts | 16 ++ src/routes/(auth)/settings/+layout.svelte | 5 + src/routes/(auth)/settings/+page.svelte | 5 + src/routes/(forms)/login/+page.server.ts | 9 +- src/routes/+layout.svelte | 2 + 94 files changed, 2678 insertions(+), 290 deletions(-) create mode 100644 src/lib/api/notification.ts create mode 100644 src/lib/api/secret.ts create mode 100644 src/lib/components/channel-context.svelte create mode 100644 src/lib/components/notifications.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-action.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-content.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-description.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-header.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte create mode 100644 src/lib/components/ui/alert-dialog/alert-dialog-title.svelte create mode 100644 src/lib/components/ui/alert-dialog/index.ts create mode 100644 src/lib/components/ui/checkbox/checkbox.svelte create mode 100644 src/lib/components/ui/checkbox/index.ts create mode 100644 src/lib/components/ui/command/command-dialog.svelte create mode 100644 src/lib/components/ui/command/command-empty.svelte create mode 100644 src/lib/components/ui/command/command-group.svelte create mode 100644 src/lib/components/ui/command/command-input.svelte create mode 100644 src/lib/components/ui/command/command-item.svelte create mode 100644 src/lib/components/ui/command/command-list.svelte create mode 100644 src/lib/components/ui/command/command-separator.svelte create mode 100644 src/lib/components/ui/command/command-shortcut.svelte create mode 100644 src/lib/components/ui/command/command.svelte create mode 100644 src/lib/components/ui/command/index.ts create mode 100644 src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-content.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-item.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-label.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-radio-group.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-radio-item.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-separator.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-shortcut.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-sub-content.svelte create mode 100644 src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte create mode 100644 src/lib/components/ui/context-menu/index.ts create mode 100644 src/lib/components/ui/dialog/dialog-content.svelte create mode 100644 src/lib/components/ui/dialog/dialog-description.svelte create mode 100644 src/lib/components/ui/dialog/dialog-footer.svelte create mode 100644 src/lib/components/ui/dialog/dialog-header.svelte create mode 100644 src/lib/components/ui/dialog/dialog-overlay.svelte create mode 100644 src/lib/components/ui/dialog/dialog-portal.svelte create mode 100644 src/lib/components/ui/dialog/dialog-title.svelte create mode 100644 src/lib/components/ui/dialog/index.ts create mode 100644 src/lib/components/user-context.svelte create mode 100644 src/lib/components/user-search.svelte create mode 100644 src/routes/(auth)/channels/(components)/(sidebar)/channel-list-item.svelte rename src/routes/(auth)/channels/(components)/{ => (sidebar)}/channel-list.svelte (83%) create mode 100644 src/routes/(auth)/channels/(components)/(sidebar)/profile-dropdown.svelte create mode 100644 src/routes/(auth)/channels/(components)/(sidebar)/sidebar-header.svelte rename src/routes/(auth)/channels/(components)/{ => (sidebar)}/sidebar.svelte (100%) delete mode 100644 src/routes/(auth)/channels/(components)/channel-list-item.svelte delete mode 100644 src/routes/(auth)/channels/(components)/sidebar-header.svelte create mode 100644 src/routes/(auth)/channels/(forms)/add-user-to-channel/+page.server.ts create mode 100644 src/routes/(auth)/channels/(forms)/add-user-to-channel/add-user-to-channel-dialog.svelte create mode 100644 src/routes/(auth)/channels/(forms)/add-user-to-channel/add-user-to-channel-form.svelte create mode 100644 src/routes/(auth)/channels/(forms)/create-channel/+page.server.ts create mode 100644 src/routes/(auth)/channels/(forms)/create-channel/create-channel-dialog.svelte create mode 100644 src/routes/(auth)/channels/(forms)/create-channel/create-channel-form.svelte create mode 100644 src/routes/(auth)/channels/+layout.server.ts create mode 100644 src/routes/(auth)/secrets/(components)/secrets-list-item.svelte create mode 100644 src/routes/(auth)/secrets/(components)/secrets-list.svelte create mode 100644 src/routes/(auth)/secrets/(forms)/secret-form.svelte create mode 100644 src/routes/(auth)/secrets/+layout.server.ts create mode 100644 src/routes/(auth)/secrets/+layout.svelte create mode 100644 src/routes/(auth)/secrets/+page.svelte create mode 100644 src/routes/(auth)/secrets/[secret_id=integer]/+page.server.ts create mode 100644 src/routes/(auth)/secrets/[secret_id=integer]/+page.svelte create mode 100644 src/routes/(auth)/secrets/[secret_id=integer]/+page.ts create mode 100644 src/routes/(auth)/secrets/new/+page.server.ts create mode 100644 src/routes/(auth)/secrets/new/+page.svelte create mode 100644 src/routes/(auth)/secrets/new/+page.ts create mode 100644 src/routes/(auth)/settings/+layout.svelte create mode 100644 src/routes/(auth)/settings/+page.svelte diff --git a/bun.lockb b/bun.lockb index 9931be6a8bb56d9f4fc2c9ec180acfa1eb5fa35d..33b43e62b005cf7883e2e8c23760d1c3feceb0a3 100644 GIT binary patch delta 15824 zcmeHudwfk--u_-Y2{|E11-T+Fbq^uQK}g&$J#GmRw}g{8xsdyXq!mf1xBAjiS!F3L zHAAZ%I&N*uv|5y@YANmLV5)6eTCH1AMHRp2*?XVz&X|7s{x0w5_lN!QoM(NX_1$ZI z*LCmgZ7+Vt_VHTV)8QS$-@n*;+mDg8hfWSsI5e%Hrd8%TbMvyMYud}OeP9xxc?5vE;VppPhTF%VrS%vwUmXnp6m6KIeywNHy8_bX< zplV<6C@}kSJ3^#Bd7{aqz|8+vR@u5%%$#a^IW6{?W;Qh2H7l1vX|P$!c`N@Km>oCW z<;w4pRiq`rW`_*{vz9P$TkwpcE=FHC3p4Vag+;E$(An|cU^YH0BR8+mrRmxo8R65S zq8fuw`3Z~jyJY93Ig3$8HX2#1J>fF*FQ%B`1)$#;mb(vUy2@rT+C9)&PI|VpxY#va z`vU$9&s9`XoRwXqxrz!(Fb>+44D)_FPd=6Tg%mMinjaEe?xvs{IkE70jb#muj$9Bv7#5mQrSRD&76ADr0% zb-?VHp9)O9*5XM;roXEw&6(|-?ivZ3v!fT76_=qL1{7POX)VAyH~<_3ZUat0x7XKX zc>SP`!)BXZ^)Vz?^<}Z9wFG0n8T>SsCQmIY-BwutKkBxuf8q7Z+nyOc+xF_TRj-!+ zPW#AXTV8zGgt=2kw`kL9&nJsyBmaH@V?Uex)#Q_v_5Rsu+Zg{3;(YBW4y7B1pAsEK zCqJeyJfdmQR+dfH7I%_|huG>nG%eN=`^wrdhy^xph>;LIr8CqnzLCer#o1bq(6pY= z>cGh^Cd-M5apD#EEPlV1#}ni9fRQq2e5}niQq$sCo?IJZx2=QK7nYEN22BcZ{0act{fMuuNfszptS4o8DyNlwx!+HXSAmEhUF>Owzlg_ zM$45+vHAz2MhAJyPXz4{`WzeKp+d7E-3=!4ZF2L#oOUSTLyI0gWG(w&X zjnPZT$)L%xwmlHoeoq+|Ww#0JPV7juc%t1F1*;pZx-zVV-Bt-}*e?a`XP!(R9^-YL z=g8#H7+dG@W_uA$m|agFFN2a}Z8Z>CzsCA)S75P)x~xTacS??rNQFR3ZuLJ zm?S5q#Cm;B7x{eG7_Vj%kS$LJ#psC>WYE-D+j0m4;9p0gv-M*W}1<6r20YkG$y8&s%h8>tcFy=;yGv}&UqbHPZ^5gxPTNV zqlc_*Ww&)tvlfmqaq?ktpuJ>mklkw?tlq3g|13>TN{{twI$hIZOk=dXBuq+G!@&d8Bb7)P^cvw8o=v`YiEMwI&MB8CloJnJv#?shM~)0 zF)+*_OqXg{>_#+uoZWU17E8f!gxd9b8S+F>tgSz;0Yl6aBJ8#&V6j@vFSL25Wub3J z*=@IAu_6RGqNg{m5Ud9q%NV=YELfvta$t;YFH)Qp=$LT3EdbXIc6(i8cW}Zo*E8qV zDp+HXXZFQeSZqDoi@s<*L({q&c~}XyG+2X-QrNZ9G9(W*krh5di3;$fwgGP3UOTq=Ed1uIhq!4xT8Px z*K_0vXRPfkM79d69A(7fqGjzN!FGLduAGz~YdZ=NI}L*5PBb7-p2&~Y-^r6f1+lh9 z`DUhW4tyaHP}d9MJm}yrOZM%$k5;z+iIkuP*FXjQ$K^%9hNzijb^(m z;{Fv6>z6IvYZ0ti89KO+ClZN9q0LL(sSQY(Rh~y`G;{SnWpYw!tX@_oSC;m!2-7r; z;6c`b)<9FB7f=`I2k_`;{F>~6^guuWm{$f52J^TddqKxF!zdqDMdJWhxD!SJxYikr zu`~`c0~%wR2G>D)Jiq|3dm8?ez&yy*aR*}@_c8-b0m#@ljbp0jFi%K@$&55pH<$%s zu^Y$zn1NxT8VA`!c6qc}1xvxyHjeu-OU1-AnuV!l9QR}TVU`%jz0C44H;ia8;Ko5_ zG-dAoM8Y`8%yT_Sudz4s*BM?0 zR~RR*hrj|}v@*ync!Oo%%k+C0AirYelN$p&0o)vG9{}}$zXJ~e-vT`TmCc!dpNxNZ z!vDG6j72bd9>z>%!Y%v{bKn1+iW?x_1_;zc9xQJb#)$o`u*n>_rj||ilD>1Bg+bIp zU@;>UKlEw=wt?GImm}vk>jgvWNDJJFKNfSkcZJQfA}sx0_K=(Awv*@n)7u!&D9huw zFkPeZ!y0;9=lyq>5%sao1N#^Ys6PccvZ94CyaTOlGN-@8vj1-|!yb+DxUVEw<&qiJ zIKhjTnfUE2Ll>K2Pq2!eY!yppH7OQ5!MrYITKQxqvn-p;Ce!}_n2Y%s_+jvEE58X7Y>_9uEY_v(67zad*HV9tS(3Nq_gk3$Hp`#PWIf9! zGrztLS;)RiT2yfSAF|}e7W-T7zs4-4iRDLTIRTbUW_}=;Qm|!*(7-`veyENKO09(@ zw6q+^oF{E8y{)DH7G@yrE&qF&6E(up?=mJRwI2B4F!cg6vfh>>nRbk2lbP&m>9Jt0 zticrj4YMJ`k;a zVBV3;viLDD{%IBERsv})%z`^jht|OV!<**{?Vh`5)@#^o(f`cNbJD$c&*m7VqH+e1 z2Jj$r?9yrAxF55pGL2l5AKX0SCi=n6^MjjbYfW%H@%T6V;N}^9^Wf&$m?FH}#y$Uo zo972N&kt^%u@WBKJpX@t^DN4@9e?^%D(Yp-(K(b|J*am@p)#ql)xQNAJ0oa zczeP3Q-f<>8T9vteg1qtH&37Tab4+qDnK6m$|0`Ez*9+b|0!Qte#(KDf$za8J?$&I zoOXz7vg~w{Z2z^dJPGTD?C^DxJO*pumyVG)?+s_}+iwNlx%1VOzPIdUe;T(Y>x;PL z$8Jm;xcSp96V5FgdgF`WMca#_U%EN9t8iR>Gj#TmS0;xaZML9l_3@0#e{8&6cegD6 zCP0=Tnp?68b@V*rD^Gvz5cpph-z3R1u$Fz}(1nm+eluRK_|{hrKI70mgseU@UJm=t zS6)5i5MHwXw@LCkto7eIL|u6a)`qjbGVwcyz>AvilH`PQzS8TgL)4S;XOpD&d0+V& zEMF3PhIkrM_~oZucM;xbwY zYuROo2$Qw2R{VelUU7)la_N;MIqWLh2dk~@{{z|wYyA%n(N121wc$s!@2W#|kZZ4^ zeb>;wA048TjQq3>LSH8v=5f+nnQGzTVQ40K>MycM1*u+NBe$4`(X8uemBrQ zSS2?cB1-OtReBTc`^h2fvfwAQ?`O0RRxcTL6YYbw;HE>w$Rn^8-a`9+c8I>R>Swg? zHrfX(PDb29`(Q1*)6viq)PYC-dlz2i&RJ$pZ)`8I03qq1A@WM~~x)6?1n5e?)KsZKWK^+K_ z)e#B{y&*)^g^;YO>O$yggK(O{R2AV3;S7ak-VmIsmcj}j2!m}9($rELgkkj{T&3Vr z{e2)@r?B1!LWa6TVMBcgiS;04skQYWOz?%^RUg7k6<;5McLNBoQOHrk7s3t-E?)?F zY72$Th7g)GfKZ^E4Il(Gg0PQ5k@9N@VLyeEh7d~BZVIJ-5ZX3^Fk2Ngg3$gU2uCTD zsW3kX$0#iDgYcL-LSbQJ2vH9~C|6YvLFnlZ;WUK`710>N84Al9L#R@<5X3yy%O5mf zEhRl(^R2&V-B?Wv67_5Lw-Y;rK0RLjE?l6oeoc7DkJdP*9Xk!4b?z>q?Z&hssZ$C<^Io0Y1vuf zGw?c>e(wLsWNI~+g2k(%W=;=LHGFx9*z42Ndb&GjzN%X;`i9{Q?e-n>U^?H}|INpq zhOM0CqH#rmCG*LM@gfGy=gh2;PrL#E9>ta)A3lv@CXNzIL4iL@5la+|qoJ zjsbYgwKP7`O+lLFx*sL;wb)4BoAao&9QjPx&uSg}6aV<%F158x;9x76W!!rgBM}nm-whH8MV6qy= zQQ^AH%A==?8mnzY0X*1%nVQoXf6v{L=b zg{PWRA?7q`2`@fr~mM7GKDiK}5IsO-5 z0}u(FZ}oZrU4d>uM<5A(F$5S23gZ$sYfiE)ns>2KT6Dqs|a87yv6Omj6dp5WfutVbuOzd>V3r&Bd`NI2o zU=8ql;Aubtd~fqQz$v~6;1vEHZR9Ng=CL;mk-(m(6$8BiuI4^KUmzBUQ(^PPWN}B8 z&lhnOXP|ID;QHr!=UX;zU_}5oqmjTsU=T1E7y=9hh69fPeSp3|ED#6u17d(^zz*~T zUPc{!VxIz}0{ly6CA1aXz}mn_hQXH~+2EN#L!=u4+)MbfhA+Cd0Q|2Pi-7e2-$|_j zUI4hitOgcBF98MvLx5qxaG)R19~b~s!mk3z0W!IPjYfj+4^|;F1>ox+ycyCKBmD>D ztpi>JX2H$?xFL-O9svdd3CJ4@@U7j;z$?H;U=!era-M<@H!SWu+n}X^rvsCLNN%9K zv`hx3022Ui$-L5419JiT@EX7^n16Sw}6|#Pc>2hAsTyCTtxajK>zRWN#8*F8t?kZe=)N@(#?SopaIYwI`@QNpb^jz@B@N? zKp+5U0yG950=Q>1rH%8Nkp-??6j@+vpcT*tP)iq!9_od~!dAg?BG`oKVAi=An1S>o zKo-FG*p5sf2I&m23*b2%cb=C9IDu&ZXJ|6Ona0_i1dIbZz-HN8CR{F281mI|;Ub~hJ0)AZ%gq=PvWOcNX)`Vsj+ z9*_&<0R55gu>7c36yv7|C;$q9{>bD>+#6>DrNAtJ!LdMUteouDc>(6I_yMfE45$Ol z1?B);Hf#_#mdC*5fOUR?F>zSgJfI3-q3-eKc(Sk(fR%Hi-5VG+2FUXmAbmTirAtI( zp9#+(jU)DG&8thqKz+POi3pDl?}@IoJ}Pp5DxhQEG!OS@LGF(Ubdl0MJR%&cKh63e z$^9(?a=L|g3y-j?F4dwlFZ||FlUglSwhZ`}D(K!TyqgN&CIUrw)o-2fSBtj_@22iA zo7^8Y=%QPAbcE`>O$4j4uZt#zSFroLrI$yC9(k_%qc>1}M0gJdHcwTs$ZGZMHqk@8 ztPXAyoxLIvnpMd|+eIs}T19OaJ?>u$nt=tWp5KmAEUH$ww!&(onr#z6V&%LB0yYLwjClb#QhOh zcKtU>{xt0H0i(%ArP<@uGds|zjpNkMJH!aRW}JHDzcsOJ7zROK+ev!d%C|o?v)?z=G3a0Z-~H39KIpuA>%6-jNyrICj^~N&(!ZWslYKX*aI(6)rostFXOSYA4^#d3&mIwHY7n4<=w?u%?0eCbw%T9qE?fwRYGy3=C`pvcBiQ4upsnx+#PZyPI zm&m~V{J~vVR^C&Uz8l3fo~okBVN=x%@{p-&H929bsyv9|oKw{~+B2uBmU}Rj&#PX0 zu#sF-IeP^DzL};L?m@24H1#HOL)>5YZF{exdO*g;wg@{S98JT_nx?)t&$qoJMuxb* z&l?~A{wQao?eD@d+MK!7(^cU+7>OrbYS%jmARt|xcn3>hPr9127Xe&Re*3UFKDk%4 z2yuU8x8j+8d;hj$LI6sM!nTK76!gO0y`p35+nMItoRDR%2j`x!eM46SUWI2QJh4)Y z@&e6b1J$5?*iI5>s7LpS?&!Ep`!Li*=eM|IkxyPtodTo>mHjAtYGC{JD3kG}FNRNn82^fCYH*%^Q7`C@LmKgT;8 z<8;2)eUuHQW8Q^FNB4ApuvZhX@6*GFZ=bSS84-ymV(P8;#E9U3j;xW`hf=xAe5u4C z%m%$g^~2N)aeveIa_GtZN4F0CQ5SYz+%O4ZOH`xxaeaI7fCzHCyJze#w&}D|Pne}P z!dd@#mipp-%=BTi)tnE|N~;>(^a}{S?>fVd-A%LNDk@dCabk%3!@;_v9-E!t;lhA_ zjw`@i9N~41E2nz}m|+B{tp`P9u={(;qBj%&_^RIUk}*2ib2WEe?%;w-RP-Uy{a5

IJ*%$tv{UAkJgAzjdT-h1pmDq5)OVBNn-FAP9&#-sgmuzBHw>8{2uru=&8 zA<8>M57u!@(KbYHr(O-#o2d~YdRrA9tTz&oYRu=NbxlEt{*zFt!TM0Io-9Va-AZq& Uz6;j-t9D0xzrzJ_tle0)Yfx!UFdxDAzD2j>*3Q9BL$k?{1ETSO0%sEx{n#_o{^ZU)rKV$tkoco>o z-Q~M=@2yJJs}CFO_ph<9O6WG#x%sb8fBwvg*w;pfJip_y1CK;4$~l|*@SiV_?=$Rt z(9vyoj?y(AOX_ku>R0dFQeD%+TU=31si3H`qE}Usmg3U1)|ytk9UK6zbQcu33yM7* zV28tAG+NVIQLiX2DnPksptpgYG)B|ff;Yep2D__^$|{iGGDFjXVC!Hz_;E1nEh#GT z^qO9vWhw}4aQ0Z!(OpqfS~#bu@;g+>irWaYz(rKXhL4Ri8+a3;gut$GmlPvQTajtz z+vG9-@TkM%(KK?)i%QDfGc;{xaT$B7)lHCX?Kc(e%Q7o1DxKviN90ANGm495&Z=B7 z!7NrXXL?cjTumz}DlIB0s;GR=s_p?WJ5hti0>NcqMsoLud6firtl!p6Qx^KfC9tsH!X~uFyOc^Qt`MHCpIQv%IHb zZn?*!rD><~%v1I|WH7#ehi#1JEYscxI~4l#vck&pqEb)cEO+r7G&sLVc5iu;chqb% zyEixrZjEr~lw1$)0A37^2G1_mwD#ai*c{gBV8%1L#BAOLJCgPkv)ooNn|%Q%gWaj{ zl+5>(W3E8w3|nO8duwM~!XMD5cF0%_W`U%6<^XgBb6}#u)OCv&qB8n>Dhl1j?&+RV z*qj}?U^ZNXdf1_Ea$4 znVl|n%SqYk`uWkaKHF&@K1S2hS(7~0-eF$~YY;3U-PsO(#~2wk(P@vwvL9}U$2vLe zd9Vh+Y9?d5IP}dK^2kJ|elkNwO>*j8#>(7DPJ1=>2eSZN?Au|nBbsa&<Jv7^lPM5K;Mq3v8 z7Ui%ngXJ_D;85+&mi1GeHZ2DQEOrc22=ikrEDj^W&T`mK!D1~q9Wf5Q=L{JY?X(x*5_pSQAkksp z1dGjLdLiadEem6t;jnkW-pqzyap!^?j#QGNV-?t!!5R*$g%RE!dilx-O|CCZx94Hg zXHO6$TC9Ub4@7`A-iF0C%y|@vOU4u>ub6x1hpO8CCAo|5hq< z%boVbGPAeX6EG7!uvpxf0{Yf68CBu5H=ApU*dsCQQ(#Sii(R7A`i{Bs2waBX`oXh? z*@!W^8Al68*#sA7XUEjYU%j%M7ofE0z!}4wAXibghu}RZe|&g**b< zULm9AJ8g?AHEq0%nV)7mfYb;h)vij@@{E)hslhTYI?etXQYomaCA(+41}n)pi;44n zO>o;Phh?65PW5MCvA?=e=4)74My=@!e5tpQGTRKQ##ut{Kv6C0Yn*z0wLDTYz}rbP zu4?$-WE~s_v;z>C)&fWecr;}{*h2vU46_*1%Q$YtHsp)~Sij5gGT3V*DX=13`V7X^ z%s9v_fZ1gn4&=#!$055*77xMtSsQRG778=GIcC; zaRuy}=najwb9iGVXxP3KyCAH{@o>TVmxmWmde@(#iC@8(>FP0z7`6adXEC?ls&@ z7V?2R!-FY2V5tVnjdzC26D$3_oGELp95NeP3$Pw#+3UbO$kgir`fUJs{1W>k|4)XO z$t?FMKz>Zq%o&MwHz(+t^;5fkJpR?KiuWyO9MUyY{wZ7S|1{(03j7c2DL>n@CJhB49f3*LiV~Zyc-=tU~xu-@$6X4 z85L#OWFZH6!@JR|11vkZD^>8#7I&k8!(h4bzVK0;_6bl~s*jc1l-ZMHOaCPn^5lJ8 z~#j}W4xJGy=3-kyk$4x z?vQVo@e^IhF-tmnS#7RWO;cvG`OsT{Jz%=bu>8nO&a`-zrIXpM*_KUaat{7uJEfLR zraez!@lz|eGRRC;SoSY5XVIPTYXiO?%nq-z%8{9@vurZ+*IK;Z($@}}$}3ZArVGLuhPHkpyWWZ7i;ZwGU2 zz5~7q{H>KwW3Z4nC$j@rEt`yg+1ih0Lc3@)}k!`rp$TT+0vV^d7luE44$SQV0N#kuXX~m%fgKnQrq3;wBbhr{2ACZg3#MO|l~1OgZP`tk(nS2n%W)x?`{gW)OThT2 zm2v!;;L*u~FHJoT_}{!G_PW>_j;73r{*P{nXW(AgJjgspZh##q0CIWV?b6~!?KdMi5#cjZi%fC`R$hYpWX5Pc1w(@(x02r&oI~1Z@0u4gx_w7 zf4e3AHMhcCNc7{H`t6n&gV5wwm}5ix|NAZR)=8^RWJ=F**O=4BkeL59JNVs`=SP(v z>hPz9ZR5nqb3qviN7m0gb&s?6?kOK^*<1Kh!O0b6MF+;rxY+Gr>5zXZxbjrYmgg6@ zns7Ma{%_m#>6yJ>9{pQ{4ExF@&dBv&+5W} z4QBi|F8!J&%fHE%v%U_LhrV$Myr4LdBO|^El*>=Jgr7VBYY(iHlP+PCOHSs<>Jx$T z3s@~=;;9^Y^T|ND_LNKDjRCB~utuDAiI%eNbdFqkDo~z-6)1;%nk_Ty z8Cb_*Wu0+}VA*g65q=veZQr>>h|K&B5uOQ@e}NSy#rKF1mgjqy2$x%6ZTc=yhMjfc zrHT72BK#f^!itu`=MdppM0n06V&qO(+hE22;1V6>ydQGptaFGDR;-LYkNAE-eCJ)F zvpfK653H06E)g%6TtIy15g)9sGVvnfyMXvExxddoAgj>F2j;u49n;R@orjQFm)M6%4hiukS|K3FMI{D}Bqd46;Whui{d z(^bTG%_aIv_cg@#BjSUVwl)~=O#QC~uB{R-F=*{hSlg}zs(1l2UCk3BXYDKzs1DH{ zqGENCqap-^Hvj36jJ;kj8IGbAXMuRzMyccN^Azc1L*Y1utQHV5RYMC1>zYHb`9m15GW{WpYysgf z6ta}CL%2Y}V~3EVwousQ4m+CrG88rniw7YxA` z0--`>hCmqE7Q$aBR4EY(;Q|FuD1-%S3x!P~5W>PB)F^itgo&XLc2iiWg4;px4}(zE z4#Fa}lfpI%@!=4>YF;>mS?xq%c2jTW=<6N_f5Q!n9f zbKxfjEmdr9(OcCf3V$`dm-xiCD;tkx)Vyf%egJ+$Q(0N$DR)d(9eRraHap%I1*+J1 z@oz>QgZrQk>BbYTMrgeS#YR-LS+{k;E1f`Al1{P1AFx;AX+iDnc;y%X=KbkvpcT@a z=dxEr=@;kw^-5uJQNrvB^Y236FIh~ju`W?OCmNqg6-!113|?g31CR8o%e7*V_s47I z!JWCxx|;K;91ow(ro#+H)8<+}{Ii_z0C?b`r*Rj+$1{WJjKlZblTS)2%_fa!piJ_q zOO>g4jZCKb>?avunwhMJk838Ij@kk+lYIEp2=J)3G>nhtLIHN5#?ttx25&iy;|@#1 z*lD9#366z^hG#~BF#N1u^XV(|`TS8@jvO#1`AX&%fE6#cG>())n*Mz5Nk6`p;!8&! zwGh}TzP=et0mm}S4}ZgJNIIs;t*J_#!Y_@rzSkPA!(rT}?BJ}?!S2BZUn0Y3Ii1d@Pc zpf8XD@agYf;5}d;@IJ5~H~@Sg(Dx6KI0$?Md<+~0{;FPCCOWIXEfbvvorHY~I1Ri4 zyawz9UI*R)b^~t$ZvlL_c_+X>q6KU}RkB=k#BOQMT~5dO0H;o zNlipD3Fr%?0uJC^;5}d;!1o2kKnaiwa8^I1sY}a6s`o|YKLI2|>jNYK2|#avFOT?Z z(0)LFU;q#Y@bN8QXg&ypARP*X0j+^H0H2`E0*U}WAZ`Wt1Ds^MS-%4C<{ht1y~dMs zK21Ll@U_4h;5*;0DR@u9yT^BEgBpG@J;DM0Jp3L;P(KxD!wAv1aMVx z6>*AjMcjkDWxx_(9!Ikr32a?je}HQ?4HyUv0-V5cm3g zSmisNsXzfR8W;);1BL@5fLnl(z$hRM7zhjkoIpA-07wB+0SB-NE%M>;IA8+6C(i4k zDFBbry~a-i_?&$X1mn>p(mKFhVXZwL%<(^hk3?7H22D}Nb@a+vb6Q!anQNN-41XIn+{9@CIfCDAD9Mki^>705%l?NxiGFC+M~bc zHWin}=`;@+PXp{2ry7Tg@9pA&nEW0oXsrF&*HlF9f>*mWc*j>Z z0I&~ujd#{u^tS?Ra0HMJ^ar@Faj|pJbMf~DIOCGRi9jEq8_*lzKyX9u3Pix?CU6g+ z7tj;v4$${zfI96ST&GD$a5-?-;Vv@}@a;T(CH6c_?<^oIf5sUAloF7RzudW@wr zkAcJ>KNHM#%%Hd&vw;af7BCsujP{M5bBH)J90^9uXgSpwB_qoR*bs-C+b7fX<*6wE z^HeZR@bu7+C?yAA;@5%g+K*R36ulu94n;8#>qZ? zKGHlb!2lbt0(9UGpc>%1VL;rS7JzF2tNiW8#9?ELfkgl-^__2?PgXV$VB?%a7PwPdojR9sLseG+ud7)OQjcRP0NlT{G+sDeB=D#R&UwjBF&Xo*UH3 z7e%CaMvd79GTf5IcC}%f=&6>xBx2R^Z6XSPcn0Vp^m&P`s&BVsW#ii@PO~e8P^MZd!h2Tqfkwzs@^U}xA%Pj zGUL7dwu|oilbPzKSJ2E$nJV`cF{9(BndS!t+>7`Z z(ZlhzJ$o$o6oiTk$ca|LjUvoHHet5h}eGM(Hn4o6ACTjJ%3F^da$lsTxChP#0O;p==h=^$4_ai4R z6dauz;@=lF_BCS&P^Wi@$kb;rFL7T}yXv#lM#t_x0imM1HAUV>PAGD&w9~UT6}(Y; zJ?HaD%DGcSr0STEr^18N zB=L0O-kv+3zw5e3b*|dM+9f=~;8FNx&3gflY-rN{CM)N|1rNKjcU69Wz3eNwDrgtN z{tO0uO{yjH31*uQhqc1Mo&?n?!vIno}zlbj$vIi zMNNAhcHI>995~eXMaLh%|KsC5zrK1&7mrR+L2rnNiEm+l?}8dU_3M1~YESij zanjBD#*KmyqIMS>LxQgEHpU#4O)65M}!$Bx0TRU7x4+zJVAT^B&wS>h*nj z>Y?2jaNoBv8`gJub&P!CEDAUhk`i#KJC&#Q?-nzneV^S#?y0jMUB3BgcqC)xC*zu` zGT#(4MJM&*n^;`k^3|y~(LmpP755f+XuirO&&*f%k}LAnt8Za-+?B5m)4o4nwb~;h zM3CCKM?@;;9ud{v_eIi zT{G*^;rL0DYJ=VuecSuKM9R*5H^bfLg||>B)tv5I3swHxIHQ}Ut7qOu=lXlpn{SKS z=+ALa(-LQ?q+5rAr`8mV)dkl$PJtGx-0z6w_P%e6)~y-*#=&h9BT!{ZLb5e(o8J-L zJ9nIAuJ#(R9aXt^#qPd!UF11<_Jt=-mQht`bK|6&Y1QLh(O;imq-MM;l2U(W(LIc1Ag`@LeHwY80I@B1ie?6U_JocuU41toYEQ5Vkh zg!e@EE+b3L&zk<(j7U-k-xE>#3#IB?6w`N5%g zG`IWhqzpUCCMNVvNKNfmTCP^Tk8`=OTs`%^7#;mzd)7w$9d+=O_^IIiB2w>Msh-=9 zwcz^>>dBa+dk#H6;s;$g68iH*X;UlJwEY-W%TqVkM#MjCGe1cs91yegiYm4404Dg^ zDz*23sBiE4mg>1rUOo|9(KE`3(zuHGsxym6MBi}bC(_mIY5O$`jC-fry2jUEFN#ZC zdA)~Q7O1%g5wum6xhNA_;2h(A!diLg*oqGkg}JRoi2oeEo>r;i4vM5-7zVQuEafWo z=s{5*@W}h-jl{J5YUCj?xEU`IYULrZx$B^D=7YXv{ZjUd>YyK2?KeFutuI3Z23GZR zc1l!(*NBM51s{onx2dWw`Ykp`Lgmam)TuZ('/channel', 'get', undefined, (data) => { +export async function getAllChannels(token?: string) { + return await apiRequest('/channel', 'get', { token }, (data) => { data.forEach((channel) => { channelsCache.set(channel.id, channel); }); }); } -export async function getChannelById(channelId: number) { - return await apiRequest(`/channel/${channelId}`, 'get', undefined, (data) => { +export async function getChannelById(channelId: number, token?: string) { + return await apiRequest(`/channel/${channelId}`, 'get', { token }, (data) => { channelsCache.set(data.id, data); }); } -export async function createChannel(name: string) { - return await apiRequest('/channel', 'post', { data: { name } }, (data) => { +export async function createChannel(name: string, token?: string) { + return await apiRequest('/channel', 'post', { data: { name }, token }, (data) => { channelsCache.set(data.id, data); }); } -export async function deleteChannel(channelId: number) { - return await apiRequest(`/channel/${channelId}`, 'delete', undefined, (data) => { +export async function deleteChannel(channelId: number, token?: string) { + return await apiRequest(`/channel/${channelId}`, 'delete', { token }, (data) => { channelsCache.remove(data.id); + channelsUserCache.remove(data.id); }); } -export async function addUserToChannel(channelId: number, userId: number) { - return await apiRequest(`/channel/${channelId}/user/${userId}`, 'post'); +export async function addUserToChannel(channelId: number, userId: number, token?: string) { + return await apiRequest(`/channel/${channelId}/user/${userId}`, 'post', { token }, async () => { + const channelUsers = await channelsUserCache.get(channelId) || new Set(); + channelUsers.add(userId); + channelsUserCache.set(channelId, channelUsers); + }); } -export async function removeUserFromChannel(channelId: number, userId: number) { - return await apiRequest(`/channel/${channelId}/user/${userId}`, 'delete'); +export async function removeUserFromChannel(channelId: number, userId: number, token?: string) { + return await apiRequest(`/channel/${channelId}/user/${userId}`, 'delete', { token }, async () => { + const channelUsers = await channelsUserCache.get(channelId) || new Set(); + channelUsers.delete(userId); + channelsUserCache.set(channelId, channelUsers); + }); } -export async function getMessagesByChannelId(channelId: number, beforeId?: number, limit?: number) { +export async function getMessagesByChannelId(channelId: number, beforeId?: number, limit?: number, token?: string) { return await apiRequest( - `/channel/${channelId}/message${beforeId || limit ? '?' : ''}${beforeId ? `before=${beforeId}` : ''}${limit ? `&limit=${limit}` : ''}`, + `/channel/${channelId}/message`, 'get', - undefined, + { params: { before: beforeId, limit }, token }, (data) => { data.forEach((message) => { messagesCache.set(message.id, message); @@ -48,3 +57,7 @@ export async function getMessagesByChannelId(channelId: number, beforeId?: numbe } ); } + +export async function getChannelUserPermissions(channelId: number, userId: number, token?: string) { + return await apiRequest(`/channel/${channelId}/users/${userId}/permissions`, 'get', { token }); +} diff --git a/src/lib/api/notification.ts b/src/lib/api/notification.ts new file mode 100644 index 0000000..3b50d32 --- /dev/null +++ b/src/lib/api/notification.ts @@ -0,0 +1,26 @@ +import { notificationsCache } from "$lib/stores/cache"; +import type { Notification } from "$lib/types"; +import { apiRequest } from "./utils"; + +export async function getNotificationById(notificationId: number, token?: string) { + return await apiRequest(`/notification/${notificationId}`, 'get', { token }, (data) => { + notificationsCache.set(data.id, data); + }); +} + +export async function getAllNotifications(limit?: number, offset?: number, token?: string) { + return await apiRequest('/notification', 'get', { params: { limit, offset }, token }, (data) => { + data.forEach((notification) => { + notificationsCache.set(notification.id, notification); + }); + }); +} + +export async function seenNotification(notificationId: number, token?: string) { + return await apiRequest(`/notification/${notificationId}`, 'post', { token }, async () => { + const notification = await notificationsCache.get(notificationId); + if (notification) { + notificationsCache.set(notificationId, { ...notification, seen: true }); + } + }); +} diff --git a/src/lib/api/secret.ts b/src/lib/api/secret.ts new file mode 100644 index 0000000..9b2f9fb --- /dev/null +++ b/src/lib/api/secret.ts @@ -0,0 +1,52 @@ +import { secretCache, usersCache } from "$lib/stores/cache"; +import type { Secret, User } from "$lib/types"; +import { apiRequest } from "./utils"; + +export async function getSecretById(secretId: number, token?: string) { + return await apiRequest(`/secret/${secretId}`, 'get', { token }, (data) => { + secretCache.set(data.id, data); + }); +} + +export async function getSecretsBySelf(token?: string) { + return await apiRequest(`/secret`, 'get', { token }, (data) => { + data.forEach((secret) => { + secretCache.set(secret.id, secret); + }); + }); +} + + +export async function createSecret(name: string, content: string, timeoutSeconds: number, recipients: number[], token?: string) { + return await apiRequest('/secret', 'post', { data: { name, content, timeoutSeconds, recipients }, token }, (data) => { + secretCache.set(data.id, data); + }); +} + +export async function updateSecret(secretId: number, name: string, content: string, timeoutSeconds: number, recipients: number[], token?: string) { + return await apiRequest(`/secret/${secretId}`, 'put', { data: { name, content, timeoutSeconds, recipients }, token }, (data) => { + secretCache.set(data.id, data); + }); +} + +export async function deleteSecret(secretId: number, token?: string) { + return await apiRequest(`/secret/${secretId}`, 'delete', { token }, (data) => { + secretCache.remove(data.id); + }); +} + +export async function addSecretRecipient(secretId: number, recipientId: number, token?: string) { + return await apiRequest(`/secret/${secretId}/recipient/${recipientId}`, 'post', { token }); +} + +export async function removeSecretRecipient(secretId: number, recipientId: number, token?: string) { + return await apiRequest(`/secret/${secretId}/recipient/${recipientId}`, 'delete', { token }); +} + +export async function getSecretRecipients(secretId: number, token?: string) { + return await apiRequest(`/secret/${secretId}/recipients`, 'get', { token }, (data) => { + data.forEach((user) => { + usersCache.set(user.id, user); + }); + }); +} \ No newline at end of file diff --git a/src/lib/api/user.ts b/src/lib/api/user.ts index e271a63..2195360 100644 --- a/src/lib/api/user.ts +++ b/src/lib/api/user.ts @@ -1,4 +1,4 @@ -import { usersCache } from '$lib/stores/cache'; +import { channelsUserCache, followingUserCache, usersCache } from '$lib/stores/cache'; import type { Token, User } from '../types'; import { apiRequest } from './utils'; @@ -32,3 +32,47 @@ export async function loginUser(username: string, password: string) { export async function registerUser(username: string, password: string) { return await apiRequest('/user/register', 'post', { data: { username, password } }); } + +export async function getFollowedUsers(token?: string) { + return await apiRequest('/user/me/follow', 'get', { token }, (data) => { + data.forEach((user) => { + usersCache.set(user.id, user); + followingUserCache.set(user.id, true); + }); + }); +} + +export async function isFollowing(userId: number) { + return await apiRequest(`/user/${userId}/follow`, 'get', undefined, data => { + followingUserCache.set(userId, data); + }); +} + +export async function followUser(userId: number) { + return await apiRequest(`/user/${userId}/follow`, 'post', undefined, () => { + followingUserCache.set(userId, true); + }); +} + +export async function unfollowUser(userId: number) { + return await apiRequest(`/user/${userId}/follow`, 'delete', undefined, () => { + followingUserCache.set(userId, false); + }); +} + +export async function getUsersByChannelId(channelId: number, token?: string) { + return await apiRequest(`/channel/${channelId}/users`, 'get', { token }, (data) => { + data.forEach((user) => { + usersCache.set(user.id, user); + }); + channelsUserCache.set(channelId, new Set(data.map((user) => user.id))); + }); +} + +export async function searchUsersByUsername(query: string, limit?: number, offset?: number, token?: string) { + return await apiRequest(`/user/search`, 'get', { params: { query, limit, offset }, token }, (data) => { + data.forEach((user) => { + usersCache.set(user.id, user); + }); + }); +} diff --git a/src/lib/api/utils.ts b/src/lib/api/utils.ts index 90ed6e7..27868c8 100644 --- a/src/lib/api/utils.ts +++ b/src/lib/api/utils.ts @@ -12,7 +12,7 @@ export async function apiRequest( ): Promise { const url = API_URL + path; - console.log(`[API] ${method.toUpperCase()} ${url}`); + console.log(`[API] ${method.toUpperCase()} ${url} `, JSON.stringify(options?.data)); const token = options?.token || getUserToken(); diff --git a/src/lib/components/channel-context.svelte b/src/lib/components/channel-context.svelte new file mode 100644 index 0000000..70dbc00 --- /dev/null +++ b/src/lib/components/channel-context.svelte @@ -0,0 +1,171 @@ + + + + + + + Are you absolutely sure? + + This action cannot be undone. + + + + Cancel + deleteChannel(channel.id)}> + Continue + + + + + + addUserToChannel(channel.id, user.id)} +/> + + removeUserFromChannel(channel.id, user.id)} +/> + + + + + + + + + + {channel.name} + + handleInvite()}> + + Invite + + {#if permissions && permissions.admin} + (kickMemberDialogOpen = true)}> + + Kick member + + {/if} + {#if permissions && permissions.admin} + + (deleteChannelDialogOpen = true)}> + + Delete + + {/if} + + diff --git a/src/lib/components/notifications.svelte b/src/lib/components/notifications.svelte new file mode 100644 index 0000000..5a2166c --- /dev/null +++ b/src/lib/components/notifications.svelte @@ -0,0 +1,65 @@ + + + + +

+ +
+ + {#if notifications.length > 0} + + +
Notifications
+ {#each sortedNotifications as notification} + +
+
{notification.title}
+
{notification.body}
+
+ {new Date(notification.createdAt).toLocaleString()} +
+
+ {/each} +
+
+ {/if} + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte new file mode 100644 index 0000000..57d643b --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte new file mode 100644 index 0000000..ef0a953 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte new file mode 100644 index 0000000..256a5ff --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte new file mode 100644 index 0000000..18acce9 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte new file mode 100644 index 0000000..a235d1f --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte @@ -0,0 +1,16 @@ + + +
+ +
diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte new file mode 100644 index 0000000..2650ef9 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte @@ -0,0 +1,13 @@ + + +
+ +
diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte new file mode 100644 index 0000000..3081d75 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte new file mode 100644 index 0000000..e227219 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte new file mode 100644 index 0000000..7f98004 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/src/lib/components/ui/alert-dialog/index.ts b/src/lib/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000..be56dd7 --- /dev/null +++ b/src/lib/components/ui/alert-dialog/index.ts @@ -0,0 +1,40 @@ +import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; + +import Title from "./alert-dialog-title.svelte"; +import Action from "./alert-dialog-action.svelte"; +import Cancel from "./alert-dialog-cancel.svelte"; +import Portal from "./alert-dialog-portal.svelte"; +import Footer from "./alert-dialog-footer.svelte"; +import Header from "./alert-dialog-header.svelte"; +import Overlay from "./alert-dialog-overlay.svelte"; +import Content from "./alert-dialog-content.svelte"; +import Description from "./alert-dialog-description.svelte"; + +const Root = AlertDialogPrimitive.Root; +const Trigger = AlertDialogPrimitive.Trigger; + +export { + Root, + Title, + Action, + Cancel, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + // + Root as AlertDialog, + Title as AlertDialogTitle, + Action as AlertDialogAction, + Cancel as AlertDialogCancel, + Portal as AlertDialogPortal, + Footer as AlertDialogFooter, + Header as AlertDialogHeader, + Trigger as AlertDialogTrigger, + Overlay as AlertDialogOverlay, + Content as AlertDialogContent, + Description as AlertDialogDescription, +}; diff --git a/src/lib/components/ui/checkbox/checkbox.svelte b/src/lib/components/ui/checkbox/checkbox.svelte new file mode 100644 index 0000000..d203953 --- /dev/null +++ b/src/lib/components/ui/checkbox/checkbox.svelte @@ -0,0 +1,35 @@ + + + + + {#if isChecked} + + {:else if isIndeterminate} + + {/if} + + diff --git a/src/lib/components/ui/checkbox/index.ts b/src/lib/components/ui/checkbox/index.ts new file mode 100644 index 0000000..6d92d94 --- /dev/null +++ b/src/lib/components/ui/checkbox/index.ts @@ -0,0 +1,6 @@ +import Root from "./checkbox.svelte"; +export { + Root, + // + Root as Checkbox, +}; diff --git a/src/lib/components/ui/command/command-dialog.svelte b/src/lib/components/ui/command/command-dialog.svelte new file mode 100644 index 0000000..c6bb11a --- /dev/null +++ b/src/lib/components/ui/command/command-dialog.svelte @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/src/lib/components/ui/command/command-empty.svelte b/src/lib/components/ui/command/command-empty.svelte new file mode 100644 index 0000000..3a0819d --- /dev/null +++ b/src/lib/components/ui/command/command-empty.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/lib/components/ui/command/command-group.svelte b/src/lib/components/ui/command/command-group.svelte new file mode 100644 index 0000000..0d78a28 --- /dev/null +++ b/src/lib/components/ui/command/command-group.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/src/lib/components/ui/command/command-input.svelte b/src/lib/components/ui/command/command-input.svelte new file mode 100644 index 0000000..c9d9d38 --- /dev/null +++ b/src/lib/components/ui/command/command-input.svelte @@ -0,0 +1,23 @@ + + +
+ + +
diff --git a/src/lib/components/ui/command/command-item.svelte b/src/lib/components/ui/command/command-item.svelte new file mode 100644 index 0000000..63bbdac --- /dev/null +++ b/src/lib/components/ui/command/command-item.svelte @@ -0,0 +1,24 @@ + + + + + diff --git a/src/lib/components/ui/command/command-list.svelte b/src/lib/components/ui/command/command-list.svelte new file mode 100644 index 0000000..8ceda03 --- /dev/null +++ b/src/lib/components/ui/command/command-list.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/src/lib/components/ui/command/command-separator.svelte b/src/lib/components/ui/command/command-separator.svelte new file mode 100644 index 0000000..75caf5b --- /dev/null +++ b/src/lib/components/ui/command/command-separator.svelte @@ -0,0 +1,10 @@ + + + diff --git a/src/lib/components/ui/command/command-shortcut.svelte b/src/lib/components/ui/command/command-shortcut.svelte new file mode 100644 index 0000000..b327ccb --- /dev/null +++ b/src/lib/components/ui/command/command-shortcut.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/src/lib/components/ui/command/command.svelte b/src/lib/components/ui/command/command.svelte new file mode 100644 index 0000000..0e2ce48 --- /dev/null +++ b/src/lib/components/ui/command/command.svelte @@ -0,0 +1,22 @@ + + + + + diff --git a/src/lib/components/ui/command/index.ts b/src/lib/components/ui/command/index.ts new file mode 100644 index 0000000..d8a2e7c --- /dev/null +++ b/src/lib/components/ui/command/index.ts @@ -0,0 +1,37 @@ +import { Command as CommandPrimitive } from "cmdk-sv"; + +import Root from "./command.svelte"; +import Dialog from "./command-dialog.svelte"; +import Empty from "./command-empty.svelte"; +import Group from "./command-group.svelte"; +import Item from "./command-item.svelte"; +import Input from "./command-input.svelte"; +import List from "./command-list.svelte"; +import Separator from "./command-separator.svelte"; +import Shortcut from "./command-shortcut.svelte"; + +const Loading = CommandPrimitive.Loading; + +export { + Root, + Dialog, + Empty, + Group, + Item, + Input, + List, + Separator, + Shortcut, + Loading, + // + Root as Command, + Dialog as CommandDialog, + Empty as CommandEmpty, + Group as CommandGroup, + Item as CommandItem, + Input as CommandInput, + List as CommandList, + Separator as CommandSeparator, + Shortcut as CommandShortcut, + Loading as CommandLoading, +}; diff --git a/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte b/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte new file mode 100644 index 0000000..452d40f --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-checkbox-item.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/src/lib/components/ui/context-menu/context-menu-content.svelte b/src/lib/components/ui/context-menu/context-menu-content.svelte new file mode 100644 index 0000000..952ca50 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-content.svelte @@ -0,0 +1,24 @@ + + + + + diff --git a/src/lib/components/ui/context-menu/context-menu-item.svelte b/src/lib/components/ui/context-menu/context-menu-item.svelte new file mode 100644 index 0000000..cd91b8b --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-item.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/src/lib/components/ui/context-menu/context-menu-label.svelte b/src/lib/components/ui/context-menu/context-menu-label.svelte new file mode 100644 index 0000000..5d52f79 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-label.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/src/lib/components/ui/context-menu/context-menu-radio-group.svelte b/src/lib/components/ui/context-menu/context-menu-radio-group.svelte new file mode 100644 index 0000000..53fa692 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-radio-group.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/src/lib/components/ui/context-menu/context-menu-radio-item.svelte b/src/lib/components/ui/context-menu/context-menu-radio-item.svelte new file mode 100644 index 0000000..a0ef943 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-radio-item.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/src/lib/components/ui/context-menu/context-menu-separator.svelte b/src/lib/components/ui/context-menu/context-menu-separator.svelte new file mode 100644 index 0000000..8dc3a61 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-separator.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/lib/components/ui/context-menu/context-menu-shortcut.svelte b/src/lib/components/ui/context-menu/context-menu-shortcut.svelte new file mode 100644 index 0000000..09ab9f8 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-shortcut.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/src/lib/components/ui/context-menu/context-menu-sub-content.svelte b/src/lib/components/ui/context-menu/context-menu-sub-content.svelte new file mode 100644 index 0000000..20f7e75 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-sub-content.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte b/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte new file mode 100644 index 0000000..2a5f7f5 --- /dev/null +++ b/src/lib/components/ui/context-menu/context-menu-sub-trigger.svelte @@ -0,0 +1,32 @@ + + + + + + diff --git a/src/lib/components/ui/context-menu/index.ts b/src/lib/components/ui/context-menu/index.ts new file mode 100644 index 0000000..7d4af84 --- /dev/null +++ b/src/lib/components/ui/context-menu/index.ts @@ -0,0 +1,49 @@ +import { ContextMenu as ContextMenuPrimitive } from "bits-ui"; + +import Item from "./context-menu-item.svelte"; +import Label from "./context-menu-label.svelte"; +import Content from "./context-menu-content.svelte"; +import Shortcut from "./context-menu-shortcut.svelte"; +import RadioItem from "./context-menu-radio-item.svelte"; +import Separator from "./context-menu-separator.svelte"; +import RadioGroup from "./context-menu-radio-group.svelte"; +import SubContent from "./context-menu-sub-content.svelte"; +import SubTrigger from "./context-menu-sub-trigger.svelte"; +import CheckboxItem from "./context-menu-checkbox-item.svelte"; + +const Sub = ContextMenuPrimitive.Sub; +const Root = ContextMenuPrimitive.Root; +const Trigger = ContextMenuPrimitive.Trigger; +const Group = ContextMenuPrimitive.Group; + +export { + Sub, + Root, + Item, + Label, + Group, + Trigger, + Content, + Shortcut, + Separator, + RadioItem, + SubContent, + SubTrigger, + RadioGroup, + CheckboxItem, + // + Root as ContextMenu, + Sub as ContextMenuSub, + Item as ContextMenuItem, + Label as ContextMenuLabel, + Group as ContextMenuGroup, + Content as ContextMenuContent, + Trigger as ContextMenuTrigger, + Shortcut as ContextMenuShortcut, + RadioItem as ContextMenuRadioItem, + Separator as ContextMenuSeparator, + RadioGroup as ContextMenuRadioGroup, + SubContent as ContextMenuSubContent, + SubTrigger as ContextMenuSubTrigger, + CheckboxItem as ContextMenuCheckboxItem, +}; diff --git a/src/lib/components/ui/dialog/dialog-content.svelte b/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 0000000..9512ba8 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,36 @@ + + + + + + + + + Close + + + diff --git a/src/lib/components/ui/dialog/dialog-description.svelte b/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 0000000..e1d796a --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/src/lib/components/ui/dialog/dialog-footer.svelte b/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 0000000..a235d1f --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,16 @@ + + +
+ +
diff --git a/src/lib/components/ui/dialog/dialog-header.svelte b/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 0000000..6b4448c --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,13 @@ + + +
+ +
diff --git a/src/lib/components/ui/dialog/dialog-overlay.svelte b/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 0000000..3721361 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/ui/dialog/dialog-portal.svelte b/src/lib/components/ui/dialog/dialog-portal.svelte new file mode 100644 index 0000000..eb5d0a5 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-portal.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/src/lib/components/ui/dialog/dialog-title.svelte b/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 0000000..06574f3 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/src/lib/components/ui/dialog/index.ts b/src/lib/components/ui/dialog/index.ts new file mode 100644 index 0000000..b17ba5e --- /dev/null +++ b/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,37 @@ +import { Dialog as DialogPrimitive } from "bits-ui"; + +import Title from "./dialog-title.svelte"; +import Portal from "./dialog-portal.svelte"; +import Footer from "./dialog-footer.svelte"; +import Header from "./dialog-header.svelte"; +import Overlay from "./dialog-overlay.svelte"; +import Content from "./dialog-content.svelte"; +import Description from "./dialog-description.svelte"; + +const Root = DialogPrimitive.Root; +const Trigger = DialogPrimitive.Trigger; +const Close = DialogPrimitive.Close; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose, +}; diff --git a/src/lib/components/user-context.svelte b/src/lib/components/user-context.svelte new file mode 100644 index 0000000..1547309 --- /dev/null +++ b/src/lib/components/user-context.svelte @@ -0,0 +1,68 @@ + + + + + + + Are you absolutely sure? + + This action cannot be undone. + + + + Cancel + unfollowUser(user.id)}>Continue + + + + + + + + + + + {user.username} + + {#if isFollowing} + (unfollowDialogOpen = true)}> + + Unfollow + + {:else} + followUser(user.id)}> + + Follow + + {/if} + + diff --git a/src/lib/components/user-search.svelte b/src/lib/components/user-search.svelte new file mode 100644 index 0000000..eccba40 --- /dev/null +++ b/src/lib/components/user-search.svelte @@ -0,0 +1,68 @@ + + + + + + + + + No results found. + {#each users as user} + { + onSelect(user); + open = false; + }} + > +
+ + + {user.username[0].toUpperCase()} + + {user.username} +
+
+ {/each} +
+
+
+
diff --git a/src/lib/event.ts b/src/lib/event.ts index b39fda7..9764fd8 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -1,15 +1,17 @@ +import { onDestroy, onMount } from "svelte"; + export interface Listener { (event: D): unknown; } -export interface Disposable { +export interface ListenerDisposable { dispose(): void; } export class EventEmitter { private listeners: Map[]> = new Map(); - on = (event: E, listener: Listener): Disposable => { + on = (event: E, listener: Listener): ListenerDisposable => { if (!this.listeners.has(event)) this.listeners.set(event, []); this.listeners.get(event)?.push(listener); @@ -19,6 +21,18 @@ export class EventEmitter { }; }; + on_autoDispose = (event: E, callback: (data: D) => void) => { + let disposable: ListenerDisposable; + + onMount(() => { + disposable = this.on(event, callback); + }) + + onDestroy(() => { + disposable.dispose(); + }) + }; + off = (event: E, listener: Listener) => { if (!this.listeners.has(event)) return; @@ -33,7 +47,7 @@ export class EventEmitter { this.listeners.get(event)?.forEach((listener) => setTimeout(() => listener(data), 0)); }; - pipe = (event: E, te: EventEmitter): Disposable => { + pipe = (event: E, te: EventEmitter): ListenerDisposable => { return this.on(event, (e) => te.emit(event, e)); }; } diff --git a/src/lib/stores/cache/index.ts b/src/lib/stores/cache/index.ts index 66b1ea3..83df4c9 100644 --- a/src/lib/stores/cache/index.ts +++ b/src/lib/stores/cache/index.ts @@ -1,23 +1,49 @@ import { getChannelById } from '$lib/api/channel'; import { getMessageById } from '$lib/api/message'; -import { getUserById } from '$lib/api/user'; -import { isErrorResponse, type Channel, type Message, type User } from '$lib/types'; +import { getNotificationById } from '$lib/api/notification'; +import { getSecretById, getSecretRecipients } from '$lib/api/secret'; +import { getUserById, getUsersByChannelId, isFollowing } from '$lib/api/user'; +import { dataOrNull, type Channel, type Message, type Secret, type User, type Notification } from '$lib/types'; import { Cache } from './utils'; -export const usersCache: Cache = new Cache(async (id) => { +export const usersCache: Cache = new Cache('User', async (id) => { const response = await getUserById(id); - if (isErrorResponse(response)) return null; - return response; + return dataOrNull(response); }); -export const messagesCache: Cache = new Cache(async (id) => { +export const messagesCache: Cache = new Cache('Message', async (id) => { const response = await getMessageById(id); - if (isErrorResponse(response)) return null; - return response; + return dataOrNull(response); }); -export const channelsCache: Cache = new Cache(async (id) => { +export const channelsCache: Cache = new Cache('Channel', async (id) => { const response = await getChannelById(id); - if (isErrorResponse(response)) return null; - return response; + return dataOrNull(response); +}); + +type Following = boolean; + +export const followingUserCache: Cache = new Cache('Following', async (id) => { + const response = await isFollowing(id); + return dataOrNull(response); +}); + +export const secretCache: Cache = new Cache('Secret', async (id) => { + const response = await getSecretById(id); + return dataOrNull(response); +}); + +export const secretRecipientsCache: Cache> = new Cache('SecretRecipients', async (id) => { + const response = await getSecretRecipients(id); + return new Set(dataOrNull(response)?.map((user) => user.id)) || null; +}); + +export const notificationsCache: Cache = new Cache('Notifications', async (id) => { + const response = await getNotificationById(id); + return dataOrNull(response); +}); + +export const channelsUserCache: Cache> = new Cache('ChannelsUser', async (id) => { + const response = await getUsersByChannelId(id); + return new Set(dataOrNull(response)?.map((user) => user.id)) || null; }); diff --git a/src/lib/stores/cache/utils.ts b/src/lib/stores/cache/utils.ts index 05b52e7..ca9ef3e 100644 --- a/src/lib/stores/cache/utils.ts +++ b/src/lib/stores/cache/utils.ts @@ -1,19 +1,26 @@ +import { EventEmitter } from '$lib/event'; import { get, writable, type Writable } from 'svelte/store'; +export type CacheEvent = 'add' | 'update' | 'remove'; + export class Cache { private data: Writable> = writable(new Map()); private runningCaches: Set = new Set(); + private eventEmitter = new EventEmitter(); + + private name: string; private resolver: (data: I) => Promise; - constructor(resolver: (data: I) => Promise) { + constructor(name: string, resolver: (data: I) => Promise) { + this.name = name; this.resolver = resolver; } async get(key: I): Promise { const cached = get(this.data).get(key); if (cached) { - console.log(`[Cache] Found in cache: `, cached); + console.log(`[Cache] Found in cache ${key}/${this.name}: `, cached); return cached; } @@ -34,24 +41,56 @@ export class Cache { this.runningCaches.delete(key); - if (data) - console.log(`[Cache] Added to cache: `, data); - return data; } set(key: I, value: T) { - console.log(`[Cache] Added to cache: `, value); + const data = get(this.data); + + if (data.has(key)) { + console.log(`[Cache] Updated cache ${key}/${this.name}: `, value); + this.eventEmitter.emit('update', [key, value]); + } + else { + console.log(`[Cache] Added to cache ${key}/${this.name}: `, value); + this.eventEmitter.emit('add', [key, value]); + } this.data.update((data) => data.set(key, value)); } remove(key: I) { - console.log(`[Cache] Removed from cache: `, key); + console.log(`[Cache] Removed from cache ${key}/${this.name}: `, key); this.data.update((data) => { data.delete(key); return data; }); + + this.eventEmitter.emit('remove', [key, null]); + } + + subscribe(event: CacheEvent, callback: (data: [I, T | null]) => void) { + return this.eventEmitter.on(event, callback); + } + + subscribeKey(key: I, event: CacheEvent, callback: (data: T | null) => void) { + return this.subscribe(event, (data) => { + if (data[0] === key) callback(data[1]); + }); + } + + unsubscribe(event: CacheEvent, callback: (data: [I, T | null]) => void) { + return this.eventEmitter.off(event, callback); + } + + subscribe_autoDispose(event: CacheEvent, callback: (data: [I, T | null]) => void) { + this.eventEmitter.on_autoDispose(event, callback); + } + + subscribeKey_autoDispose(key: I, event: CacheEvent, callback: (data: T | null) => void) { + this.subscribe_autoDispose(event, (data) => { + if (data[0] === key) callback(data[1]); + }); } } diff --git a/src/lib/stores/websocket.ts b/src/lib/stores/websocket.ts index e70c08d..79ae6c1 100644 --- a/src/lib/stores/websocket.ts +++ b/src/lib/stores/websocket.ts @@ -2,26 +2,56 @@ import { BASE_API_URL } from '$lib/constants'; import { EventEmitter } from '$lib/event'; import { derived, get } from 'svelte/store'; import { token as tokenStore } from './user'; -import type { Channel, Message } from '$lib/types'; -import { messagesCache, channelsCache } from './cache'; +import type { Channel, Message, Secret, Notification } from '$lib/types'; +import { messagesCache, channelsCache, channelsUserCache, secretCache, notificationsCache, followingUserCache } from './cache'; +import { browser } from '$app/environment'; export type WebSocketMessageType = | 'createMessage' | 'updateChannel' | 'createChannel' | 'deleteChannel' + | 'addedUserToChannel' + | 'removedUserFromChannel' + | 'createSecret' + | 'updateSecret' + | 'secretRecipientAdded' + | 'secretRecipientDeleted' + | 'deleteSecret' + | 'createNotification' + | 'seenNotification' + | 'followUser' + | 'unfollowUser' + | 'connect' | 'disconnect' | 'any'; +type Id = { + id: number; +} + +type IdUser = Id & { + user_id: number; +} + +type UserIdChannelId = { + user_id: number; + channel_id: number; +} + export type WebSocketMessageData = | Message | Channel - | { id: number } + | Secret + | Notification + | Id + | IdUser | null + | UserIdChannelId | { - type: WebSocketMessageType; - }; + type: WebSocketMessageType; + }; export type WebsoketMessage = { type: WebSocketMessageType; @@ -34,7 +64,7 @@ appWebsocket.on('any', (data) => { console.log(`[WS] Recieved message: `, data); }); -function updateCache(type: WebSocketMessageType, data: WebSocketMessageData) { +async function updateCache(type: WebSocketMessageType, data: WebSocketMessageData) { switch (type) { case 'createMessage': messagesCache.set((data as Message).id, data as Message); @@ -46,7 +76,50 @@ function updateCache(type: WebSocketMessageType, data: WebSocketMessageData) { channelsCache.set((data as Channel).id, data as Channel); break; case 'deleteChannel': - channelsCache.remove((data as { id: number }).id); + channelsCache.remove((data as Id).id); + break; + case 'addedUserToChannel': { + const { user_id, channel_id } = data as UserIdChannelId; + + const channelUsers = await channelsUserCache.get(channel_id) || new Set(); + channelUsers.add(user_id); + + channelsUserCache.set(channel_id, channelUsers); + } + break; + case 'removedUserFromChannel': { + const { user_id, channel_id } = data as UserIdChannelId; + + const channelUsers = await channelsUserCache.get(channel_id) || new Set(); + channelUsers.delete(user_id); + + channelsUserCache.set(channel_id, channelUsers); + } + break; + case 'createSecret': + secretCache.set((data as Secret).id, data as Secret); + break; + case 'updateSecret': + secretCache.set((data as Secret).id, data as Secret); + break; + case 'deleteSecret': + secretCache.remove((data as Id).id); + break; + case 'createNotification': + notificationsCache.set((data as Notification).id, data as Notification); + break; + case 'seenNotification': { + const notification = await notificationsCache.get((data as Id).id); + if (notification) { + notificationsCache.set(notification.id, { ...notification, seen: true }); + } + } + break; + case 'followUser': + followingUserCache.set((data as IdUser).user_id, true); + break; + case 'unfollowUser': + followingUserCache.set((data as IdUser).user_id, false); break; default: break; @@ -54,6 +127,8 @@ function updateCache(type: WebSocketMessageType, data: WebSocketMessageData) { } const connect = (token: string) => { + if (!browser) + return null; const websocket = new WebSocket(`ws://${BASE_API_URL}/ws/${token}`); websocket.onopen = () => { diff --git a/src/lib/types.ts b/src/lib/types.ts index 3cac946..35aba94 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -6,6 +6,11 @@ export function isErrorResponse(data: unknown): data is ErrorResponse { return (data as ErrorResponse).error !== undefined; } +export function dataOrNull(data: T | ErrorResponse): T | null { + if (isErrorResponse(data)) return null; + return data; +} + export type Token = { token: string; userId: number; @@ -17,6 +22,7 @@ export type User = { id: number; username: string; avatar?: string; + lastSeen: string; createdAt: string; }; @@ -26,6 +32,7 @@ export type Message = { authorId: number; content: string; createdAt: string; + system: boolean; }; export type Channel = { @@ -34,3 +41,28 @@ export type Channel = { lastMessageId?: number; createdAt: string; }; + +export type Secret = { + id: number; + userId: number; + name: string; + content: string; + timeoutSeconds: number; + expired: boolean; + createdAt: string; +}; + +export type ChannelUserPermissions = { + userId: number; + channelId: number; + admin: boolean; +}; + +export type Notification = { + id: number; + userId: number; + title: string; + body: string; + seen: boolean; + createdAt: string; +}; \ No newline at end of file diff --git a/src/routes/(auth)/+layout.svelte b/src/routes/(auth)/+layout.svelte index b2b5a2a..a27102f 100644 --- a/src/routes/(auth)/+layout.svelte +++ b/src/routes/(auth)/+layout.svelte @@ -3,6 +3,7 @@ import { token } from '$lib/stores/user'; import { usersCache } from '$lib/stores/cache'; import { appWebsocket } from '$lib/stores/websocket'; + import { toast } from 'svelte-sonner'; export let data: LayoutData; @@ -10,6 +11,16 @@ const user = data.user; usersCache.set(user.id, user); + + appWebsocket.on_autoDispose('createNotification', (data) => { + const typedNotification = data as unknown as Notification; + + console.log('createNotification', typedNotification); + + toast.info(typedNotification.body, { + position: 'bottom-right' + }); + }); diff --git a/src/routes/(auth)/channels/(components)/(channel)/channel-area.svelte b/src/routes/(auth)/channels/(components)/(channel)/channel-area.svelte index 4fc8c14..c1f25f7 100644 --- a/src/routes/(auth)/channels/(components)/(channel)/channel-area.svelte +++ b/src/routes/(auth)/channels/(components)/(channel)/channel-area.svelte @@ -10,6 +10,7 @@ export let messages: MessageType[] = []; let messageArea: MessageArea; + let scrollToBottom = false; const sendMessage = (content: string) => { if (!channel) return; @@ -17,12 +18,19 @@ createMessage(channel.id, content); }; - export function updateMessages(newMessages: Message[]) { + export function updateMessages(newMessages: Message[], scrollToBottom: boolean = false) { messages = newMessages; + + scrollToBottom = scrollToBottom; } afterUpdate(() => { if (!messageArea) return; + if (scrollToBottom) { + messageArea.scroll('bottom', 'instant'); + scrollToBottom = false; + } + if (messageArea.getScrollPercent() > 0.95) messageArea.scroll('bottom', 'smooth'); }); @@ -41,7 +49,9 @@
- + {#key messages} + + {/key}
diff --git a/src/routes/(auth)/channels/(components)/(channel)/message.svelte b/src/routes/(auth)/channels/(components)/(channel)/message.svelte index bdeac4a..4d54255 100644 --- a/src/routes/(auth)/channels/(components)/(channel)/message.svelte +++ b/src/routes/(auth)/channels/(components)/(channel)/message.svelte @@ -3,43 +3,53 @@ import { user } from '$lib/stores/user'; import { cn } from '$lib/utils'; import * as Avatar from '$lib/components/ui/avatar'; - import { usersCache } from '$lib/stores/cache'; + import { usersCache, messagesCache } from '$lib/stores/cache'; import { writable, type Writable } from 'svelte/store'; + import { onDestroy, onMount } from 'svelte'; + import type { ListenerDisposable } from '$lib/event'; + import UserContext from '$lib/components/user-context.svelte'; export let message: Message; - let sender: Writable = writable(null); + let sender = usersCache.get(message.authorId) as Promise; - usersCache.get(message.authorId).then((user) => ($sender = user)); - - $: username = (isSelf ? $user?.username : $sender?.username) || 'N'; $: isSelf = $user?.id === message.authorId; $: color = isSelf ? 'bg-accent' : 'bg-secondary'; $: position = isSelf ? 'justify-end' : 'justify-start'; $: timestampPosition = isSelf ? 'text-right' : 'text-left'; + + function updateMessage(cachedMessage: Message | null) { + message = cachedMessage || message; + } + + messagesCache.subscribeKey_autoDispose(message.id, 'update', updateMessage); -
-
- {#if !isSelf} - - {username[0].toUpperCase()} - - {/if} +{#await sender then sender} +
+
+ {#if !isSelf} + + + {sender.username[0].toUpperCase()} + + + {/if} -
- - {message.content} - - {new Date(message.createdAt).toLocaleString()} - +
+ + {message.content} + + {new Date(message.createdAt).toLocaleString()} + +
+ {#if isSelf} + + {sender.username[0].toUpperCase()} + + {/if}
- {#if isSelf} - - {username[0].toUpperCase()} - - {/if}
-
+{/await} diff --git a/src/routes/(auth)/channels/(components)/(sidebar)/channel-list-item.svelte b/src/routes/(auth)/channels/(components)/(sidebar)/channel-list-item.svelte new file mode 100644 index 0000000..f36bd54 --- /dev/null +++ b/src/routes/(auth)/channels/(components)/(sidebar)/channel-list-item.svelte @@ -0,0 +1,78 @@ + + + + + diff --git a/src/routes/(auth)/channels/(components)/channel-list.svelte b/src/routes/(auth)/channels/(components)/(sidebar)/channel-list.svelte similarity index 83% rename from src/routes/(auth)/channels/(components)/channel-list.svelte rename to src/routes/(auth)/channels/(components)/(sidebar)/channel-list.svelte index 3fdace5..c3275da 100644 --- a/src/routes/(auth)/channels/(components)/channel-list.svelte +++ b/src/routes/(auth)/channels/(components)/(sidebar)/channel-list.svelte @@ -17,6 +17,14 @@ export function deselect() { selectedChannel.set(undefined); } + + export function getSelectedId() { + return $selectedChannel; + } + + export function updateChannels(newChannels: Channel[]) { + channels = newChannels; + }
diff --git a/src/routes/(auth)/channels/(components)/(sidebar)/profile-dropdown.svelte b/src/routes/(auth)/channels/(components)/(sidebar)/profile-dropdown.svelte new file mode 100644 index 0000000..ac9b68c --- /dev/null +++ b/src/routes/(auth)/channels/(components)/(sidebar)/profile-dropdown.svelte @@ -0,0 +1,131 @@ + + + + + followUser(user.id)} + onQueryUpdate={getQueryUsers} +/> + + + + + + + My Account + + {#each menuItems as item} + {#if item && 'name' in item} +
+ + {item.name} +
+ {:else} + + {/if} + {/each} +
+
diff --git a/src/routes/(auth)/channels/(components)/(sidebar)/sidebar-header.svelte b/src/routes/(auth)/channels/(components)/(sidebar)/sidebar-header.svelte new file mode 100644 index 0000000..97a761d --- /dev/null +++ b/src/routes/(auth)/channels/(components)/(sidebar)/sidebar-header.svelte @@ -0,0 +1,60 @@ + + +
+
+ +
+ +
+ {$user?.username} +
+ +
+ seenNotification(notification.id)} + /> +
+ +
+ +
+
diff --git a/src/routes/(auth)/channels/(components)/sidebar.svelte b/src/routes/(auth)/channels/(components)/(sidebar)/sidebar.svelte similarity index 100% rename from src/routes/(auth)/channels/(components)/sidebar.svelte rename to src/routes/(auth)/channels/(components)/(sidebar)/sidebar.svelte diff --git a/src/routes/(auth)/channels/(components)/channel-list-item.svelte b/src/routes/(auth)/channels/(components)/channel-list-item.svelte deleted file mode 100644 index 59eb304..0000000 --- a/src/routes/(auth)/channels/(components)/channel-list-item.svelte +++ /dev/null @@ -1,48 +0,0 @@ - - - diff --git a/src/routes/(auth)/channels/(components)/sidebar-header.svelte b/src/routes/(auth)/channels/(components)/sidebar-header.svelte deleted file mode 100644 index e505cee..0000000 --- a/src/routes/(auth)/channels/(components)/sidebar-header.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - - - -
- - - - - - My Account - - {#each menuItems as item} -
- - {item.name} -
- {/each} -
-
- -
- {$user?.username} -
- -
- -
-
diff --git a/src/routes/(auth)/channels/(forms)/add-user-to-channel/+page.server.ts b/src/routes/(auth)/channels/(forms)/add-user-to-channel/+page.server.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/(auth)/channels/(forms)/add-user-to-channel/add-user-to-channel-dialog.svelte b/src/routes/(auth)/channels/(forms)/add-user-to-channel/add-user-to-channel-dialog.svelte new file mode 100644 index 0000000..0b7973d --- /dev/null +++ b/src/routes/(auth)/channels/(forms)/add-user-to-channel/add-user-to-channel-dialog.svelte @@ -0,0 +1,26 @@ + + + + + + Create Channel + + { + if (data?.success) open = false; + }} + /> + + + + diff --git a/src/routes/(auth)/channels/(forms)/add-user-to-channel/add-user-to-channel-form.svelte b/src/routes/(auth)/channels/(forms)/add-user-to-channel/add-user-to-channel-form.svelte new file mode 100644 index 0000000..0820c31 --- /dev/null +++ b/src/routes/(auth)/channels/(forms)/add-user-to-channel/add-user-to-channel-form.svelte @@ -0,0 +1,41 @@ + + + + +
+ +
\ No newline at end of file diff --git a/src/routes/(auth)/channels/(forms)/create-channel/+page.server.ts b/src/routes/(auth)/channels/(forms)/create-channel/+page.server.ts new file mode 100644 index 0000000..42a1d49 --- /dev/null +++ b/src/routes/(auth)/channels/(forms)/create-channel/+page.server.ts @@ -0,0 +1,36 @@ +import type { Actions, RequestEvent } from "@sveltejs/kit"; +import { fail, superValidate } from "sveltekit-superforms"; +import { zod } from "sveltekit-superforms/adapters"; +import { isErrorResponse } from "$lib/types"; +import { createChannel } from "$lib/api/channel"; +import { createChannelFormSchema } from "./create-channel-form.svelte"; + +export const actions: Actions = { + default: async (event) => { + const token = event.cookies.get('token'); + + const result = await processCreateChannelForm(event, token); + + if (!result.valid) return fail(400, { createChannelForm: result.form }); + + return { createChannelForm: result.form, success: true }; + } +}; + +async function processCreateChannelForm(event: RequestEvent>, string | null>, token: string | undefined) { + const createChannelForm = await superValidate(event, zod(createChannelFormSchema)); + const result = { form: createChannelForm, valid: createChannelForm.valid }; + + console.log(createChannelForm.data); + + if (!createChannelForm.valid) return result; + + const response = await createChannel(createChannelForm.data.name, token); + + if (isErrorResponse(response)) { + result.form.errors.name = [response.error]; + return result; + } + + return result; +} \ No newline at end of file diff --git a/src/routes/(auth)/channels/(forms)/create-channel/create-channel-dialog.svelte b/src/routes/(auth)/channels/(forms)/create-channel/create-channel-dialog.svelte new file mode 100644 index 0000000..4617cf2 --- /dev/null +++ b/src/routes/(auth)/channels/(forms)/create-channel/create-channel-dialog.svelte @@ -0,0 +1,26 @@ + + + + + + Create Channel + + { + if (data?.success) open = false; + }} + /> + + + + diff --git a/src/routes/(auth)/channels/(forms)/create-channel/create-channel-form.svelte b/src/routes/(auth)/channels/(forms)/create-channel/create-channel-form.svelte new file mode 100644 index 0000000..57a2580 --- /dev/null +++ b/src/routes/(auth)/channels/(forms)/create-channel/create-channel-form.svelte @@ -0,0 +1,52 @@ + + + + +
+
+ + + Name + + + + +
+ Create +
+
+
diff --git a/src/routes/(auth)/channels/+layout.server.ts b/src/routes/(auth)/channels/+layout.server.ts new file mode 100644 index 0000000..89fffb2 --- /dev/null +++ b/src/routes/(auth)/channels/+layout.server.ts @@ -0,0 +1,12 @@ +import { superValidate } from 'sveltekit-superforms'; +import type { LayoutServerLoad } from './$types'; +import { zod } from 'sveltekit-superforms/adapters'; +import { createChannelFormSchema } from './(forms)/create-channel/create-channel-form.svelte'; +import { addUserToChannelFormSchema } from './(forms)/add-user-to-channel/add-user-to-channel-form.svelte'; + +export const load = (async () => { + return { + createChannelForm: await superValidate(zod(createChannelFormSchema)), + addUserToChannelForm: await superValidate(zod(addUserToChannelFormSchema)) + }; +}) satisfies LayoutServerLoad; \ No newline at end of file diff --git a/src/routes/(auth)/channels/+layout.svelte b/src/routes/(auth)/channels/+layout.svelte index 8cf3ae6..f115f69 100644 --- a/src/routes/(auth)/channels/+layout.svelte +++ b/src/routes/(auth)/channels/+layout.svelte @@ -2,17 +2,12 @@ import type { LayoutData } from './$types'; import { goto } from '$app/navigation'; import { page } from '$app/stores'; - import Sidebar from './(components)/sidebar.svelte'; - import SidebarHeader from './(components)/sidebar-header.svelte'; - import { type Icon } from 'lucide-svelte'; - import Settings from 'lucide-svelte/icons/settings'; - import LogOut from 'lucide-svelte/icons/log-out'; - import { onDestroy, onMount, type ComponentType } from 'svelte'; - import type { MenuItem } from './(components)/sidebar-header.svelte'; - import ChannelList from './(components)/channel-list.svelte'; import { appWebsocket, type WebSocketMessageType } from '$lib/stores/websocket'; import type { Channel } from '$lib/types'; + import Sidebar from './(components)/(sidebar)/sidebar.svelte'; + import SidebarHeader from './(components)/(sidebar)/sidebar-header.svelte'; + import ChannelList from './(components)/(sidebar)/channel-list.svelte'; export let data: LayoutData; @@ -21,29 +16,12 @@ let channelList: ChannelList | undefined; function onKeyUp(event: KeyboardEvent) { - if (event.key === 'Escape') { - channelList?.deselect(); - goto('/channels'); - } + // if (event.key === 'Escape') { + // channelList?.deselect(); + // goto('/channels'); + // } } - const menuItems: MenuItem[] = [ - { - name: 'Settings', - icon: Settings as ComponentType, - onClick: () => { - goto('/settings'); - } - }, - { - name: 'Logout', - icon: LogOut as ComponentType, - onClick: () => { - goto('/logout'); - } - } - ]; - $: channelId = parseInt($page.params.channel_id); function handleChannelCreated(channel: unknown) { @@ -52,19 +30,7 @@ if (!channelList) return; channels.push(typedChannel); - } - - function handleChannelUpdated(channel: unknown) { - const typedChannel = channel as Channel; - - if (!channelList) return; - - for (let i = 0; i < channels.length; i++) { - if (channels[i].id == typedChannel.id) { - channels[i] = typedChannel; - break; - } - } + channelList.updateChannels(channels); } function handleChannelDeleted(channel_id: unknown) { @@ -73,23 +39,19 @@ if (!channelList) return; channels = channels.filter((c) => c.id != id); + + if (channelId == id) goto('/channels'); + + channelList.updateChannels(channels); } const handlers = { createChannel: handleChannelCreated, - updateChannel: handleChannelUpdated, deleteChannel: handleChannelDeleted }; - onMount(() => { - for (const [key, value] of Object.entries(handlers)) - appWebsocket.on(key as WebSocketMessageType, value); - }); - - onDestroy(() => { - for (const [key, value] of Object.entries(handlers)) - appWebsocket.off(key as WebSocketMessageType, value); - }); + for (const [key, callback] of Object.entries(handlers)) + appWebsocket.on_autoDispose(key as WebSocketMessageType, callback); @@ -98,7 +60,7 @@