From cd759f104973f270b91141363721346e1c164e8f Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Wed, 14 Aug 2024 15:54:16 +0300 Subject: [PATCH 01/38] CW-firebase-messaging-for-web Added push notification config for web --- public/firebase-messaging-sw.js | 39 +++++++++ public/logo.png | Bin 0 -> 21322 bytes src/config.tsx | 8 ++ src/pages/App/App.tsx | 2 + .../NotificationsHandler.tsx | 58 +++++++++++++ .../handlers/NotificationsHandler/index.ts | 1 + src/pages/App/handlers/index.ts | 1 + src/services/Notification.ts | 78 ++++++++++++++++++ src/services/index.ts | 1 + src/shared/interfaces/Configuration.tsx | 1 + src/shared/utils/firebase.tsx | 1 + src/shared/utils/tests/mockConfig.ts | 1 + 12 files changed, 191 insertions(+) create mode 100644 public/firebase-messaging-sw.js create mode 100644 public/logo.png create mode 100644 src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx create mode 100644 src/pages/App/handlers/NotificationsHandler/index.ts create mode 100644 src/services/Notification.ts diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js new file mode 100644 index 0000000000..8d7336b2eb --- /dev/null +++ b/public/firebase-messaging-sw.js @@ -0,0 +1,39 @@ +/* eslint-disable */ +// firebase-messaging-sw.js +importScripts( + "https://www.gstatic.com/firebasejs/10.1.0/firebase-app-compat.js", +); +importScripts( + "https://www.gstatic.com/firebasejs/10.1.0/firebase-messaging-compat.js", +); + +let firebaseConfig = {}; + +self.addEventListener("message", (event) => { + if (event.data && event.data.type === "INIT_ENV") { + firebaseConfig = event.data.env; + initializeFirebase(); + } +}); + +function initializeFirebase() { + if (firebaseConfig.apiKey) { + firebase.initializeApp(firebaseConfig); + + const messaging = firebase.messaging(); + + messaging.onBackgroundMessage((payload) => { + const notificationTitle = payload.notification.title; + const notificationOptions = { + body: payload.notification.body, + data: payload.data, + icon: "/logo.png", + }; + + self.registration.showNotification( + notificationTitle, + notificationOptions, + ); + }); + } +} diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..94e4735b4d9f056089265ae2b003d2b324fbedfd GIT binary patch literal 21322 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¥(;3@vm6LFN^T8keiynulZy_ zE^!;jpr76%sOlmsQ31~K8rXA#Qyw`xXCV}Bv%8X!sB0wewTt#2`?M=Lq$cHoVudTh7f#QzO^6ZlDSY`(pZ$%=<>CjYpV6OmpuObXE_1zXO zi<#q*a=ssKKsU$$*qAj#HE-un_~hJNV8l1sBbCOzjg}9FHMKN-4BzRVFtm{I^cZs* znqnIOrA8{z!{Z5_Hm^j-=tQ|*TrWSznW zv~R}ciCP!8D;Hs9k6oLo&9W!(^gESJygP2VM#^<<737 zD@GUJ?1fOwzfQM~63VPfC%z}La2RwpJn{xZdXHn6u(j2mb>|L!^}j7tSdY6YyQf1D zKq{@b2^a9JV8395DR$StFm7fBCnlbxoAZI?lYiEX6vFv!74DT!cEscNAd4)=25n^a zv%6bMuwjyCV!jLeKPT#MpC1jI?eM{lKVqzXC5Mg0TUlptwLAZUxEz>&`XJtdASY3T zxc=>Fw+W>M6@&$r4EqEMO&-v}of8)rbHSbGY{M}JOuSq;8@-coTf{Ma%Lu2$BzcyW zbzGL%n1iVEw~I8NDFa47Ei3zCj<5^CjK(0zY4al6C}P;L*L)Y$3=qw5lS=erTb1B%mb&0d7duXVCfES@bQ=_ zk|5hSTh=%_lvZ#$vsg0y~hYM-CC=IMQQRwOTuanrsBQi)fT$&$#fU#}j=pE9UjXG7@%squ!`{kd>6DEEg@$-N*=AFo`-j_?$o$BgF*gi>AbcqtGVqhGZ|Ei; zVuXWvZ|?@k2?L2!yLs@CQcx&IM(~e%;(VSjeZDpbvsq;?3KV}&|4{;)4?*+rNlE(;Tqqd4o_Bva`iwp>G{b17pVs+u z^hVPp5mN@1Mgg9^pkK?!Zpi{1spqVz`hF{>-em$#^I%?Ne@YRnzq6DZRh> z^JNHkn1lZt!gi>tv#hkb+fTw=}&naXRU!aVg&kpUE%fB0e3!r;-R$tszLgC}$ZX%WiMz672h*q>&o77-<->G1peRh^>^fSno7<8B zU={alTTtsMOU&k|`zhZBc5=!2d)0|SJeg29?__#pkOtf$x-F6%=3(YGD zm-@$_)#_YU6_fSyS6lzU%$Y6}B(UO>dm|7Z1{jFjr>oZtZs!c4M{@jVQ2$raW?a5M zkL@TkQ7pyYp@;MHn+^OIKd|n^Lcw6$FtIC{x9Ov(^0ckp>xp?D!7W7wW2py_(11O= zhJ)MB2e<30e8!{H!C;xbe8LeuAlfmAjjSTaDA*A7}4NCpv+?4l$}ixIcf^?MN>Wey@7@#%m6*>esEq*}@;{ zwYOE+A&{jlYE`Q6XHw<~w0@oD&$8P~WB7}b=Acj9**l{ZmS2ku0j$=7>j;fW_Cy;( zD7n!2P@la$=Q2fmP*PufAz~}?n+agv=2lYTi3eyj@qyfsaoCkmr*el(U(^cEaKnY`H1%}TAO zaL$eQKq@9Tz2@XEhJ6|f#*X+#%Dx@cp=s{mfmHAG;qiF}6;XX(>`JK!^|g(o#JOIe zlj5594~B}{h7soJMt8yTDSnC3i*)1xg$nDWK}!3vYMaRy2Zsju5#J-enF0VScwK}G z6pIDAOu}`EMKj`mtqy7XhxDAjf%Hg)4?iSRdcLZ7eedMDDMa-SyGG^A$^SU=_~V|A zktj_O8dJN#k}5FqfGdIOA6~JjBTBcJyVbD(R_Ld^ue#7f)ZQtbPrvcbX2a&KQRH_7 zqdQ_gIr2m0J>?~yW$DimIik}h>F`qGzL^1xxpH3)zV^CwGxFXe6)=CQIb-)O;|_=) zGf+|*#N|B*E=YAx@b6D-rJ5_JIGJ3uJVaRm2cz)@Mm9d+WrBCc7Q>EOa;yEV3bsGpuDCcs2^H>+ZN@%?#7NCHt8JB!f%^QnZ zl603QI*fIM9#h^?swagH9B@1V^LF3Z?I2uWqqXC1Bgy6(A&lyDsYDt8ZjfeDCtSYf zb!Hk3vd#S*DJ!C$XAcGkGXT&{zkEwKo^_K>VU}iikNFt2!Tk^HAsW*d#CS!fsOEhE zlG!V=y;3@ptiPD|Vf3K1XxppdOKqV*Yft7(j}#juRIF+A*>*?QI(aD z&+q~(n6gV5_=HIBEcas2iCxqi!9rq6Frq)5PwHuqmCGd z_x$@6PZq%{nqb36F8wbAAsy33zx8v>7Qd0xGFo9EZQfSLe}}JbZm|anHsuDW9Idzm z!kTEJd;3=6zq#S;;`w@0-|)GSG*<-6p)Qs!Zc*xnwNWfOIiXXDtr^OG(&$K%z`D?rJTe-s;*pjl>Wh0-dcj2mK zE3A@xJ_NVC9Z2C06Fqe8L%+@EhibTjL$$z%55v{Zk1i)`84fB(=Ii(n!PBlU;Pqs3 z0^hZPOz*dAI)85N3^PH?4np!XlQ= z!Rqmp!e#Wq4rKPin~*y*z$fO$L=-Cp0GR3~cl9yK@?NugXvcvALew9u($+-4t$QOU zLe$wM=6dfp_QHbjx`_~%RHXM7I9(rBSYc#43ivFtc?~f8hJMPHKk1(EP8Dq47+k9o z!<1lC20%yw<+9J|Gi~>bx7sa6-17j&7=In`Ul~o4&O1TWRz7$r`m0Nmx@9)mEybt2uJ-#wwF%jTYtTh4r z_gxe_5s1A3?*9di8z?E9goaQB12q#?JM- zqac?m$#)@bdA@UakLF>OJJZM+`^7+dtISS>l@cTF6rSVn8EBdvyU43kAi~s}ds@fi zx4TMh*JrrbnghH8=7!~A%i8zSbX_~=HF@ZV^AId;C^l8@!;7f#)nS)oX4V?jqLD%B zJZ)BI>XcPnV^xjxn}C0S!K#RJ3qEhop}iKO%3rR7-{pJP3DYy*h>__0Ch8wakG9uF zDkbb}v*TfX7*ww{1t6c{S1{mJ(+drqoXOAGi=!W~?RE#qy%#+c@fl#0d;4j2r>|)A z*XRve`2iW9$;r-tSVEs^Z5;o@K2t_hW*fE=DRUa=o_&^qC9zJn12N~BYXrSYopwiI zfzlgRe&?_sMxUT@uT-MOhwa6&*;yWHdRZ2)`zlp8g9OyB?0IV)CD(5)KAKQQ2#yb` z)@oxl58pksrVA&_!?#)JMDx)+sjqu;>mIKT1Cy@)#V+~TzS~zymOqmux*PEWlEf#O zJ8TZ^2vo?`!IpdA87e&iPm2Ycl$*BbdgYsoY0XKx7r6(92++$=;5P)poUhCAtVe8AXA1@ENMd&rFGnRGJ(+LDllaC_Pn9?do{4uh}?K&70i{|X3GM}NqkF{ z7i%M+14!*d4s8IAq$D=CX{xA-(i(2s$vPael1WL1upnx+Nx-Ytt-MHvkiYmLeyjYM zzk4b#o3ZxUzZWB-i`vrh70xDA7;e%%ia}vj17_v5De`R0oV3FJWHHP)>BXl)wC?`{ z^jF#a52a{^!=5vye>YF#Up-P!O$blpf7g4sj0==Z!2O#nZwFM2d)-6FzH}vpy0}<* zqw;@soM||ee;3EcFvbisjAiWmHkmM%B>OIsrR;jpc9=U}KueD|Us{)RI~icP@8 ziy$5cS#B4Nj6w`CLbut)$2%ppW0ecL#0 zWj`GAvk)J0)%l&qBtBWl&LW=>pP!8=J3uGud*zYQ4&N7UJ!BvEEHG+59EQvs#g4uE zZvGTxCcGu2VI7^k9j;Gp-STupjKvTFsETEgAe!{CwKF&+T`-l&Q>JZl83YABEP9Bg zDMRSL(I5Gv=(X=!`bg(B==5Rbl5qw6cPRQUX#ut z<1q1ewN24S7vQBY93&^z{Z?7SXi?#Ug&rFJXD%IM^`4}|F#lYZ1Mi0cEX!M2o*;S) z-{giT#_yjR>3VW8)*ilex7pmY>q6PLPj&pMb1J`+B0B56g61E>L51z#{Q{O96i~C? z4YPXgUUEwOGgGqz${-6_2_uv20pd9eSK(i)IGkW)_c6hbZ`d>!aYvVc%y{Ku)dZ%< z3nX=6kqmFy&UryNo@Xd<6OLxYXxW3Bi#{rV&fbFtraG*P3Prv`Y4U{?CC5v@D^c5M zWSP!Z$aLIQ8S2n(eMoL&%AYa&87}X~V{XaEKhIWHa7wtW0Ze!hEuNA(#nBCasz(;X%pP_ZU^rSsf9-6IY<_awoSdD3-`5#qAwawp7D~Up2EFKTz%yQZZQg=_ zPBSzFYdU-`GTHy@)9R47eP>_0MUC&cJ4_1 z^@M!(L=aG~+*pbl`&x0W3gmCN!-@VOWIhMc{Dv{FBIhvRj^)_nJ(%idh!+yKG@ZGblP(G6*XaGT1vzPoj&6gKF zosBY{M<=`y14wxgVFS%t-FNJ*LYm?r+K|@R8h2DE(c?_RRGwfcRe8$%a*orDPS)@~ zht0`2M$1bg#^e^TVOQ^`LC-R=co?H=3mLzaBBMz3()PC4UAyrW~eiqJ|LvVpJ=dEh)B?S8Ir%4 zMdB)=oQUzM6(ZiqjA`|gb?gr_Zk5{eE>V0GDQ(XpISP#bMB={SJm&#&QIO4}@gwoX zMY+Je(8FjoF1Q2uFG1ZIvgF3f^qS_Hs!b8&n2w=V{5poU7EbMf7FkY>jS8dI`rvBNZf}uT^M`Kl56x*$XvdN# zO|LR>ncF%UQwqtzL|lq571;s|Drd+_HS>q?`-|@N7)y{&jsf?qK`hnZUlk|UbE%T9 zxf#@9mR=2S7j%Wh@BoZ2Q#&_Tx#Q!LVaSRu?R45L%vTAG<0=8msiod(+0PHS!NW|C zxuW3}YMgb3>@_0KjE}WDuzIFva;t8|tQd}kcz+EO3+D%t`NYK+kGg+l$WfjGQ16dq zqAXXJKkXY&OdtzT5^Yr4ipYY3f2RrizYsfnJZq-Z=C3;UK35w(bEJ<9wF&96oU)s` zL6fFw%a|dJQcdQ|uDgfW;hE1zflEE3nb&4Dq_W=%R0-QL98fm`Z+eC$0ec4Jwlt85 zTc{?wIM;kIXCLa0QmL8XZ09?NDj7s1+ZW!(bf-qJRQ!RDNM*o|p9xUbc$_C=0;ZyD z9h9&{4+_Gze}xGeL2NZ6E* zbGV~6KIhqI4bM2sO$@SomG)fda}h^<5KQ6dt*D!W6*9B`B570rR(MP=ACN~#Y%HvHn%ruK5dn=O!nEeXRK-44ZvIl<%<090)ao9>#XBG1Y3uv@ z!#j1C)<Z~>*Za2o22`V5_zq^LAX3A6dVbn+g_CJFo; z4yIxUJ~n9zsQenrO;k(XWOfdrKbPPqbsIytq~r%bdM2>7KW=443moWGK#Y|vQmUN} z`BQ&!vwWgA)fx)atwfNjeI8k!m}61sesaHeNlCJTt!3e8b5=T7e(M{R9Z-A8{}kLM z$aM*gvPpY@yZQt zZeiz;lI`P+612vjaCLl1wG9~Pw&Bm*wm-EstDjRh|7mwTCi>D}2?@a$mWKz7I3u-2?b&$QJ471*>yS8 zIn4*tY&vp|q$$@IP*XS)OX9aERXClQuXZ03;b@M^zK;XcIkH4Hs#+5x$JB`iiLPF& zMu7~+C>_?m3r(I9v%S;xO7<+E0p(_Lru;|Q3I3+Ay2iU=|pFso!fq_y0-95b<2HXJHK@~?LumNZT(pA zN+zQBW)!3LOADV{o3Mp7+!ZVJc<@D>Mv}qnQ|Z)i;Vhva_VA7Fln~=`Pzx6UaXRWF zw@aL-mrGoxXyB2ShVHy_WY-DX18}iX@b5lhTRV}))fit+eA1n7>-Y^zKaWVu2+z2e z=76_AUA0Tvz~|YIjo?gX;zo_pldUtmnrNAP_OT>L1FmDn)1`NQrttjWQ|`TP$(O*wZgY+L z!tdfA(IP*SQJ(j{R3D@8c{a;GB^ai88j|&JqswN1k-)<2cv`ZI!ra)jfKm?ewGFrL;5>WX`GtVDU=WGxu&bSpRsY8t3tv2 zW?{AY28Y^gi(gxTZ?cxhS=R-=EOp!8ZaV(lG`z|2P@>-e)BsQd{p@ zFZ?@It@jE93t13ChZ`s>#2ajPyOzEtoianf0#}7JENeUoM4`@#7(6AN464{PmZx2R2OZ4yD#Qu*`{=$9;LRYAMtH7b1ehugZRBh-5>{$U8%$AQ zx&w2Sk6-t1`n>x3So*wu&pv{2vaGGfP!j5mJ#s7&@A>Ur`Njsk4I;-;LFuI3<2c}~ zxW?}y$KtOobsw?%@ukt(bYIe411|hsPV}T+;c`pKZ!5vw!qL! z_j@O3CedPZ1EQNCnz5iYk37 zmRp!>gaJyt@D!_n;nud1@X*r(NwoQnaw*qDck&rWyraoOjLQ4)YEk5$w@fJLbWnE0 zoz5DEXqg(_N3tU@Y1~N0Qg>Np4>*bpK4yJ-bISO12j zIIR=ZUz`rF-`@l3nJGn44)Toog%w7E`w!%kFo6 zr{2?0p@ywH-8K<+JA1o))kC?vk4VV9a2w{lpz8tAlcb5yKXWhMYX$-}M&$308KK7- z)5*sDtWL=(yM3>`Q@l8avg&!ZZXBs_veW{g# z=YXDDRxhVOaVjoqLNU*02^D7qw?hiAvBCbFxiI1t_~%j+L7VnTQ?AxGA4PpuB$2I^%=Ez|MVM8V&@=|m3N zOSm*nJX(9DR&h2}zVyDAUo>ASh82IOZDq#kL5bk{^9Qdc zd=zJUPlMBM-qbK1eaI3;@1D5!c`oIcQ-AiZ`x<{F8UzB#hM78tUB-v`YWalt0uPWX zPE}nIr>Ur-X0M{ErJ|vQQ^DeJS~#4S*_Ai{<8b}P<*R { + diff --git a/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx new file mode 100644 index 0000000000..ba2f8c1cfc --- /dev/null +++ b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx @@ -0,0 +1,58 @@ +import { FC, useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import firebaseConfig from "@/config"; +import { selectUser } from "@/pages/Auth/store/selectors"; +import { NotificationService } from "@/services"; + +const NotificationsHandler: FC = () => { + const user = useSelector(selectUser()); + const [isRegistered, setIsRegistered] = useState(false); + const userId = user?.uid; + + useEffect(() => { + NotificationService.requestPermissions(); + }, []); + + useEffect(() => { + if ("serviceWorker" in navigator) { + const unsubscribe = window.addEventListener("load", () => { + navigator.serviceWorker + .register("/firebase-messaging-sw.js") + .then((registration) => { + console.log( + "ServiceWorker registration successful with scope: ", + registration.scope, + ); + registration.active?.postMessage({ + type: "INIT_ENV", + env: firebaseConfig.firebase, + }); + setIsRegistered(true); + return registration; + }) + .catch((err) => { + console.log("ServiceWorker registration failed: ", err); + }); + }); + + return unsubscribe; + } + }, [setIsRegistered]); + + useEffect(() => { + console.log("--userId", userId, "--isRegistered", isRegistered); + if (!userId || !isRegistered) { + return; + } + + NotificationService.saveFCMToken(); + + const unsubscribe = NotificationService.onForegroundMessage(); + + return unsubscribe; + }, [userId, isRegistered]); + + return null; +}; + +export default NotificationsHandler; diff --git a/src/pages/App/handlers/NotificationsHandler/index.ts b/src/pages/App/handlers/NotificationsHandler/index.ts new file mode 100644 index 0000000000..c7c53200f0 --- /dev/null +++ b/src/pages/App/handlers/NotificationsHandler/index.ts @@ -0,0 +1 @@ +export { default as NotificationsHandler } from "./NotificationsHandler"; \ No newline at end of file diff --git a/src/pages/App/handlers/index.ts b/src/pages/App/handlers/index.ts index 712e7a4d41..37e8eb0fb0 100644 --- a/src/pages/App/handlers/index.ts +++ b/src/pages/App/handlers/index.ts @@ -3,3 +3,4 @@ export * from "./TextDirectionHandler"; export * from "./UserNotificationsAmountHandler"; export * from "./WebViewLoginHandler"; export * from "./ThemeHandler"; +export * from "./NotificationsHandler"; diff --git a/src/services/Notification.ts b/src/services/Notification.ts new file mode 100644 index 0000000000..47e03a088e --- /dev/null +++ b/src/services/Notification.ts @@ -0,0 +1,78 @@ +import firebase from "@/shared/utils/firebase"; +import firebaseConfig from "@/config"; +import Api from "./Api"; + +enum NOTIFICATIONS_PERMISSIONS { + DEFAULT = "default", + DENIED = "denied", + GRANTED = "granted" +} + + +class NotificationService { + private endpoints: { + setFCMToken: string; + }; + + constructor() { + this.endpoints = { + setFCMToken: '/users/auth/google/set-fcm-token', + }; + } + + public requestPermissions = async (): Promise => { + try { + const permission = await Notification.requestPermission(); + if (permission === NOTIFICATIONS_PERMISSIONS.GRANTED) { + console.log('Notification permission granted.'); + return true; + } else { + return false; + } + } catch (err) { + return false; + } + } + + public saveFCMToken = async (): Promise => { + try { + console.log('-hastPermissions', firebaseConfig.vapidKey); + const token = await firebase.messaging().getToken({ vapidKey: firebaseConfig.vapidKey }); + if (token) { + try { + console.log("FCM Token:", token); + + await Api.post( + this.endpoints.setFCMToken, + { + token, + } + ); + } catch (error) { + console.error("An error occurred while retrieving token. ", error); + } + } else { + console.log("No registration token available. Request permission to generate one."); + } + } catch (error) { + console.error("An error occurred while retrieving token. ", error); + } + } + + public onForegroundMessage = () => { + return firebase.messaging().onMessage((payload) => { + console.log('Message received. ', payload); + + const { title, body } = payload.notification; + if (Notification.permission === 'granted') { + new Notification(title, { + body, + data: payload.data, + icon: '/logo.png', // Replace with your icon + }); + } + }); + } +} + +export default new NotificationService(); diff --git a/src/services/index.ts b/src/services/index.ts index 03d9bf706f..bf6d10f1ef 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -25,3 +25,4 @@ export { } from "./DiscussionMessage"; export { default as NotionService } from "./Notion"; export { default as FeatureFlagService } from "./FeatureFlag"; +export { default as NotificationService } from "./Notification"; diff --git a/src/shared/interfaces/Configuration.tsx b/src/shared/interfaces/Configuration.tsx index 94c71bcfce..73f6e96ef6 100644 --- a/src/shared/interfaces/Configuration.tsx +++ b/src/shared/interfaces/Configuration.tsx @@ -15,6 +15,7 @@ export interface Configuration { deadSeaCommonId: string; parentsForClimateCommonId: string; saadiaCommonId: string; + vapidKey: string; } export type ConfigurationObject = Record; diff --git a/src/shared/utils/firebase.tsx b/src/shared/utils/firebase.tsx index 3db4544c9a..a7f790cf01 100644 --- a/src/shared/utils/firebase.tsx +++ b/src/shared/utils/firebase.tsx @@ -1,6 +1,7 @@ import firebase from "firebase/compat/app"; import "firebase/compat/auth"; import "firebase/compat/firestore"; +import "firebase/compat/messaging"; import "firebase/compat/performance"; import "firebase/compat/storage"; import { getPerformance } from "firebase/performance"; diff --git a/src/shared/utils/tests/mockConfig.ts b/src/shared/utils/tests/mockConfig.ts index 502ef3bcfd..8f9410f5a9 100644 --- a/src/shared/utils/tests/mockConfig.ts +++ b/src/shared/utils/tests/mockConfig.ts @@ -18,5 +18,6 @@ jest.mock( deadSeaCommonId: "958dca85-7bc1-4714-95bd-1fc6343f0654", parentsForClimateCommonId: "958dca85-7bc1-4714-95bd-1fc6343f0654", saadiaCommonId: "958dca85-7bc1-4714-95bd-1fc6343f0654", + vapidKey: "VAPID_KEY", }) ); From 6b7537e27f47786c8615832ba6cea2845a85f104 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Thu, 15 Aug 2024 13:34:04 +0300 Subject: [PATCH 02/38] CW-firebase-messaging-for-web Update settings --- public/firebase-messaging-sw.js | 7 +- .../NotificationsHandler.tsx | 117 ++++++++++++------ src/services/Notification.ts | 11 +- 3 files changed, 94 insertions(+), 41 deletions(-) diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js index 8d7336b2eb..29db6ca46e 100644 --- a/public/firebase-messaging-sw.js +++ b/public/firebase-messaging-sw.js @@ -1,15 +1,16 @@ /* eslint-disable */ // firebase-messaging-sw.js importScripts( - "https://www.gstatic.com/firebasejs/10.1.0/firebase-app-compat.js", + "https://www.gstatic.com/firebasejs/10.7.1/firebase-app-compat.js", ); importScripts( - "https://www.gstatic.com/firebasejs/10.1.0/firebase-messaging-compat.js", + "https://www.gstatic.com/firebasejs/10.7.1/firebase-messaging-compat.js", ); let firebaseConfig = {}; self.addEventListener("message", (event) => { + console.log("--INIT_ENV", event); if (event.data && event.data.type === "INIT_ENV") { firebaseConfig = event.data.env; initializeFirebase(); @@ -18,11 +19,13 @@ self.addEventListener("message", (event) => { function initializeFirebase() { if (firebaseConfig.apiKey) { + console.log("--firebaseConfig", firebaseConfig); firebase.initializeApp(firebaseConfig); const messaging = firebase.messaging(); messaging.onBackgroundMessage((payload) => { + console.log("--notif-back", payload); const notificationTitle = payload.notification.title; const notificationOptions = { body: payload.notification.body, diff --git a/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx index ba2f8c1cfc..4cde6340fd 100644 --- a/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx +++ b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx @@ -1,4 +1,4 @@ -import { FC, useEffect, useState } from "react"; +import { FC, useEffect } from "react"; import { useSelector } from "react-redux"; import firebaseConfig from "@/config"; import { selectUser } from "@/pages/Auth/store/selectors"; @@ -6,51 +6,94 @@ import { NotificationService } from "@/services"; const NotificationsHandler: FC = () => { const user = useSelector(selectUser()); - const [isRegistered, setIsRegistered] = useState(false); const userId = user?.uid; - useEffect(() => { - NotificationService.requestPermissions(); - }, []); - - useEffect(() => { - if ("serviceWorker" in navigator) { - const unsubscribe = window.addEventListener("load", () => { - navigator.serviceWorker - .register("/firebase-messaging-sw.js") - .then((registration) => { - console.log( - "ServiceWorker registration successful with scope: ", - registration.scope, - ); - registration.active?.postMessage({ - type: "INIT_ENV", - env: firebaseConfig.firebase, - }); - setIsRegistered(true); - return registration; - }) - .catch((err) => { - console.log("ServiceWorker registration failed: ", err); - }); - }); - - return unsubscribe; - } - }, [setIsRegistered]); + // useEffect(() => { + // NotificationService.requestPermissions(); + // }, []); useEffect(() => { - console.log("--userId", userId, "--isRegistered", isRegistered); - if (!userId || !isRegistered) { + if (!userId) { return; } - NotificationService.saveFCMToken(); + let unsubscribeOnMessage; + let unsubscribeLoad; + (async () => { + const hasPermissions = await NotificationService.requestPermissions(); + console.log("-hasPermissions", hasPermissions); + if (hasPermissions) { + await NotificationService.saveFCMToken(); + + unsubscribeOnMessage = NotificationService.onForegroundMessage(); + + if ("serviceWorker" in navigator) { + console.log("--here"); + navigator.serviceWorker + .register("/firebase-messaging-sw.js") + .then((registration) => { + console.log( + "ServiceWorker registration successful with scope: ", + registration.scope, + ); + // registration.active?.postMessage({ + // type: "INIT_ENV", + // env: firebaseConfig.firebase, + // }); + + console.log("---registration", registration); + if (registration.active) { + registration.active.postMessage({ + type: "INIT_ENV", + env: firebaseConfig.firebase, + }); + } else if (registration?.installing) { + registration.installing.addEventListener("statechange", () => { + if (registration?.installing?.state === "activated") { + registration.installing.postMessage({ + type: "INIT_ENV", + env: firebaseConfig.firebase, + }); + } + }); + } else if (registration?.waiting) { + registration.waiting.addEventListener("statechange", () => { + if (registration?.waiting?.state === "activated") { + registration.waiting.postMessage({ + type: "INIT_ENV", + env: firebaseConfig.firebase, + }); + } + }); + } + return registration; + }) + .catch((err) => { + console.log("ServiceWorker registration failed: ", err); + }); + unsubscribeLoad = window.addEventListener("load", () => {}); + } + } + })(); + + return () => { + unsubscribeLoad && unsubscribeLoad(); + unsubscribeOnMessage && unsubscribeOnMessage(); + }; + }, [userId]); + + // useEffect(() => { + // console.log("--userId", userId, "--isRegistered", isRegistered); + // if (!userId || !isRegistered) { + // return; + // } + + // NotificationService.saveFCMToken(); - const unsubscribe = NotificationService.onForegroundMessage(); + // const unsubscribe = NotificationService.onForegroundMessage(); - return unsubscribe; - }, [userId, isRegistered]); + // return unsubscribe; + // }, [userId, isRegistered]); return null; }; diff --git a/src/services/Notification.ts b/src/services/Notification.ts index 47e03a088e..f5cf00a7a5 100644 --- a/src/services/Notification.ts +++ b/src/services/Notification.ts @@ -1,6 +1,7 @@ import firebase from "@/shared/utils/firebase"; import firebaseConfig from "@/config"; import Api from "./Api"; +import { NODATA } from "dns"; enum NOTIFICATIONS_PERMISSIONS { DEFAULT = "default", @@ -22,6 +23,9 @@ class NotificationService { public requestPermissions = async (): Promise => { try { + if(Notification.permission === NOTIFICATIONS_PERMISSIONS.GRANTED) { + return true; + } const permission = await Notification.requestPermission(); if (permission === NOTIFICATIONS_PERMISSIONS.GRANTED) { console.log('Notification permission granted.'); @@ -60,18 +64,21 @@ class NotificationService { } public onForegroundMessage = () => { - return firebase.messaging().onMessage((payload) => { + console.log('-subscribe'); + const unsubscribe = firebase.messaging().onMessage((payload) => { console.log('Message received. ', payload); const { title, body } = payload.notification; if (Notification.permission === 'granted') { new Notification(title, { body, - data: payload.data, + data: payload?.data, icon: '/logo.png', // Replace with your icon }); } }); + + return unsubscribe; } } From 92e2dfc771851b95dcdd27ebbed2c1e232cc4aad Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Thu, 15 Aug 2024 16:09:33 +0300 Subject: [PATCH 03/38] CW-firebase-messaging-for-web Updated config --- public/firebase-messaging-sw.js | 59 ++++++++++--------- .../NotificationsHandler.tsx | 8 +-- src/services/Notification.ts | 2 +- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js index 29db6ca46e..0b5d824165 100644 --- a/public/firebase-messaging-sw.js +++ b/public/firebase-messaging-sw.js @@ -7,36 +7,41 @@ importScripts( "https://www.gstatic.com/firebasejs/10.7.1/firebase-messaging-compat.js", ); -let firebaseConfig = {}; +let firebaseConfig = { + apiKey: "AIzaSyDbTFuksgOkIVWDiFe_HG7-BE8X6Dwsg-0", + authDomain: "common-dev-34b09.firebaseapp.com", + databaseURL: "https://common-dev-34b09.firebaseio.com", + projectId: "common-dev-34b09", + storageBucket: "common-dev-34b09.appspot.com", + messagingSenderId: "870639147922", + appId: "1:870639147922:web:9ee954bb1dd52e25cb7f4b", +}; -self.addEventListener("message", (event) => { - console.log("--INIT_ENV", event); - if (event.data && event.data.type === "INIT_ENV") { - firebaseConfig = event.data.env; - initializeFirebase(); - } -}); +// self.addEventListener("message", (event) => { +// console.log("--INIT_ENV", event); +// if (event.data && event.data.type === "INIT_ENV") { +// firebaseConfig = event.data.env; +// initializeFirebase(); +// } +// }); -function initializeFirebase() { - if (firebaseConfig.apiKey) { - console.log("--firebaseConfig", firebaseConfig); - firebase.initializeApp(firebaseConfig); +// function initializeFirebase() { +if (firebaseConfig.apiKey) { + console.log("--firebaseConfig", firebaseConfig); + firebase.initializeApp(firebaseConfig); - const messaging = firebase.messaging(); + const messaging = firebase.messaging(); - messaging.onBackgroundMessage((payload) => { - console.log("--notif-back", payload); - const notificationTitle = payload.notification.title; - const notificationOptions = { - body: payload.notification.body, - data: payload.data, - icon: "/logo.png", - }; + messaging.onBackgroundMessage((payload) => { + console.log("--notif-back", payload); + const notificationTitle = payload.notification.title; + const notificationOptions = { + body: payload.notification.body, + data: payload.data, + icon: "/logo.png", + }; - self.registration.showNotification( - notificationTitle, - notificationOptions, - ); - }); - } + self.registration.showNotification(notificationTitle, notificationOptions); + }); } +// } diff --git a/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx index 4cde6340fd..db19bce431 100644 --- a/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx +++ b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx @@ -76,10 +76,10 @@ const NotificationsHandler: FC = () => { } })(); - return () => { - unsubscribeLoad && unsubscribeLoad(); - unsubscribeOnMessage && unsubscribeOnMessage(); - }; + // return () => { + // unsubscribeLoad && unsubscribeLoad(); + // unsubscribeOnMessage && unsubscribeOnMessage(); + // }; }, [userId]); // useEffect(() => { diff --git a/src/services/Notification.ts b/src/services/Notification.ts index f5cf00a7a5..ad86ad403b 100644 --- a/src/services/Notification.ts +++ b/src/services/Notification.ts @@ -64,7 +64,7 @@ class NotificationService { } public onForegroundMessage = () => { - console.log('-subscribe'); + console.log('-subscribe', firebase.messaging().onMessage); const unsubscribe = firebase.messaging().onMessage((payload) => { console.log('Message received. ', payload); From a71b7cadee0dbea073162c406c14650845685068 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Fri, 16 Aug 2024 13:34:45 +0300 Subject: [PATCH 04/38] CW-firebase-messaging-for-web Fixed env issue for service worker --- public/firebase-messaging-sw.js | 105 ++++++++++++------ .../NotificationsHandler.tsx | 93 ++++------------ src/services/Notification.ts | 26 ++--- 3 files changed, 97 insertions(+), 127 deletions(-) diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js index 0b5d824165..6d0bd55e61 100644 --- a/public/firebase-messaging-sw.js +++ b/public/firebase-messaging-sw.js @@ -7,41 +7,76 @@ importScripts( "https://www.gstatic.com/firebasejs/10.7.1/firebase-messaging-compat.js", ); -let firebaseConfig = { - apiKey: "AIzaSyDbTFuksgOkIVWDiFe_HG7-BE8X6Dwsg-0", - authDomain: "common-dev-34b09.firebaseapp.com", - databaseURL: "https://common-dev-34b09.firebaseio.com", - projectId: "common-dev-34b09", - storageBucket: "common-dev-34b09.appspot.com", - messagingSenderId: "870639147922", - appId: "1:870639147922:web:9ee954bb1dd52e25cb7f4b", +const ENV = { + LOCAL: "http://localhost:3000", + DEV: "https://web-dev.common.io", + STAGE: "https://web-staging.common.io", + PRODUCTION: "https://common.io", }; -// self.addEventListener("message", (event) => { -// console.log("--INIT_ENV", event); -// if (event.data && event.data.type === "INIT_ENV") { -// firebaseConfig = event.data.env; -// initializeFirebase(); -// } -// }); - -// function initializeFirebase() { -if (firebaseConfig.apiKey) { - console.log("--firebaseConfig", firebaseConfig); - firebase.initializeApp(firebaseConfig); - - const messaging = firebase.messaging(); - - messaging.onBackgroundMessage((payload) => { - console.log("--notif-back", payload); - const notificationTitle = payload.notification.title; - const notificationOptions = { - body: payload.notification.body, - data: payload.data, - icon: "/logo.png", - }; - - self.registration.showNotification(notificationTitle, notificationOptions); - }); +const FIREBASE_CONFIG_ENV = { + DEV: { + apiKey: "AIzaSyDbTFuksgOkIVWDiFe_HG7-BE8X6Dwsg-0", + authDomain: "common-dev-34b09.firebaseapp.com", + databaseURL: "https://common-dev-34b09.firebaseio.com", + projectId: "common-dev-34b09", + storageBucket: "common-dev-34b09.appspot.com", + messagingSenderId: "870639147922", + appId: "1:870639147922:web:9ee954bb1dd52e25cb7f4b", + }, + STAGE: { + apiKey: "AIzaSyBASCWJMV64mZJObeFEitLmdUC1HqmtjJk", + authDomain: "common-staging-1d426.firebaseapp.com", + databaseURL: "https://common-staging-1d426.firebaseio.com", + projectId: "common-staging-1d426", + storageBucket: "common-staging-1d426.appspot.com", + messagingSenderId: "701579202562", + appId: "1:701579202562:web:5729d8a875f98f6709571b", + }, + PRODUCTION: { + apiKey: "AIzaSyAlYrKLd6KNKVkhmNEMKfb0cWHSWicCBOY", + authDomain: "common-production-67641.firebaseapp.com", + databaseURL: "https://common-production-67641.firebaseio.com", + projectId: "common-production-67641", + storageBucket: "common-production-67641.appspot.com", + messagingSenderId: "461029494046", + appId: "1:461029494046:web:4e2e4afbbeb7b487b48d0f", + }, +}; + +let firebaseConfig = {}; + +switch (location.origin) { + case ENV.LOCAL: + case ENV.DEV: { + firebaseConfig = FIREBASE_CONFIG_ENV.DEV; + break; + } + case ENV.STAGE: { + firebaseConfig = FIREBASE_CONFIG_ENV.STAGE; + break; + } + case ENV.PRODUCTION: { + firebaseConfig = FIREBASE_CONFIG_ENV.PRODUCTION; + break; + } + default: { + firebaseConfig = FIREBASE_CONFIG_ENV.DEV; + break; + } } -// } + +firebase.initializeApp(firebaseConfig); + +const messaging = firebase.messaging(); + +messaging.onBackgroundMessage((payload) => { + const notificationTitle = payload.notification.title; + const notificationOptions = { + body: payload.notification.body, + data: payload.data, + icon: "/logo.png", + }; + + self.registration.showNotification(notificationTitle, notificationOptions); +}); diff --git a/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx index db19bce431..885102c75c 100644 --- a/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx +++ b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx @@ -1,99 +1,46 @@ -import { FC, useEffect } from "react"; +import { FC, useEffect, useState } from "react"; import { useSelector } from "react-redux"; -import firebaseConfig from "@/config"; import { selectUser } from "@/pages/Auth/store/selectors"; import { NotificationService } from "@/services"; const NotificationsHandler: FC = () => { const user = useSelector(selectUser()); const userId = user?.uid; + const [isRegistered, setIsRegistered] = useState(false); - // useEffect(() => { - // NotificationService.requestPermissions(); - // }, []); + useEffect(() => { + if ("serviceWorker" in navigator) { + navigator.serviceWorker + .register("/firebase-messaging-sw-dev.js") + .then((registration) => { + setIsRegistered(true); + return registration; + }) + .catch((err) => { + console.log("ServiceWorker registration failed: ", err); + }); + } + }, []); useEffect(() => { - if (!userId) { + if (!userId && !isRegistered) { return; } let unsubscribeOnMessage; - let unsubscribeLoad; (async () => { const hasPermissions = await NotificationService.requestPermissions(); - console.log("-hasPermissions", hasPermissions); if (hasPermissions) { await NotificationService.saveFCMToken(); unsubscribeOnMessage = NotificationService.onForegroundMessage(); - - if ("serviceWorker" in navigator) { - console.log("--here"); - navigator.serviceWorker - .register("/firebase-messaging-sw.js") - .then((registration) => { - console.log( - "ServiceWorker registration successful with scope: ", - registration.scope, - ); - // registration.active?.postMessage({ - // type: "INIT_ENV", - // env: firebaseConfig.firebase, - // }); - - console.log("---registration", registration); - if (registration.active) { - registration.active.postMessage({ - type: "INIT_ENV", - env: firebaseConfig.firebase, - }); - } else if (registration?.installing) { - registration.installing.addEventListener("statechange", () => { - if (registration?.installing?.state === "activated") { - registration.installing.postMessage({ - type: "INIT_ENV", - env: firebaseConfig.firebase, - }); - } - }); - } else if (registration?.waiting) { - registration.waiting.addEventListener("statechange", () => { - if (registration?.waiting?.state === "activated") { - registration.waiting.postMessage({ - type: "INIT_ENV", - env: firebaseConfig.firebase, - }); - } - }); - } - return registration; - }) - .catch((err) => { - console.log("ServiceWorker registration failed: ", err); - }); - unsubscribeLoad = window.addEventListener("load", () => {}); - } } })(); - // return () => { - // unsubscribeLoad && unsubscribeLoad(); - // unsubscribeOnMessage && unsubscribeOnMessage(); - // }; - }, [userId]); - - // useEffect(() => { - // console.log("--userId", userId, "--isRegistered", isRegistered); - // if (!userId || !isRegistered) { - // return; - // } - - // NotificationService.saveFCMToken(); - - // const unsubscribe = NotificationService.onForegroundMessage(); - - // return unsubscribe; - // }, [userId, isRegistered]); + return () => { + unsubscribeOnMessage && unsubscribeOnMessage(); + }; + }, [userId, isRegistered]); return null; }; diff --git a/src/services/Notification.ts b/src/services/Notification.ts index ad86ad403b..25e7597b3c 100644 --- a/src/services/Notification.ts +++ b/src/services/Notification.ts @@ -1,7 +1,6 @@ import firebase from "@/shared/utils/firebase"; import firebaseConfig from "@/config"; import Api from "./Api"; -import { NODATA } from "dns"; enum NOTIFICATIONS_PERMISSIONS { DEFAULT = "default", @@ -28,7 +27,6 @@ class NotificationService { } const permission = await Notification.requestPermission(); if (permission === NOTIFICATIONS_PERMISSIONS.GRANTED) { - console.log('Notification permission granted.'); return true; } else { return false; @@ -40,23 +38,15 @@ class NotificationService { public saveFCMToken = async (): Promise => { try { - console.log('-hastPermissions', firebaseConfig.vapidKey); const token = await firebase.messaging().getToken({ vapidKey: firebaseConfig.vapidKey }); if (token) { - try { - console.log("FCM Token:", token); - await Api.post( - this.endpoints.setFCMToken, - { - token, - } - ); - } catch (error) { - console.error("An error occurred while retrieving token. ", error); - } - } else { - console.log("No registration token available. Request permission to generate one."); + await Api.post( + this.endpoints.setFCMToken, + { + token, + } + ); } } catch (error) { console.error("An error occurred while retrieving token. ", error); @@ -64,16 +54,14 @@ class NotificationService { } public onForegroundMessage = () => { - console.log('-subscribe', firebase.messaging().onMessage); const unsubscribe = firebase.messaging().onMessage((payload) => { - console.log('Message received. ', payload); const { title, body } = payload.notification; if (Notification.permission === 'granted') { new Notification(title, { body, data: payload?.data, - icon: '/logo.png', // Replace with your icon + icon: "/logo.png", }); } }); From 69c4a85896bafe426f9d432cb30c74c4f051990d Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Fri, 16 Aug 2024 20:07:28 +0300 Subject: [PATCH 05/38] CW-firebase-messaging-for-web Fix service-worker naming --- .../App/handlers/NotificationsHandler/NotificationsHandler.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx index 885102c75c..de144355b3 100644 --- a/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx +++ b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx @@ -11,7 +11,7 @@ const NotificationsHandler: FC = () => { useEffect(() => { if ("serviceWorker" in navigator) { navigator.serviceWorker - .register("/firebase-messaging-sw-dev.js") + .register("/firebase-messaging-sw.js") .then((registration) => { setIsRegistered(true); return registration; From e5a56d63e709dfc4d154bf31a957033ec82402f7 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Wed, 28 Aug 2024 13:33:04 +0300 Subject: [PATCH 06/38] CW-Optimistic-streams Add optimistic UI --- src/index.tsx | 3 + .../AddDiscussionComponent.tsx | 5 + .../interfaces/CreateDiscussionDto.tsx | 1 + src/pages/OldCommon/store/saga.tsx | 2 +- .../ChatComponent/ChatComponent.tsx | 6 + .../NewDiscussionCreation.tsx | 56 +++ .../DiscussionFeedCard/DiscussionFeedCard.tsx | 3 +- .../common/components/FeedItem/FeedItem.tsx | 5 + .../common/components/FeedItems/FeedItems.tsx | 38 +- .../OptimisticDiscussionFeedCard.tsx | 326 ++++++++++++++++++ .../LinkStreamModal.module.scss | 70 ++++ .../LinkStreamModal/LinkStreamModal.tsx | 113 ++++++ .../LinkStreamProjects/LinkStreamProjects.tsx | 46 +++ .../NameRightContent.module.scss | 11 + .../NameRightContent/NameRightContent.tsx | 25 ++ .../components/NameRightContent/index.ts | 1 + .../LinkStreamProjects/components/index.ts | 1 + .../components/LinkStreamProjects/index.ts | 1 + .../LinkStreamModal/components/index.ts | 1 + .../components/LinkStreamModal/index.ts | 1 + .../MoveStreamModal.module.scss | 70 ++++ .../MoveStreamModal/MoveStreamModal.tsx | 110 ++++++ .../MoveStreamProjects/MoveStreamProjects.tsx | 42 +++ .../components/MoveStreamProjects/index.ts | 1 + .../MoveStreamModal/components/index.ts | 1 + .../components/MoveStreamModal/index.ts | 1 + .../OptimisticDiscussionFeedCardContent.tsx | 109 ++++++ .../index.ts | 1 + .../components/Projects/Projects.module.scss | 71 ++++ .../components/Projects/Projects.tsx | 86 +++++ .../components/Projects/hooks/index.ts | 1 + .../Projects/hooks/useProjectsData.ts | 127 +++++++ .../components/Projects/index.ts | 2 + .../UnlinkStreamModal.module.scss | 39 +++ .../UnlinkStreamModal/UnlinkStreamModal.tsx | 116 +++++++ .../components/UnlinkStreamModal/index.ts | 1 + .../components/index.ts | 4 + .../hooks/index.ts | 1 + .../hooks/useMenuItems.tsx | 226 ++++++++++++ .../OptimisticDiscussionFeedCard/index.ts | 1 + .../utils/checkIsEditItemAllowed.ts | 13 + .../utils/checkIsLinkToAllowed.ts | 32 ++ .../utils/checkIsMoveToAllowed.ts | 28 ++ .../utils/checkIsPinUnpinAllowed.ts | 44 +++ .../utils/checkIsRemoveDiscussionAllowed.ts | 38 ++ .../utils/getAllowedItems.ts | 100 ++++++ .../utils/index.ts | 1 + src/pages/common/components/index.ts | 1 + src/pages/commonFeed/CommonFeed.tsx | 25 +- .../components/FeedLayout/FeedLayout.tsx | 2 + .../hooks/useCases/useCommonFeedItems.ts | 18 +- src/shared/models/CommonFeed.tsx | 11 + src/store/states/common/actions.ts | 23 ++ src/store/states/common/constants.ts | 6 + src/store/states/common/reducer.ts | 77 ++++- src/store/states/common/selectors.ts | 6 + src/store/states/common/types.ts | 4 +- 57 files changed, 2123 insertions(+), 31 deletions(-) create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/OptimisticDiscussionFeedCard.tsx create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.module.scss create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.tsx create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/LinkStreamProjects.tsx create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.module.scss create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.tsx create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.module.scss create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.tsx create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/MoveStreamProjects.tsx create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/OptimisticDiscussionFeedCardContent.tsx create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.module.scss create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.tsx create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/useProjectsData.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.module.scss create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.tsx create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/hooks/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/hooks/useMenuItems.tsx create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/index.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsEditItemAllowed.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsLinkToAllowed.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsMoveToAllowed.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsPinUnpinAllowed.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsRemoveDiscussionAllowed.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/utils/getAllowedItems.ts create mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/utils/index.ts diff --git a/src/index.tsx b/src/index.tsx index d5cdd816d6..e41e020189 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,8 +1,11 @@ import "./projectSetupImports"; import React from "react"; import ReactDOM from "react-dom"; +import { enableMapSet } from "immer"; import { App, AppWrapper } from "@/pages/App"; +enableMapSet(); + ReactDOM.render( diff --git a/src/pages/OldCommon/components/CommonDetailContainer/AddDiscussionComponent/AddDiscussionComponent.tsx b/src/pages/OldCommon/components/CommonDetailContainer/AddDiscussionComponent/AddDiscussionComponent.tsx index 7037f4c722..465f93f95f 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/AddDiscussionComponent/AddDiscussionComponent.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/AddDiscussionComponent/AddDiscussionComponent.tsx @@ -5,6 +5,7 @@ import classNames from "classnames"; import { Formik } from "formik"; import { omit } from "lodash"; import * as Yup from "yup"; +import { v4 as uuidv4 } from "uuid"; import { createDiscussion } from "@/pages/OldCommon/store/actions"; import { getCommonGovernanceCircles } from "@/pages/OldCommon/store/api"; import { Modal } from "@/shared/components"; @@ -109,10 +110,14 @@ const AddDiscussionComponent = ({ ); const payload = omit(values, "isLimitedDiscussion"); + + // TODO: CHECK if it needed for optimistic + const discussionId = uuidv4(); dispatch( createDiscussion.request({ payload: { ...payload, + id: discussionId, ownerId: uid, commonId: commonId, circleVisibility, diff --git a/src/pages/OldCommon/interfaces/CreateDiscussionDto.tsx b/src/pages/OldCommon/interfaces/CreateDiscussionDto.tsx index 7785ed7aa0..0c4bdd4da7 100644 --- a/src/pages/OldCommon/interfaces/CreateDiscussionDto.tsx +++ b/src/pages/OldCommon/interfaces/CreateDiscussionDto.tsx @@ -1,6 +1,7 @@ import { CommonLink } from "@/shared/models"; export interface CreateDiscussionDto { + id: string; title: string; message: string; ownerId: string; diff --git a/src/pages/OldCommon/store/saga.tsx b/src/pages/OldCommon/store/saga.tsx index feb6a14dee..3554dff7cb 100644 --- a/src/pages/OldCommon/store/saga.tsx +++ b/src/pages/OldCommon/store/saga.tsx @@ -555,7 +555,7 @@ export function* createDiscussionSaga( ); } - yield put(startLoading()); + // yield put(startLoading()); const discussion = (yield createDiscussion( action.payload.payload, diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index de1e7796fd..0e0503f2f8 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -62,6 +62,7 @@ import { selectCurrentDiscussionMessageReply, selectFilesPreview, FileInfo, + selectOptimisticFeedItems, } from "@/store/states"; import { ChatContentContext, ChatContentData } from "../CommonContent/context"; import { @@ -163,9 +164,14 @@ export default function ChatComponent({ const discussionMessageReply = useSelector( selectCurrentDiscussionMessageReply(), ); + const optimisticFeedItems = useSelector(selectOptimisticFeedItems); const user = useSelector(selectUser()); const userId = user?.uid; const discussionId = discussion?.id || ""; + + const isOptimisticChat = optimisticFeedItems.has(discussionId); + + console.log("---isOptimisticChat", isOptimisticChat); const isChatChannel = Boolean(chatChannel); const hasPermissionToHide = diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx index e32b147eb9..63098068cc 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx @@ -1,6 +1,9 @@ import React, { FC, useCallback, useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { Timestamp as FirestoreTimestamp } from "firebase/firestore"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; +import { DiscussionMessageOwnerType } from "@/shared/constants"; import { NewDiscussionCreationFormValues, UploadFile, @@ -9,13 +12,16 @@ import { Circle, CirclesPermissions, Common, + CommonFeedType, CommonMember, Governance, + OptimisticFeedItemState, } from "@/shared/models"; import { TextEditorValue, parseStringToTextEditorValue, } from "@/shared/ui-kit/TextEditor"; +import { getUserName } from "@/shared/utils"; import { selectDiscussionCreationData, selectIsDiscussionCreationLoading, @@ -116,9 +122,59 @@ const NewDiscussionCreation: FC = (props) => { }), ); } else { + const discussionId = uuidv4(); + const currentDate = FirestoreTimestamp.now(); + + const optimisticFeedItemId = uuidv4(); + dispatch( + commonActions.setOptimisticFeedItem({ + id: optimisticFeedItemId, + createdAt: currentDate, + updatedAt: currentDate, + isDeleted: false, + userId, + commonId: common.id, + data: { + type: CommonFeedType.OptimisticDiscussion, + id: discussionId, + discussionId: null, + lastMessage: { + userName: getUserName(user), + ownerId: userId, + content: JSON.stringify(values.content), + ownerType: DiscussionMessageOwnerType.User, + }, + hasFiles: false, + hasImages: false, + }, + optimisticData: { + id: discussionId, + title: values.title, + message: JSON.stringify(values.content), + ownerId: userId, + commonId: common.id, + lastMessage: currentDate, + updatedAt: currentDate, + createdAt: currentDate, + messageCount: 0, + followers: [], + files: [], + images: [], + discussionMessages: [], + isDeleted: false, + circleVisibility, + circleVisibilityByCommon: null, + linkedCommonIds: [], + state: OptimisticFeedItemState.loading, + }, + circleVisibility, + }), + ); + dispatch( commonActions.createDiscussion.request({ payload: { + id: discussionId, title: values.title, message: JSON.stringify(values.content), ownerId: userId, diff --git a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx index 5ae93d5034..21542caea6 100644 --- a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx +++ b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx @@ -248,6 +248,7 @@ const DiscussionFeedCard = forwardRef( feedItemUserMetadata?.hasUnseenMention, nestedItemData, isPreviewMode, + isActive, ]); const onDiscussionDelete = useCallback(async () => { @@ -316,7 +317,7 @@ const DiscussionFeedCard = forwardRef( if (isActive && shouldAllowChatAutoOpen !== null) { handleOpenChat(); } - }, [isActive, shouldAllowChatAutoOpen, handleOpenChat]); + }, [isActive, shouldAllowChatAutoOpen, handleOpenChat, item]); useEffect(() => { if (isActive && cardTitle) { diff --git a/src/pages/common/components/FeedItem/FeedItem.tsx b/src/pages/common/components/FeedItem/FeedItem.tsx index 366753b495..c423e5b41f 100644 --- a/src/pages/common/components/FeedItem/FeedItem.tsx +++ b/src/pages/common/components/FeedItem/FeedItem.tsx @@ -22,6 +22,7 @@ import { } from "@/shared/models"; import { checkIsItemVisibleForUser, InternalLinkData } from "@/shared/utils"; import { useFeedItemSubscription } from "../../hooks"; +import { OptimisticDiscussionFeedCard } from "../OptimisticDiscussionFeedCard"; import { DiscussionFeedCard } from "../DiscussionFeedCard"; import { ProposalFeedCard } from "../ProposalFeedCard"; import { ProjectFeedItem } from "./components"; @@ -180,6 +181,10 @@ const FeedItem = forwardRef((props, ref) => { onInternalLinkClick, }; + if (item.data.type === CommonFeedType.OptimisticDiscussion) { + return + } + if (item.data.type === CommonFeedType.Discussion) { return ; } diff --git a/src/pages/common/components/FeedItems/FeedItems.tsx b/src/pages/common/components/FeedItems/FeedItems.tsx index cf27ab1b2f..80f514cdac 100644 --- a/src/pages/common/components/FeedItems/FeedItems.tsx +++ b/src/pages/common/components/FeedItems/FeedItems.tsx @@ -70,24 +70,26 @@ const FeedItems: FC = (props) => { const isPinned = (common.pinnedFeedItems || []).some( (pinnedItem) => pinnedItem.feedObjectId === item.feedItem.id, ); - return ( - - ); + + return <>; + // return ( + // + // ); })} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/OptimisticDiscussionFeedCard.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/OptimisticDiscussionFeedCard.tsx new file mode 100644 index 0000000000..9b774f85c3 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/OptimisticDiscussionFeedCard.tsx @@ -0,0 +1,326 @@ +import React, { + forwardRef, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import { useSelector } from "react-redux"; +import { useUpdateEffect } from "react-use"; +import { debounce } from "lodash"; +import { selectUser } from "@/pages/Auth/store/selectors"; +import { InboxItemType } from "@/shared/constants"; +import { + FeedItemFollowState, + useCommon, + useFeedItemUserMetadata, + usePreloadDiscussionMessagesById, + useUserById, +} from "@/shared/hooks/useCases"; +import { FeedLayoutItemChangeData } from "@/shared/interfaces"; +import { + Common, + CommonFeed, + CommonMember, + CommonNotion, + DirectParent, + Discussion, + Governance, +} from "@/shared/models"; +import { TextEditorValue } from "@/shared/ui-kit"; +import { getUserName, InternalLinkData } from "@/shared/utils"; +import { useChatContext } from "../ChatComponent"; +import { FeedCard } from "../FeedCard"; +import { + FeedItemRef, + GetLastMessageOptions, + GetNonAllowedItemsOptions, +} from "../FeedItem"; +import { OptimisticDiscussionFeedCardContent } from "./components"; + +interface OptimisticDiscussionFeedCardProps { + item: CommonFeed; + governanceCircles?: Governance["circles"]; + isMobileVersion?: boolean; + commonId?: string; + commonName: string; + commonImage: string; + commonNotion?: CommonNotion; + pinnedFeedItems?: Common["pinnedFeedItems"]; + commonMember?: CommonMember | null; + isProject: boolean; + isPinned: boolean; + isPreviewMode: boolean; + isActive: boolean; + isExpanded: boolean; + getLastMessage: (options: GetLastMessageOptions) => TextEditorValue; + getNonAllowedItems?: GetNonAllowedItemsOptions; + onActiveItemDataChange?: (data: FeedLayoutItemChangeData) => void; + discussion?: Discussion; + directParent?: DirectParent | null; + rootCommonId?: string; + feedItemFollow: FeedItemFollowState; + shouldPreLoadMessages: boolean; + withoutMenu?: boolean; + onUserClick?: (userId: string) => void; + onFeedItemClick: (feedItemId: string) => void; + onInternalLinkClick: (data: InternalLinkData) => void; +} + +const OptimisticDiscussionFeedCard = forwardRef< + FeedItemRef, + OptimisticDiscussionFeedCardProps +>((props, ref) => { + const { + setChatItem, + feedItemIdForAutoChatOpen, + shouldAllowChatAutoOpen, + nestedItemData, + } = useChatContext(); + const { + item, + governanceCircles, + isMobileVersion = false, + commonId, + commonName, + commonImage, + commonNotion: outerCommonNotion, + isProject, + discussion, + isPreviewMode, + isActive, + isExpanded, + getLastMessage, + onActiveItemDataChange, + directParent, + feedItemFollow, + shouldPreLoadMessages, + onUserClick, + onFeedItemClick, + onInternalLinkClick, + } = props; + + const { fetchUser: fetchDiscussionCreator, data: discussionCreator } = + useUserById(); + const isHome = false; + const discussionNotion = undefined; + const { + data: feedItemUserMetadata, + fetched: isFeedItemUserMetadataFetched, + fetchFeedItemUserMetadata, + } = useFeedItemUserMetadata(); + const shouldLoadCommonData = + isHome || (discussionNotion && !outerCommonNotion); + const { data: common } = useCommon(shouldLoadCommonData ? commonId : ""); + const preloadDiscussionMessagesData = usePreloadDiscussionMessagesById({ + commonId, + discussionId: discussion?.id, + onUserClick, + onFeedItemClick, + onInternalLinkClick, + }); + const menuItems = []; + const user = useSelector(selectUser()); + const [isHovering, setHovering] = useState(false); + const onHover = (isMouseEnter: boolean): void => { + setHovering(isMouseEnter); + }; + const userId = user?.uid; + const cardTitle = discussion?.title; + const commonNotion = outerCommonNotion ?? common?.notion; + + const handleOpenChat = useCallback(() => { + if (discussion && !isPreviewMode) { + setChatItem({ + feedItemId: item.id, + discussion, + circleVisibility: item.circleVisibility, + lastSeenItem: feedItemUserMetadata?.lastSeen, + lastSeenAt: feedItemUserMetadata?.lastSeenAt, + count: feedItemUserMetadata?.count, + seenOnce: feedItemUserMetadata?.seenOnce, + seen: feedItemUserMetadata?.seen, + hasUnseenMention: feedItemUserMetadata?.hasUnseenMention, + nestedItemData: nestedItemData && { + ...nestedItemData, + feedItem: { + type: InboxItemType.FeedItemFollow, + itemId: item.id, + feedItem: item, + }, + }, + }); + } + }, [ + discussion, + item.id, + item.circleVisibility, + feedItemUserMetadata?.lastSeen, + feedItemUserMetadata?.lastSeenAt, + feedItemUserMetadata?.count, + feedItemUserMetadata?.seenOnce, + feedItemUserMetadata?.seen, + feedItemUserMetadata?.hasUnseenMention, + nestedItemData, + isPreviewMode, + ]); + + const preloadDiscussionMessages = useMemo( + () => + debounce( + (...args) => + preloadDiscussionMessagesData.preloadDiscussionMessages(...args), + 6000, + ), + [preloadDiscussionMessagesData.preloadDiscussionMessages], + ); + + useEffect(() => { + fetchDiscussionCreator(item.userId); + }, [item.userId]); + + useEffect(() => { + if (commonId) { + fetchFeedItemUserMetadata({ + userId: userId || "", + commonId, + feedObjectId: item.id, + }); + } + }, [userId, commonId, item.id]); + + useEffect(() => { + if ( + (!isActive || + shouldAllowChatAutoOpen === null || + shouldAllowChatAutoOpen) && + isFeedItemUserMetadataFetched && + item.id === feedItemIdForAutoChatOpen && + !isMobileVersion + ) { + handleOpenChat(); + } + }, [isFeedItemUserMetadataFetched, shouldAllowChatAutoOpen]); + + useEffect(() => { + if (isActive && shouldAllowChatAutoOpen !== null) { + handleOpenChat(); + } + }, [isActive, shouldAllowChatAutoOpen, handleOpenChat]); + + useEffect(() => { + if (isActive && cardTitle) { + onActiveItemDataChange?.({ + itemId: item.id, + title: cardTitle, + }); + } + }, [isActive, cardTitle]); + + useEffect(() => { + if ( + shouldPreLoadMessages && + !isActive && + commonId && + item.circleVisibility + ) { + preloadDiscussionMessages(item.circleVisibility); + } + }, [shouldPreLoadMessages, isActive]); + + useUpdateEffect(() => { + if ( + shouldPreLoadMessages && + !isActive && + commonId && + item.circleVisibility + ) { + preloadDiscussionMessages(item.circleVisibility, true); + } + }, [item.data.lastMessage?.content]); + + const lastMessage = useMemo(() => { + return getLastMessage({ + commonFeedType: item.data.type, + lastMessage: item.data.lastMessage, + discussion, + currentUserId: userId, + feedItemCreatorName: getUserName(discussionCreator), + commonName, + isProject, + hasFiles: item.data.hasFiles, + hasImages: item.data.hasImages, + }); + }, [ + item.data.type, + item.data.lastMessage, + discussion, + userId, + discussionCreator, + commonName, + isProject, + item.data.hasFiles, + item.data.hasImages, + ]); + + return ( + <> + + {(isExpanded || isActive) && ( + onUserClick(item.userId))} + discussionCreator={discussionCreator} + isHome={isHome} + menuItems={menuItems} + discussionImages={discussion?.images ?? []} + discussionMessage={discussion?.message} + common={common} + discussionNotion={discussionNotion} + handleOpenChat={handleOpenChat} + onHover={onHover} + isLoading={false} + /> + )} + + + ); +}); + +export default OptimisticDiscussionFeedCard; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.module.scss b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.module.scss new file mode 100644 index 0000000000..b99c0e1f41 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.module.scss @@ -0,0 +1,70 @@ +@import "../../../../../../constants"; +@import "../../../../../../styles/sizes"; + +.modal { + max-width: 31.875rem; + width: 100%; + max-height: 33.75rem; + min-height: 24rem; + border-radius: 0; + box-shadow: 0 0.25rem 0.9375rem var(--drop-shadow); + + :global(.modal__header-wrapper--with-modal-padding) { + .modalHeader { + justify-content: flex-start; + } + + .modalTitle { + margin: 0; + font-family: PoppinsSans, sans-serif; + font-weight: 600; + font-size: 1.25rem; + color: var(--primary-text); + text-align: left; + word-break: break-word; + } + } + + .modalContent { + width: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + box-sizing: border-box; + } + + .modalCloseWrapper { + top: 1.7rem; + margin: 0; + + @include tablet { + top: 1.1rem; + } + } + + @include tablet { + max-width: unset; + max-height: unset; + } +} + +.submitButtonWrapper { + margin-top: auto; + padding-top: 1.5rem; + display: flex; + justify-content: flex-end; +} + +.submitButton { + --btn-w: 100%; + + max-width: 9.75rem; + + @include tablet { + max-width: 100%; + } +} + +.loader { + margin: 0 auto; +} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.tsx new file mode 100644 index 0000000000..b3d56b1a68 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.tsx @@ -0,0 +1,113 @@ +import React, { FC, ReactElement, useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { selectUser } from "@/pages/Auth/store/selectors"; +import { Modal } from "@/shared/components"; +import { useNotification } from "@/shared/hooks"; +import { useStreamLinking } from "@/shared/hooks/useCases"; +import { Button, ButtonVariant, Loader } from "@/shared/ui-kit"; +import { emptyFunction } from "@/shared/utils"; +import { LinkStreamProjects } from "./components"; +import styles from "./LinkStreamModal.module.scss"; + +interface LinkStreamModalProps { + isOpen: boolean; + onClose: () => void; + feedItemId: string; + title: string; + rootCommonId: string; + commonId: string; + originalCommonId: string; + linkedCommonIds?: string[]; + circleVisibility: string[]; +} + +const LinkStreamModal: FC = (props) => { + const { + isOpen, + onClose, + feedItemId, + title, + rootCommonId, + commonId, + originalCommonId, + linkedCommonIds = [], + circleVisibility, + } = props; + const { notify } = useNotification(); + const { isStreamLinking, isStreamLinked, linkStream } = useStreamLinking(); + const [activeItemId, setActiveItemId] = useState(""); + const user = useSelector(selectUser()); + const userId = user?.uid; + + const handleSubmit = () => { + if (!userId) { + return; + } + + linkStream({ + userId, + feedObjectId: feedItemId, + sourceCommonId: commonId, + targetCommonId: activeItemId, + }); + }; + + const renderContent = (): ReactElement => { + if (isStreamLinking) { + return ; + } + + return ( + <> + +
+ +
+ + ); + }; + + useEffect(() => { + if (isStreamLinked) { + notify("Stream is successfully linked"); + onClose(); + } + }, [isStreamLinking, isStreamLinked]); + + return ( + + {renderContent()} + + ); +}; + +export default LinkStreamModal; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/LinkStreamProjects.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/LinkStreamProjects.tsx new file mode 100644 index 0000000000..bdc8828423 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/LinkStreamProjects.tsx @@ -0,0 +1,46 @@ +import React, { FC, useCallback } from "react"; +import { Item } from "@/shared/layouts/SidenavLayout/components/SidenavContent/components"; +import { ProjectsStateItem } from "@/store/states"; +import { Projects, ProjectsProps } from "../../../Projects"; +import { NameRightContent } from "./components"; + +interface LinkStreamProjects + extends Omit { + originalCommonId: string; + linkedCommonIds: string[]; + circleVisibility: string[]; +} + +const LinkStreamProjects: FC = (props) => { + const { rootCommonId, originalCommonId, linkedCommonIds, circleVisibility } = + props; + + const getAdditionalItemData = useCallback( + (projectsStateItem: ProjectsStateItem): Partial => { + const isAllowedToLink = + circleVisibility.length === 0 || + projectsStateItem.commonId === rootCommonId || + projectsStateItem.rootCommonId === rootCommonId; + + return { + disabled: + !isAllowedToLink || + !projectsStateItem.hasPermissionToLinkToHere || + projectsStateItem.commonId === originalCommonId || + linkedCommonIds.includes(projectsStateItem.commonId), + nameRightContent: ( + + ), + }; + }, + [originalCommonId, linkedCommonIds, rootCommonId, circleVisibility], + ); + + return ; +}; + +export default LinkStreamProjects; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.module.scss b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.module.scss new file mode 100644 index 0000000000..e54f537a46 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.module.scss @@ -0,0 +1,11 @@ +.originalText { + margin-left: 0.5rem; + font-weight: normal; + font-size: 0.875rem; + font-style: italic; +} + +.linkIcon { + flex-shrink: 0; + margin-left: 0.5rem; +} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.tsx new file mode 100644 index 0000000000..9d834a9512 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.tsx @@ -0,0 +1,25 @@ +import React, { FC } from "react"; +import { Link4Icon } from "@/shared/icons"; +import { ProjectsStateItem } from "@/store/states"; +import styles from "./NameRightContent.module.scss"; + +interface NameRightContentProps { + projectsStateItem: ProjectsStateItem; + originalCommonId: string; + linkedCommonIds?: string[]; +} + +const NameRightContent: FC = (props) => { + const { projectsStateItem, originalCommonId, linkedCommonIds = [] } = props; + + if (projectsStateItem.commonId === originalCommonId) { + return original; + } + if (linkedCommonIds.includes(projectsStateItem.commonId)) { + return ; + } + + return null; +}; + +export default NameRightContent; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/index.ts new file mode 100644 index 0000000000..3c341bfce7 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/index.ts @@ -0,0 +1 @@ +export { default as NameRightContent } from "./NameRightContent"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/index.ts new file mode 100644 index 0000000000..f5705eab2d --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/index.ts @@ -0,0 +1 @@ +export * from "./NameRightContent"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/index.ts new file mode 100644 index 0000000000..f6931ce7fb --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/index.ts @@ -0,0 +1 @@ +export { default as LinkStreamProjects } from "./LinkStreamProjects"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/index.ts new file mode 100644 index 0000000000..159661f715 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/index.ts @@ -0,0 +1 @@ +export * from "./LinkStreamProjects"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/index.ts new file mode 100644 index 0000000000..79c9f12e5d --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/index.ts @@ -0,0 +1 @@ +export { default as LinkStreamModal } from "./LinkStreamModal"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.module.scss b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.module.scss new file mode 100644 index 0000000000..b99c0e1f41 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.module.scss @@ -0,0 +1,70 @@ +@import "../../../../../../constants"; +@import "../../../../../../styles/sizes"; + +.modal { + max-width: 31.875rem; + width: 100%; + max-height: 33.75rem; + min-height: 24rem; + border-radius: 0; + box-shadow: 0 0.25rem 0.9375rem var(--drop-shadow); + + :global(.modal__header-wrapper--with-modal-padding) { + .modalHeader { + justify-content: flex-start; + } + + .modalTitle { + margin: 0; + font-family: PoppinsSans, sans-serif; + font-weight: 600; + font-size: 1.25rem; + color: var(--primary-text); + text-align: left; + word-break: break-word; + } + } + + .modalContent { + width: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + box-sizing: border-box; + } + + .modalCloseWrapper { + top: 1.7rem; + margin: 0; + + @include tablet { + top: 1.1rem; + } + } + + @include tablet { + max-width: unset; + max-height: unset; + } +} + +.submitButtonWrapper { + margin-top: auto; + padding-top: 1.5rem; + display: flex; + justify-content: flex-end; +} + +.submitButton { + --btn-w: 100%; + + max-width: 9.75rem; + + @include tablet { + max-width: 100%; + } +} + +.loader { + margin: 0 auto; +} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.tsx new file mode 100644 index 0000000000..46dc541967 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.tsx @@ -0,0 +1,110 @@ +import React, { FC, ReactElement, useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { selectUser } from "@/pages/Auth/store/selectors"; +import { Modal } from "@/shared/components"; +import { useNotification } from "@/shared/hooks"; +import { useStreamMoving } from "@/shared/hooks/useCases"; +import { Button, ButtonVariant, Loader } from "@/shared/ui-kit"; +import { emptyFunction } from "@/shared/utils"; +import { MoveStreamProjects } from "./components"; +import styles from "./MoveStreamModal.module.scss"; + +interface MoveStreamModalProps { + isOpen: boolean; + onClose: () => void; + feedItemId: string; + title: string; + rootCommonId: string; + commonId: string; + originalCommonId: string; + circleVisibility: string[]; +} + +const MoveStreamModal: FC = (props) => { + const { + isOpen, + onClose, + feedItemId, + title, + rootCommonId, + commonId, + originalCommonId, + circleVisibility, + } = props; + const { notify } = useNotification(); + const { isStreamMoving, isStreamMoved, moveStream } = useStreamMoving(); + const [activeItemId, setActiveItemId] = useState(""); + const user = useSelector(selectUser()); + const userId = user?.uid; + + const handleSubmit = () => { + if (!userId) { + return; + } + + moveStream({ + userId, + feedObjectId: feedItemId, + sourceCommonId: commonId, + targetCommonId: activeItemId, + }); + }; + + const renderContent = (): ReactElement => { + if (isStreamMoving) { + return ; + } + + return ( + <> + +
+ +
+ + ); + }; + + useEffect(() => { + if (isStreamMoved) { + notify("Stream is successfully moved"); + onClose(); + } + }, [isStreamMoving, isStreamMoved]); + + return ( + + {renderContent()} + + ); +}; + +export default MoveStreamModal; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/MoveStreamProjects.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/MoveStreamProjects.tsx new file mode 100644 index 0000000000..78e25f4148 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/MoveStreamProjects.tsx @@ -0,0 +1,42 @@ +import React, { FC, useCallback } from "react"; +import { Item } from "@/shared/layouts/SidenavLayout/components/SidenavContent/components"; +import { ProjectsStateItem } from "@/store/states"; +import { NameRightContent } from "../../../LinkStreamModal/components/LinkStreamProjects/components"; +import { Projects, ProjectsProps } from "../../../Projects"; + +interface MoveStreamProjectsProps + extends Omit { + originalCommonId: string; + circleVisibility: string[]; +} + +const MoveStreamProjects: FC = (props) => { + const { rootCommonId, originalCommonId, circleVisibility } = props; + + const getAdditionalItemData = useCallback( + (projectsStateItem: ProjectsStateItem): Partial => { + const isAllowedToMove = + circleVisibility.length === 0 || + projectsStateItem.commonId === rootCommonId || + projectsStateItem.rootCommonId === rootCommonId; + + return { + disabled: + !isAllowedToMove || + !projectsStateItem.hasPermissionToMoveToHere || + projectsStateItem.commonId === originalCommonId, + nameRightContent: ( + + ), + }; + }, + [originalCommonId, rootCommonId, circleVisibility], + ); + + return ; +}; + +export default MoveStreamProjects; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/index.ts new file mode 100644 index 0000000000..cd9ba5fc65 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/index.ts @@ -0,0 +1 @@ +export { default as MoveStreamProjects } from "./MoveStreamProjects"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/index.ts new file mode 100644 index 0000000000..35fef4926d --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/index.ts @@ -0,0 +1 @@ +export * from "./MoveStreamProjects"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/index.ts new file mode 100644 index 0000000000..219e936dd5 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/index.ts @@ -0,0 +1 @@ +export { default as MoveStreamModal } from "./MoveStreamModal"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/OptimisticDiscussionFeedCardContent.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/OptimisticDiscussionFeedCardContent.tsx new file mode 100644 index 0000000000..8bab55824e --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/OptimisticDiscussionFeedCardContent.tsx @@ -0,0 +1,109 @@ +import React from "react"; +import { ContextMenuItem } from "@/shared/interfaces"; +import { + Common, + CommonFeed, + DirectParent, + DiscussionNotion, + Governance, + Link, + User, +} from "@/shared/models"; +import { getUserName } from "@/shared/utils"; +import { + FeedCardContent, + FeedCardHeader, + FeedCountdown, + getVisibilityString, +} from "../../../FeedCard"; + +interface OptimisticDiscussionFeedCardContentProps { + item: CommonFeed; + governanceCircles?: Governance["circles"]; + isMobileVersion?: boolean; + commonId?: string; + directParent?: DirectParent | null; + onUserSelect?: (userId: string, commonId?: string) => void; + discussionCreator: User | null; + isHome: boolean; + menuItems: ContextMenuItem[]; + discussionMessage?: string; + discussionImages: Link[]; + common: Common | null; + discussionNotion?: DiscussionNotion; + handleOpenChat: () => void; + onHover: (isMouseEnter: boolean) => void; + isLoading: boolean; +} + +export function OptimisticDiscussionFeedCardContent( + props: OptimisticDiscussionFeedCardContentProps, +) { + const { + item, + governanceCircles, + isMobileVersion = false, + commonId, + directParent, + onUserSelect, + discussionCreator, + isHome, + menuItems, + common, + discussionNotion, + handleOpenChat, + onHover, + isLoading, + discussionMessage, + discussionImages, + } = props; + + if (isLoading || !commonId) { + return null; + } + + const circleVisibility = governanceCircles + ? getVisibilityString(governanceCircles, item?.circleVisibility) + : undefined; + + return ( + <> + + Created:{" "} + + + } + type={isHome ? "Home" : "Discussion"} + circleVisibility={circleVisibility} + menuItems={menuItems} + isMobileVersion={isMobileVersion} + commonId={commonId} + userId={item.userId} + directParent={directParent} + onUserSelect={ + onUserSelect && (() => onUserSelect(item.userId, commonId)) + } + /> + { + onHover(true); + }} + onMouseLeave={() => { + onHover(false); + }} + /> + + ); +} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/index.ts new file mode 100644 index 0000000000..c38bf83825 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/index.ts @@ -0,0 +1 @@ +export * from "./OptimisticDiscussionFeedCardContent"; \ No newline at end of file diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.module.scss b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.module.scss new file mode 100644 index 0000000000..9776f0251b --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.module.scss @@ -0,0 +1,71 @@ +@import "../../../../../../constants"; +@import "../../../../../../styles/sizes"; + +.projectsTree { + overflow-y: auto; + box-sizing: border-box; +} + +.projectsTreeItemTriggerClassName { + --item-pl-per-level: 1.25rem; + --item-arrow-pl: 0.5rem; + + height: 3rem; + border-radius: 0; + + &:hover { + --bg-color: var(--hover-fill); + --item-text-color: var(--primary-text); + } + + @media (hover: none) { + &:hover { + --bg-color: var(--primary-background); + --item-text-color: var(--primary-text); + } + } +} +.projectsTreeItemTriggerActiveClassName { + --bg-color: var(--primary-fill); + --item-text-color: #{$c-shades-white}; + + &:hover { + --bg-color: var(--primary-fill); + } + + @media (hover: none) { + &:hover { + --bg-color: var(--primary-fill); + --item-text-color: #{$c-shades-white}; + } + } +} + +.projectsTreeItemTriggerNameClassName { + font-family: PoppinsSans, sans-serif; + font-weight: 500; +} + +.projectsTreeItemTriggerImageClassName { + width: 1.5rem; + height: 1.5rem; + margin-right: 0.875rem; +} +.projectsTreeItemTriggerImageNonRoundedClassName { + border-radius: 0.375rem; +} + +.loader { + margin: 1rem auto 0; + display: block; +} + +.createCommonButton { + width: 100%; + padding-left: 2.125rem; + padding-right: 0.875rem; +} + +.commonsMenuClassName { + max-height: 15rem; +} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.tsx new file mode 100644 index 0000000000..ef60d43e99 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.tsx @@ -0,0 +1,86 @@ +import React, { FC, ReactNode, useEffect, useMemo, useState } from "react"; +import { LOADER_APPEARANCE_DELAY } from "@/shared/constants"; +import { TreeItemTriggerStyles } from "@/shared/layouts"; +import { ProjectsTree } from "@/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/ProjectsTree"; +import { Item } from "@/shared/layouts/SidenavLayout/components/SidenavContent/components"; +import { Loader } from "@/shared/ui-kit"; +import { ProjectsStateItem } from "@/store/states"; +import { useProjectsData } from "./hooks"; +import styles from "./Projects.module.scss"; + +export interface ProjectsProps { + rootCommonId: string; + commonId: string; + activeItemId: string; + onActiveItemId: (activeItemId: string) => void; + renderNoItemsInfo?: () => ReactNode; + getAdditionalItemData?: ( + projectsStateItem: ProjectsStateItem, + ) => Partial; +} + +const Projects: FC = (props) => { + const { + activeItemId, + onActiveItemId, + renderNoItemsInfo, + getAdditionalItemData, + } = props; + const [currentCommonId, setCurrentCommonId] = useState(props.rootCommonId); + const { + parentItem, + areCommonsLoading, + areProjectsLoading, + commons, + items, + activeItem, + parentItemIds, + } = useProjectsData({ + currentCommonId, + activeItemId, + getAdditionalItemData, + }); + const treeItemTriggerStyles = useMemo( + () => ({ + container: styles.projectsTreeItemTriggerClassName, + containerActive: styles.projectsTreeItemTriggerActiveClassName, + name: styles.projectsTreeItemTriggerNameClassName, + image: styles.projectsTreeItemTriggerImageClassName, + imageNonRounded: styles.projectsTreeItemTriggerImageNonRoundedClassName, + }), + [], + ); + + useEffect(() => { + onActiveItemId(""); + }, [currentCommonId]); + + if (!parentItem) { + return areCommonsLoading ? ( + + ) : ( + <>{renderNoItemsInfo?.() || null} + ); + } + + return ( + + ); +}; + +export default Projects; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/index.ts new file mode 100644 index 0000000000..ced5d7bb9c --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useProjectsData"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/useProjectsData.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/useProjectsData.ts new file mode 100644 index 0000000000..1bb8ed7c8e --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/useProjectsData.ts @@ -0,0 +1,127 @@ +import { useEffect, useMemo, useRef } from "react"; +import { useSelector } from "react-redux"; +import { selectUser } from "@/pages/Auth/store/selectors"; +import { useLoadingState } from "@/shared/hooks"; +import { + generateProjectsTreeItems, + getItemById, + getItemFromProjectsStateItem, + getParentItemIds, + Item, +} from "@/shared/layouts/SidenavLayout/components/SidenavContent/components"; +import { + ProjectsStateItem, + selectCommonLayoutCommonsState, +} from "@/store/states"; +import { getProjects as getProjectsUtil } from "@/store/states/commonLayout/saga/utils"; + +interface ProjectsInfo { + currentCommonId: string; + activeItemId: string; + getAdditionalItemData?: ( + projectsStateItem: ProjectsStateItem, + ) => Partial; +} + +interface Return { + parentItem: Item | null; + areCommonsLoading: boolean; + areProjectsLoading: boolean; + commons: ProjectsStateItem[]; + items: Item[]; + activeItem: Item | null; + parentItemIds: string[]; +} + +const generateItemCommonPagePath = () => ""; + +export const useProjectsData = (projectsInfo: ProjectsInfo): Return => { + const { currentCommonId, activeItemId, getAdditionalItemData } = projectsInfo; + const currentCommonIdRef = useRef(currentCommonId); + currentCommonIdRef.current = currentCommonId; + const { commons, areCommonsLoading } = useSelector( + selectCommonLayoutCommonsState, + ); + const user = useSelector(selectUser()); + const userId = user?.uid; + const [{ data: projects, loading: areProjectsLoading }, setProjectsState] = + useLoadingState([]); + const currentCommon = commons.find( + ({ commonId }) => commonId === currentCommonId, + ); + + const parentItem = useMemo( + () => + currentCommon + ? getItemFromProjectsStateItem( + currentCommon, + generateItemCommonPagePath, + undefined, + getAdditionalItemData, + ) + : null, + [currentCommon, getAdditionalItemData], + ); + const items = useMemo(() => { + const [item] = generateProjectsTreeItems( + currentCommon ? projects.concat(currentCommon) : projects, + generateItemCommonPagePath, + getAdditionalItemData, + ); + + return item?.items || []; + }, [currentCommon, projects, getAdditionalItemData]); + const activeItem = getItemById( + activeItemId, + parentItem ? [parentItem, ...items] : items, + ); + const parentItemIds = getParentItemIds( + activeItemId, + currentCommon ? projects.concat(currentCommon) : projects, + ); + + useEffect(() => { + let isRelevantLoading = true; + + (async () => { + try { + setProjectsState({ + data: [], + loading: true, + fetched: false, + }); + const projectsData = await getProjectsUtil(currentCommonId, userId); + + if (isRelevantLoading) { + setProjectsState({ + data: projectsData, + loading: false, + fetched: true, + }); + } + } catch (err) { + if (isRelevantLoading) { + setProjectsState({ + data: [], + loading: false, + fetched: true, + }); + } + } + })(); + + return () => { + isRelevantLoading = false; + }; + }, [currentCommonId]); + + return { + parentItem, + areCommonsLoading, + areProjectsLoading, + commons, + items, + activeItem, + parentItemIds, + }; +}; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/index.ts new file mode 100644 index 0000000000..6ca2075a54 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/index.ts @@ -0,0 +1,2 @@ +export { default as Projects } from "./Projects"; +export type { ProjectsProps } from "./Projects"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.module.scss b/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.module.scss new file mode 100644 index 0000000000..3cf05ba9e8 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.module.scss @@ -0,0 +1,39 @@ +@import "../../../../../../constants"; +@import "../../../../../../styles/sizes"; + +.modal { + max-width: 31.875rem; +} + +.buttonsWrapper { + display: flex; + margin-top: 1rem; + justify-content: flex-end; + + @include tablet { + flex-direction: column; + margin-top: 100%; + } +} + +.button { + width: 10rem; + + @include tablet { + width: 100%; + } +} + +.unlinkButton { + margin-left: 1.5rem; + + @include tablet { + margin-left: unset; + margin-top: 1rem; + } +} + +.errorText { + font-size: $xsmall; + color: $red; +} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.tsx new file mode 100644 index 0000000000..76f0886970 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.tsx @@ -0,0 +1,116 @@ +import React, { FC, useRef, useState } from "react"; +import { useSelector } from "react-redux"; +import { CancelTokenSource } from "axios"; +import classNames from "classnames"; +import { isError } from "lodash"; +import { selectUser } from "@/pages/Auth/store/selectors"; +import { + CommonFeedService, + Logger, + getCancelTokenSource, + isRequestCancelled, +} from "@/services"; +import { Modal } from "@/shared/components"; +import { useNotification } from "@/shared/hooks"; +import { Button, ButtonVariant } from "@/shared/ui-kit"; +import { emptyFunction } from "@/shared/utils"; +import styles from "./UnlinkStreamModal.module.scss"; + +interface UnlinkStreamModalProps { + isOpen: boolean; + onClose: () => void; + feedItemId: string; + title: string; + commonId: string; + commonName: string; +} + +const UnlinkStreamModal: FC = (props) => { + const { isOpen, onClose, feedItemId, title, commonId, commonName } = props; + const { notify } = useNotification(); + const [isUnlinking, setIsUnlinking] = useState(false); + const [errorText, setErrorText] = useState(""); + const cancelTokenRef = useRef(null); + const user = useSelector(selectUser()); + const userId = user?.uid; + + const handleSubmit = async () => { + if (!userId) { + return; + } + + setErrorText(""); + setIsUnlinking(true); + + try { + if (cancelTokenRef.current) { + cancelTokenRef.current.cancel(); + } + + cancelTokenRef.current = getCancelTokenSource(); + + await CommonFeedService.unlinkStream( + { + feedObjectId: feedItemId, + commonId: commonId, + userId: userId, + }, + { + cancelToken: cancelTokenRef.current.token, + }, + ); + + cancelTokenRef.current = null; + + notify("Stream is successfully unlinked"); + setIsUnlinking(false); + onClose(); + } catch (error) { + if (!isRequestCancelled(error)) { + Logger.error(error); + cancelTokenRef.current = null; + } + setIsUnlinking(false); + setErrorText(isError(error) ? error.message : "Something went wrong..."); + } + }; + + return ( + +
+ This stream is linked in multiple spaces.
+ If you proceed, it will be removed from "{commonName}" only and will + remain in other locations. Do you want to continue? +
+ + +
+ {errorText && {errorText}} +
+
+ ); +}; + +export default UnlinkStreamModal; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/index.ts new file mode 100644 index 0000000000..a8902e6f40 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/index.ts @@ -0,0 +1 @@ +export { default as UnlinkStreamModal } from "./UnlinkStreamModal"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/index.ts new file mode 100644 index 0000000000..605eb3dc70 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/components/index.ts @@ -0,0 +1,4 @@ +export * from "./LinkStreamModal"; +export * from "./MoveStreamModal"; +export * from "./UnlinkStreamModal"; +export * from "./OptimisticDiscussionFeedCardContent"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/hooks/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/hooks/index.ts new file mode 100644 index 0000000000..f0a637aef4 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useMenuItems"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/hooks/useMenuItems.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/hooks/useMenuItems.tsx new file mode 100644 index 0000000000..c7120aa2e3 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/hooks/useMenuItems.tsx @@ -0,0 +1,226 @@ +import React from "react"; +import { useDispatch } from "react-redux"; +import { animateScroll } from "react-scroll"; +import { CommonFeedService } from "@/services"; +import { CommonAction, FollowFeedItemAction } from "@/shared/constants"; +import { + Edit3Icon, + Pin2Icon, + Report2Icon, + Share3Icon, + FollowIcon, + Trash2Icon, + UnfollowIcon, + UnpinIcon, + Link4Icon as LinkIcon, + Message3Icon, + MoveItemIcon, +} from "@/shared/icons"; +import { + ContextMenuItem as Item, + MarkCommonFeedItemAsSeenPayload, + MarkCommonFeedItemAsUnseenPayload, + UploadFile, +} from "@/shared/interfaces"; +import { parseStringToTextEditorValue } from "@/shared/ui-kit"; +import { + getCirclesWithLowestTier, + getFilteredByIdCircles, +} from "@/shared/utils"; +import { notEmpty } from "@/shared/utils/notEmpty"; +import { commonActions } from "@/store/states"; +import { FeedItemMenuItem, GetAllowedItemsOptions } from "../../FeedItem"; +import { getAllowedItems } from "../utils"; + +interface Actions { + report: () => void; + share: () => void; + remove?: () => void; + linkStream?: () => void; + moveStream?: () => void; + unlinkStream?: () => void; + markFeedItemAsSeen: (payload: MarkCommonFeedItemAsSeenPayload) => void; + markFeedItemAsUnseen: (payload: MarkCommonFeedItemAsUnseenPayload) => void; +} + +export const useMenuItems = ( + options: GetAllowedItemsOptions, + actions: Actions, +): Item[] => { + const dispatch = useDispatch(); + + if (options.withoutMenu) { + return []; + } + + const { + discussion, + commonId, + feedItem, + feedItemFollow, + feedItemUserMetadata, + } = options; + const { + report, + share, + remove, + linkStream, + moveStream, + unlinkStream, + markFeedItemAsSeen, + markFeedItemAsUnseen, + } = actions; + const allowedMenuItems = getAllowedItems({ ...options, feedItemFollow }); + const items: Item[] = [ + { + id: FeedItemMenuItem.Pin, + text: "Pin", + onClick: async () => { + if (!commonId || !feedItem) return; + await CommonFeedService.pinItem(commonId, feedItem.id); + }, + icon: , + }, + { + id: FeedItemMenuItem.Unpin, + text: "Unpin", + onClick: async () => { + if (!commonId || !feedItem) return; + await CommonFeedService.unpinItem(commonId, feedItem.id); + }, + icon: , + }, + { + id: FeedItemMenuItem.Share, + text: "Share", + onClick: share, + icon: , + }, + { + id: FeedItemMenuItem.MarkUnread, + text: "Mark as unread", + onClick: () => { + if (!commonId || !feedItem) { + return; + } + + markFeedItemAsUnseen({ + commonId, + feedObjectId: feedItem.id, + }); + }, + icon: , + }, + { + id: FeedItemMenuItem.MarkRead, + text: "Mark as read", + onClick: () => { + if (!commonId || !feedItem) { + return; + } + + markFeedItemAsSeen({ + commonId, + feedObjectId: feedItem.id, + lastSeenId: feedItemUserMetadata?.lastSeen?.id, + type: feedItemUserMetadata?.lastSeen?.type, + }); + }, + icon: , + }, + { + id: FeedItemMenuItem.Report, + text: "Report", + onClick: report, + icon: , + }, + { + id: FeedItemMenuItem.Edit, + text: "Edit", + onClick: () => { + if (!discussion) { + return; + } + + const files: UploadFile[] = discussion.images.map((file, index) => ({ + id: index.toString(), + title: file.title, + file: file.value, + })); + const circleVisibility = + discussion.circleVisibilityByCommon?.[options.commonId || ""] || []; + const filteredByIdCircles = getFilteredByIdCircles( + options.governanceCircles + ? Object.values(options.governanceCircles) + : null, + circleVisibility, + ); + const circles = getCirclesWithLowestTier(filteredByIdCircles); + + dispatch( + commonActions.setDiscussionCreationData({ + circle: circles[0] || null, + title: discussion.title, + content: parseStringToTextEditorValue(discussion.message), + images: files, + id: discussion.id, + }), + ); + dispatch(commonActions.setCommonAction(CommonAction.EditDiscussion)); + animateScroll.scrollToTop({ containerId: document.body, smooth: true }); + }, + icon: , + }, + { + id: FeedItemMenuItem.Follow, + text: "Follow", + onClick: () => feedItemFollow.onFollowToggle(FollowFeedItemAction.Follow), + icon: , + }, + { + id: FeedItemMenuItem.Unfollow, + text: "Unfollow", + onClick: () => + feedItemFollow.onFollowToggle(FollowFeedItemAction.Unfollow), + icon: , + }, + linkStream + ? { + id: FeedItemMenuItem.LinkTo, + text: "Link to...", + onClick: linkStream, + icon: , + } + : undefined, + moveStream + ? { + id: FeedItemMenuItem.MoveTo, + text: "Move to...", + onClick: moveStream, + icon: , + } + : undefined, + unlinkStream + ? { + id: FeedItemMenuItem.Unlink, + text: "Unlink", + onClick: unlinkStream, + withWarning: true, + icon: , + } + : undefined, + remove + ? { + id: FeedItemMenuItem.Remove, + text: "Delete", + onClick: remove, + withWarning: true, + icon: , + } + : undefined, + ].filter(notEmpty); + + return items.filter((item) => + allowedMenuItems.includes(item.id as FeedItemMenuItem), + ); +}; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/index.ts new file mode 100644 index 0000000000..e3e2245f56 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/index.ts @@ -0,0 +1 @@ +export { default as OptimisticDiscussionFeedCard } from "./OptimisticDiscussionFeedCard"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsEditItemAllowed.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsEditItemAllowed.ts new file mode 100644 index 0000000000..085874a155 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsEditItemAllowed.ts @@ -0,0 +1,13 @@ +import { CommonFeedType, PredefinedTypes } from "@/shared/models"; +import { GetAllowedItemsOptions } from "../../FeedItem"; + +export function checkIsEditItemAllowed(options: GetAllowedItemsOptions) { + if (!options.commonMember) return false; + if (options.discussion?.predefinedType === PredefinedTypes.General) + return false; + + return ( + options.commonMember.userId === options.discussion?.ownerId && + options.feedItem?.data.type === CommonFeedType.Discussion + ); +} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsLinkToAllowed.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsLinkToAllowed.ts new file mode 100644 index 0000000000..7bc6b04502 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsLinkToAllowed.ts @@ -0,0 +1,32 @@ +import { GovernanceActions } from "@/shared/constants"; +import { getCirclesWithLowestTier, hasPermission } from "@/shared/utils"; +import { GetAllowedItemsOptions } from "../../FeedItem"; + +export const checkIsLinkToAllowed = ( + options: GetAllowedItemsOptions, +): boolean => { + if (!options.commonMember) { + return false; + } + + const circlesWithLowestTier = getCirclesWithLowestTier( + Object.values(options.governanceCircles || {}), + ); + const discussionCircleVisibility = + options.discussion?.circleVisibilityByCommon?.[options.commonId || ""] || + []; + + return ( + (discussionCircleVisibility.length === 0 || + discussionCircleVisibility.some((circleId) => + circlesWithLowestTier.some((circle) => circle.id === circleId), + )) && + hasPermission({ + commonMember: options.commonMember, + governance: { + circles: options.governanceCircles || {}, + }, + action: GovernanceActions.LINK_FROM_HERE, + }) + ); +}; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsMoveToAllowed.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsMoveToAllowed.ts new file mode 100644 index 0000000000..1175daaeeb --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsMoveToAllowed.ts @@ -0,0 +1,28 @@ +import { GovernanceActions } from "@/shared/constants"; +import { PredefinedTypes } from "@/shared/models"; +import { hasPermission } from "@/shared/utils"; +import { GetAllowedItemsOptions } from "../../FeedItem"; + +export const checkIsMoveToAllowed = ( + options: GetAllowedItemsOptions, +): boolean => { + if ( + !options.commonMember || + options.discussion?.predefinedType === PredefinedTypes.General + ) { + return false; + } + + const linkedCommonIds = options.discussion?.linkedCommonIds || []; + + return ( + linkedCommonIds.length === 0 && + hasPermission({ + commonMember: options.commonMember, + governance: { + circles: options.governanceCircles || {}, + }, + action: GovernanceActions.MOVE_FROM_HERE, + }) + ); +}; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsPinUnpinAllowed.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsPinUnpinAllowed.ts new file mode 100644 index 0000000000..ee5d572477 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsPinUnpinAllowed.ts @@ -0,0 +1,44 @@ +import { GovernanceActions } from "@/shared/constants"; +import { PredefinedTypes } from "@/shared/models"; +import { hasPermission } from "@/shared/utils"; +import { FeedItemPinAction, GetAllowedItemsOptions } from "../../FeedItem"; + +const PINNED_ITEMS_LIMIT = 7; + +export function checkIsPinUnpinAllowed( + action: FeedItemPinAction, + options: GetAllowedItemsOptions, +) { + const { feedItem, commonMember, discussion, pinnedFeedItems = [] } = options; + const isDiscussionPinned = pinnedFeedItems.some( + (pinnedFeedItem) => pinnedFeedItem.feedObjectId === feedItem?.id, + ); + + if (!commonMember) { + return false; + } + + if (action === FeedItemPinAction.Pin) { + const hasReachedPinLimit = pinnedFeedItems.length >= PINNED_ITEMS_LIMIT; + + if (isDiscussionPinned || hasReachedPinLimit) { + return false; + } + } else if ( + action === FeedItemPinAction.Unpin && + (!isDiscussionPinned || + discussion?.predefinedType === PredefinedTypes.General) + ) { + return false; + } + + const isAllowed = hasPermission({ + commonMember, + governance: { + circles: options.governanceCircles || {}, + }, + action: GovernanceActions.PIN_OR_UNPIN_FEED_ITEMS, + }); + + return isAllowed; +} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsRemoveDiscussionAllowed.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsRemoveDiscussionAllowed.ts new file mode 100644 index 0000000000..4977af6973 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsRemoveDiscussionAllowed.ts @@ -0,0 +1,38 @@ +import { GovernanceActions } from "@/shared/constants"; +import { PredefinedTypes } from "@/shared/models"; +import { getCirclesWithHighestTier, hasPermission } from "@/shared/utils"; +import { GetAllowedItemsOptions } from "../../FeedItem"; + +export function checkIsRemoveDiscussionAllowed( + options: GetAllowedItemsOptions, +): boolean { + const { commonMember } = options; + + if ( + !commonMember || + options.discussion?.predefinedType === PredefinedTypes.General + ) { + return false; + } + + const circles = options.governanceCircles || {}; + const isDiscussionOwner = commonMember.userId === options.discussion?.ownerId; + const hasPermissionToRemoveDiscussion = + hasPermission({ + commonMember, + governance: { circles }, + action: GovernanceActions.HIDE_OR_UNHIDE_DISCUSSION, + }) || isDiscussionOwner; + + if (!options.discussion?.proposalId) { + return hasPermissionToRemoveDiscussion; + } + + const circlesWithHighestTier = getCirclesWithHighestTier( + Object.values(circles), + ); + + return circlesWithHighestTier.some((circle) => + commonMember.circleIds.includes(circle.id), + ); +} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/getAllowedItems.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/getAllowedItems.ts new file mode 100644 index 0000000000..27f3e77cb0 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/getAllowedItems.ts @@ -0,0 +1,100 @@ +import { CommonFeedType } from "@/shared/models"; +import { notEmpty } from "@/shared/utils/notEmpty"; +import { FeedItemMenuItem, FeedItemPinAction } from "../../FeedItem/constants"; +import { GetAllowedItemsOptions } from "../../FeedItem/types"; +import { checkIsEditItemAllowed } from "./checkIsEditItemAllowed"; +import { checkIsLinkToAllowed } from "./checkIsLinkToAllowed"; +import { checkIsMoveToAllowed } from "./checkIsMoveToAllowed"; +import { checkIsPinUnpinAllowed } from "./checkIsPinUnpinAllowed"; +import { checkIsRemoveDiscussionAllowed } from "./checkIsRemoveDiscussionAllowed"; + +const MENU_ITEM_TO_CHECK_FUNCTION_MAP: Record< + FeedItemMenuItem, + (options: GetAllowedItemsOptions) => boolean +> = { + [FeedItemMenuItem.Share]: () => true, + [FeedItemMenuItem.Report]: () => false, + [FeedItemMenuItem.Edit]: (options) => checkIsEditItemAllowed(options), + [FeedItemMenuItem.Remove]: checkIsRemoveDiscussionAllowed, + [FeedItemMenuItem.Pin]: (options) => + checkIsPinUnpinAllowed(FeedItemPinAction.Pin, options), + [FeedItemMenuItem.Unpin]: (options) => + checkIsPinUnpinAllowed(FeedItemPinAction.Unpin, options), + [FeedItemMenuItem.Follow]: (options) => { + return ( + !options.feedItemFollow.isDisabled && !options.feedItemFollow.isFollowing + ); + }, + [FeedItemMenuItem.Unfollow]: (options) => { + return ( + !options.feedItemFollow.isDisabled && options.feedItemFollow.isFollowing + ); + }, + [FeedItemMenuItem.MarkUnread]: ({ feedItemUserMetadata }) => { + const { count, seen, isSeenUpdating } = feedItemUserMetadata || {}; + + if (!feedItemUserMetadata) { + return true; + } + + if (isSeenUpdating) { + return false; + } + + return notEmpty(count) && notEmpty(seen) && count === 0 && seen; + }, + [FeedItemMenuItem.MarkRead]: ({ feedItemUserMetadata }) => { + const { count, seenOnce, seen, isSeenUpdating } = + feedItemUserMetadata || {}; + + if (isSeenUpdating) { + return false; + } + + return ( + Boolean(count) || + (notEmpty(seen) && !seen) || + (notEmpty(seenOnce) && !seenOnce) + ); + }, + [FeedItemMenuItem.LinkTo]: checkIsLinkToAllowed, + [FeedItemMenuItem.MoveTo]: checkIsMoveToAllowed, + [FeedItemMenuItem.Unlink]: (options) => { + return Boolean( + options.discussion?.linkedCommonIds && + options.discussion?.linkedCommonIds.length > 0 && + checkIsRemoveDiscussionAllowed(options), + ); + }, +}; + +export const getAllowedItems = ( + options: GetAllowedItemsOptions, +): FeedItemMenuItem[] => { + const orderedItems = [ + FeedItemMenuItem.Follow, + FeedItemMenuItem.Unfollow, + FeedItemMenuItem.Pin, + FeedItemMenuItem.Unpin, + FeedItemMenuItem.Share, + FeedItemMenuItem.MarkUnread, + FeedItemMenuItem.MarkRead, + FeedItemMenuItem.Report, + FeedItemMenuItem.Edit, + FeedItemMenuItem.LinkTo, + FeedItemMenuItem.MoveTo, + FeedItemMenuItem.Unlink, + FeedItemMenuItem.Remove, + ]; + const nonAllowedItems = + options.getNonAllowedItems?.( + options?.feedItem?.data.type ?? CommonFeedType.Discussion, + options, + ) || []; + + return orderedItems.filter( + (item) => + !nonAllowedItems.includes(item) && + MENU_ITEM_TO_CHECK_FUNCTION_MAP[item](options), + ); +}; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/index.ts new file mode 100644 index 0000000000..cd586fc395 --- /dev/null +++ b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/index.ts @@ -0,0 +1 @@ +export * from "./getAllowedItems"; diff --git a/src/pages/common/components/index.ts b/src/pages/common/components/index.ts index e57e087668..5007e04803 100644 --- a/src/pages/common/components/index.ts +++ b/src/pages/common/components/index.ts @@ -3,6 +3,7 @@ export * from "./CommonMobileModal"; export * from "./CommonTabPanels"; export * from "./CommonTopNavigation"; export * from "./DiscussionFeedCard"; +export * from "./OptimisticDiscussionFeedCard"; export * from "./FeedCard"; export * from "./FeedItem"; export * from "./FeedItems"; diff --git a/src/pages/commonFeed/CommonFeed.tsx b/src/pages/commonFeed/CommonFeed.tsx index f578d5d96a..8040a12a33 100644 --- a/src/pages/commonFeed/CommonFeed.tsx +++ b/src/pages/commonFeed/CommonFeed.tsx @@ -53,6 +53,7 @@ import { selectCommonAction, selectFeedSearchValue, selectIsSearchingFeedItems, + selectOptimisticFeedItems, selectRecentStreamId, selectSharedFeedItem, } from "@/store/states"; @@ -114,6 +115,7 @@ const CommonFeedComponent: FC = (props) => { sharedFeedItemIdQueryParam) || null; const commonAction = useSelector(selectCommonAction); + const optimisticFeedItems = useSelector(selectOptimisticFeedItems); const { data: commonData, stateRef, @@ -204,7 +206,7 @@ const CommonFeedComponent: FC = (props) => { ); const sharedFeedItem = useSelector(selectSharedFeedItem); - const topFeedItems = useMemo(() => { + const topFeedItemsWithoutOptimistic = useMemo(() => { const items: FeedLayoutItem[] = []; const filteredPinnedItems = commonPinnedFeedItems?.filter( @@ -220,6 +222,18 @@ const CommonFeedComponent: FC = (props) => { return items; }, [sharedFeedItem, sharedFeedItemId, commonPinnedFeedItems]); + + const topFeedItems = useMemo(() => { + const items: FeedLayoutItem[] = [...topFeedItemsWithoutOptimistic]; + + if (optimisticFeedItems.size > 0) { + const optimisticItems = Array.from(optimisticFeedItems.values()); + items.push(...optimisticItems); + } + + return items; + }, [topFeedItemsWithoutOptimistic, optimisticFeedItems]); + const firstItem = commonFeedItems?.[0]; const isDataFetched = isCommonDataFetched; const hasPublicItems = commonData?.common.hasPublicItems ?? false; @@ -454,8 +468,15 @@ const CommonFeedComponent: FC = (props) => { feedItemId: firstItem.feedItem.id, }); dispatch(commonActions.setRecentStreamId("")); + } else if ( + checkIsFeedItemFollowLayoutItem(firstItem) && + optimisticFeedItems.has(recentStreamId) + ) { + feedLayoutRef?.setActiveItem({ + feedItemId: optimisticFeedItems.get(recentStreamId)!.feedItem.id, + }); } - }, [feedLayoutRef, recentStreamId, firstItem]); + }, [feedLayoutRef, recentStreamId, firstItem, optimisticFeedItems]); useEffect(() => { const handler: CommonEventToListener[CommonEvent.CommonDeleted] = ( diff --git a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx index 205c352549..694b12b3ef 100644 --- a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx +++ b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx @@ -75,6 +75,7 @@ import { getParamsFromOneOfRoutes, getUserName, } from "@/shared/utils"; +import { selectOptimisticFeedItems } from "@/store/states"; import { MIN_CONTENT_WIDTH } from "../../constants"; import { DesktopChat, @@ -254,6 +255,7 @@ const FeedLayout: ForwardRefRenderFunction = ( return items; }, [topFeedItems, feedItems]); + const dmChatChannelItemForProfile = useMemo( () => getDMChatChannelItemByUserIds( diff --git a/src/shared/hooks/useCases/useCommonFeedItems.ts b/src/shared/hooks/useCases/useCommonFeedItems.ts index f96da830ab..76c3d94640 100644 --- a/src/shared/hooks/useCases/useCommonFeedItems.ts +++ b/src/shared/hooks/useCases/useCommonFeedItems.ts @@ -6,6 +6,7 @@ import { FeedItems, selectFeedItems, selectFilteredFeedItems, + selectOptimisticFeedItems, } from "@/store/states"; interface Return @@ -21,6 +22,7 @@ export const useCommonFeedItems = ( const dispatch = useDispatch(); const feedItems = useSelector(selectFeedItems); const filteredFeedItems = useSelector(selectFilteredFeedItems); + const optimisticFeedItems = useSelector(selectOptimisticFeedItems); const idsForNotListeningRef = useRef(idsForNotListening || []); const isSubscriptionAllowed = feedItems.data !== null; @@ -53,6 +55,20 @@ export const useCommonFeedItems = ( return; } + + // TODO: HERE I can get optFeedItem by discussionId + const optItemIds = Array.from(optimisticFeedItems.values()).map((item) => { + return item.feedItem.data.id; + } ); + + data.forEach((item) => { + const discussionId = item.commonFeedItem.data.id; + if(optItemIds.includes(discussionId)) { + + dispatch(commonActions.removeOptimisticFeedItemState({id: discussionId})) + } + }) + const finalData = idsForNotListeningRef.current.length > 0 ? data.filter( @@ -68,7 +84,7 @@ export const useCommonFeedItems = ( ); return unsubscribe; - }, [isSubscriptionAllowed, feedItems.firstDocTimestamp, commonId]); + }, [isSubscriptionAllowed, feedItems.firstDocTimestamp, commonId, optimisticFeedItems]); useEffect(() => { return () => { diff --git a/src/shared/models/CommonFeed.tsx b/src/shared/models/CommonFeed.tsx index 3d264dc525..aeb4660d50 100644 --- a/src/shared/models/CommonFeed.tsx +++ b/src/shared/models/CommonFeed.tsx @@ -1,10 +1,12 @@ import { DiscussionMessageOwnerType } from "@/shared/constants"; import { BaseEntity } from "./BaseEntity"; +import { Discussion } from "./Discussion"; import { SoftDeleteEntity } from "./SoftDeleteEntity"; export enum CommonFeedType { Proposal = "Proposal", Discussion = "Discussion", + OptimisticDiscussion = "OptimisticDiscussion", Project = "Project", PayIn = "PayIn", ProjectCreation = "ProjectCreation", @@ -12,6 +14,12 @@ export enum CommonFeedType { JoinProjectInCommon = "JoinProjectInCommon", } +export enum OptimisticFeedItemState { + loading = 'loading', + rejected = 'failed', + fulfilled = 'fulfilled' +} + export interface CommonFeed extends BaseEntity, SoftDeleteEntity { userId: string; commonId: string; @@ -28,5 +36,8 @@ export interface CommonFeed extends BaseEntity, SoftDeleteEntity { hasFiles?: boolean; hasImages?: boolean; }; + optimisticData?: Discussion & { + state?: OptimisticFeedItemState + }; circleVisibility: string[]; } diff --git a/src/store/states/common/actions.ts b/src/store/states/common/actions.ts index d2a203959c..d9ccf72a23 100644 --- a/src/store/states/common/actions.ts +++ b/src/store/states/common/actions.ts @@ -16,7 +16,9 @@ import { CommonFeed, CommonMember, Discussion, + DiscussionMessage, Governance, + OptimisticFeedItemState, Proposal, } from "@/shared/models"; import { CommonActionType } from "./constants"; @@ -218,6 +220,27 @@ export const setSharedFeedItem = createStandardAction( CommonActionType.SET_SHARED_FEED_ITEM, )(); +export const setOptimisticFeedItem = createStandardAction( + CommonActionType.SET_OPTIMISTIC_FEED_ITEM, +)(); + +export const updateOptimisticFeedItemState = createStandardAction( + CommonActionType.UPDATE_OPTIMISTIC_FEED_ITEM, +)<{ + id: string; + state: OptimisticFeedItemState; +}>(); + +export const removeOptimisticFeedItemState = createStandardAction( + CommonActionType.REMOVE_OPTIMISTIC_FEED_ITEM, +)<{ + id: string; +}>(); + +export const setOptimisticDiscussionMessages = createStandardAction( + CommonActionType.SET_OPTIMISTIC_DISCUSSION_MESSAGES, +)(); + export const setRecentStreamId = createStandardAction( CommonActionType.SET_RECENT_STREAM_ID, )(); diff --git a/src/store/states/common/constants.ts b/src/store/states/common/constants.ts index 9a4c118b8f..e379c3b8f2 100644 --- a/src/store/states/common/constants.ts +++ b/src/store/states/common/constants.ts @@ -56,6 +56,12 @@ export enum CommonActionType { SET_SHARED_FEED_ITEM_ID = "@COMMON/SET_SHARED_FEED_ITEM_ID", SET_SHARED_FEED_ITEM = "@COMMON/SET_SHARED_FEED_ITEM", + SET_OPTIMISTIC_FEED_ITEM = "@COMMON/SET_OPTIMISTIC_FEED_ITEM", + UPDATE_OPTIMISTIC_FEED_ITEM = "@COMMON/UPDATE_OPTIMISTIC_FEED_ITEM", + REMOVE_OPTIMISTIC_FEED_ITEM = "@COMMON/REMOVE_OPTIMISTIC_FEED_ITEM", + + SET_OPTIMISTIC_DISCUSSION_MESSAGES = "@COMMON/SET_OPTIMISTIC_DISCUSSION_MESSAGES", + SET_RECENT_STREAM_ID = "@COMMON/SET_RECENT_STREAM_ID", SET_RECENT_ASSIGNED_CIRCLE_BY_MEMBER = "@COMMON/SET_RECENT_ASSIGNED_CIRCLE_BY_MEMBER", diff --git a/src/store/states/common/reducer.ts b/src/store/states/common/reducer.ts index 90e5f0d544..f2453ff31a 100644 --- a/src/store/states/common/reducer.ts +++ b/src/store/states/common/reducer.ts @@ -50,6 +50,8 @@ const initialState: CommonState = { searchState: { ...initialSearchState }, sharedFeedItemId: null, sharedFeedItem: null, + optimisticFeedItems: new Map(), + optimisticDiscussionMessages: new Map(), commonAction: null, discussionCreation: { data: null, @@ -449,9 +451,9 @@ export const reducer = createReducer(initialState) produce(state, (nextState) => { const payloadData = nextState.sharedFeedItemId ? payload.data && - payload.data.filter( - (item) => item.feedItem.id !== nextState.sharedFeedItemId, - ) + payload.data.filter( + (item) => item.feedItem.id !== nextState.sharedFeedItemId, + ) : payload.data; const nextData = nextState.feedItems.data || []; const filteredPayloadData = @@ -688,10 +690,71 @@ export const reducer = createReducer(initialState) produce(state, (nextState) => { nextState.sharedFeedItem = payload ? { - type: InboxItemType.FeedItemFollow, - itemId: payload.id, - feedItem: payload, - } + type: InboxItemType.FeedItemFollow, + itemId: payload.id, + feedItem: payload, + } : null; }), + ) + .handleAction(actions.setOptimisticFeedItem, (state, { payload }) => + produce(state, (nextState) => { + const updatedMap = new Map(nextState.optimisticFeedItems); + + // Add the new item to the Map + updatedMap.set(payload.data.id, { + type: InboxItemType.FeedItemFollow, + itemId: payload.id, + feedItem: payload, + }); + + // Assign the new Map back to the state + nextState.optimisticFeedItems = updatedMap; + nextState.recentStreamId = payload.data.id; + }), + ) + .handleAction(actions.updateOptimisticFeedItemState, (state, { payload }) => + produce(state, (nextState) => { + const updatedMap = new Map(nextState.optimisticFeedItems); + + const optimisticFeedItem = updatedMap.get(payload.id); + // Add the new item to the Map + + if(optimisticFeedItem && optimisticFeedItem?.feedItem.optimisticData) { + updatedMap.set(payload.id, { + ...optimisticFeedItem, + feedItem: { + ...optimisticFeedItem?.feedItem, + optimisticData: { + ...optimisticFeedItem.feedItem.optimisticData, + state: payload.state + } + } + }); + } + + // Assign the new Map back to the state + nextState.optimisticFeedItems = updatedMap; + }), + ) + .handleAction(actions.removeOptimisticFeedItemState, (state, { payload }) => + produce(state, (nextState) => { + const updatedMap = new Map(nextState.optimisticFeedItems); + + updatedMap.delete(payload.id); + + // Assign the new Map back to the state + nextState.optimisticFeedItems = updatedMap; + }), + ) + .handleAction(actions.setOptimisticDiscussionMessages, (state, { payload }) => + produce(state, (nextState) => { + const updatedMap = new Map(nextState.optimisticDiscussionMessages); + + // Add the new item to the Map + updatedMap.set(payload.id, payload); + + // Assign the new Map back to the state + nextState.optimisticDiscussionMessages = updatedMap; + }), ); diff --git a/src/store/states/common/selectors.ts b/src/store/states/common/selectors.ts index 025cf24e45..308be00ad4 100644 --- a/src/store/states/common/selectors.ts +++ b/src/store/states/common/selectors.ts @@ -48,6 +48,12 @@ export const selectSharedFeedItem = (state: AppState) => export const selectRecentStreamId = (state: AppState) => state.common.recentStreamId; +export const selectOptimisticFeedItems = (state: AppState) => + state.common.optimisticFeedItems; + +export const setOptimisticDiscussionMessages = (state: AppState) => + state.common.optimisticDiscussionMessages; + export const selectRecentAssignedCircle = (memberId: string) => (state: AppState) => state.common.recentAssignedCircleByMember[memberId]; diff --git a/src/store/states/common/types.ts b/src/store/states/common/types.ts index 0d9eee745d..5e59759575 100644 --- a/src/store/states/common/types.ts +++ b/src/store/states/common/types.ts @@ -4,7 +4,7 @@ import { NewDiscussionCreationFormValues, NewProposalCreationFormValues, } from "@/shared/interfaces"; -import { Circle, CommonMember, Governance, Timestamp } from "@/shared/models"; +import { Circle, CommonMember, DiscussionMessage, Governance, Timestamp } from "@/shared/models"; export type EntityCreation = { data: T | null; @@ -44,6 +44,8 @@ export interface CommonState { pinnedFeedItems: PinnedFeedItems; sharedFeedItemId: string | null; sharedFeedItem: FeedItemFollowLayoutItem | null; + optimisticFeedItems: Map; + optimisticDiscussionMessages: Map; commonAction: CommonAction | null; discussionCreation: EntityCreation; proposalCreation: EntityCreation; From de5b98c710b50b58304e3675439c5f5276e2c1f7 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Fri, 30 Aug 2024 12:50:39 +0300 Subject: [PATCH 07/38] CW-optimistic-streams Added OptimisticDiscussion type. Added optimistic store values. Added support for optimistic messages for Discussions --- .../ChatComponent/ChatComponent.tsx | 151 ++++++++---------- .../components/ChatComponent/utils/index.ts | 2 + .../ChatComponent/utils/sendMessages.ts | 49 ++++++ .../utils/uploadFilesAndImages.ts | 29 ++++ .../common/components/FeedItem/FeedItem.tsx | 9 +- src/store/states/common/actions.ts | 8 +- src/store/states/common/constants.ts | 1 + src/store/states/common/reducer.ts | 14 +- src/store/states/common/selectors.ts | 2 +- src/store/states/common/types.ts | 5 +- 10 files changed, 182 insertions(+), 88 deletions(-) create mode 100644 src/pages/common/components/ChatComponent/utils/sendMessages.ts create mode 100644 src/pages/common/components/ChatComponent/utils/uploadFilesAndImages.ts diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 0e0503f2f8..023ab4ea4a 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -11,10 +11,10 @@ import { useDispatch, useSelector } from "react-redux"; import { useDebounce, useMeasure, useScroll } from "react-use"; import classNames from "classnames"; import isHotkey from "is-hotkey"; -import { debounce, delay, omit } from "lodash"; +import { debounce } from "lodash"; import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; -import { ChatService, DiscussionMessageService, FileService } from "@/services"; +import { FileService } from "@/services"; import { Separator } from "@/shared/components"; import { ChatType, @@ -57,12 +57,13 @@ import { checkUncheckedItemsInTextEditorValue } from "@/shared/ui-kit/TextEditor import { InternalLinkData, notEmpty } from "@/shared/utils"; import { getUserName, hasPermission, isMobile } from "@/shared/utils"; import { - cacheActions, chatActions, selectCurrentDiscussionMessageReply, selectFilesPreview, FileInfo, selectOptimisticFeedItems, + commonActions, + selectOptimisticDiscussionMessages, } from "@/store/states"; import { ChatContentContext, ChatContentData } from "../CommonContent/context"; import { @@ -76,7 +77,11 @@ import { } from "./components"; import { checkIsLastSeenInPreviousDay } from "./components/ChatContent/utils"; import { useChatChannelChatAdapter, useDiscussionChatAdapter } from "./hooks"; -import { getLastNonUserMessage } from "./utils"; +import { + getLastNonUserMessage, + sendMessages, + uploadFilesAndImages, +} from "./utils"; import styles from "./ChatComponent.module.scss"; const BASE_CHAT_INPUT_HEIGHT = 48; @@ -164,14 +169,9 @@ export default function ChatComponent({ const discussionMessageReply = useSelector( selectCurrentDiscussionMessageReply(), ); - const optimisticFeedItems = useSelector(selectOptimisticFeedItems); const user = useSelector(selectUser()); const userId = user?.uid; const discussionId = discussion?.id || ""; - - const isOptimisticChat = optimisticFeedItems.has(discussionId); - - console.log("---isOptimisticChat", isOptimisticChat); const isChatChannel = Boolean(chatChannel); const hasPermissionToHide = @@ -266,6 +266,50 @@ export default function ChatComponent({ const prevFeedItemId = useRef(); const timeoutId = useRef | null>(); + const optimisticFeedItems = useSelector(selectOptimisticFeedItems); + const optimisticDiscussionMessages = useSelector( + selectOptimisticDiscussionMessages, + ); + + const isOptimisticChat = optimisticFeedItems.has(discussionId); + + useEffect(() => { + if (optimisticDiscussionMessages.size > 0) { + const entries = Array.from(optimisticDiscussionMessages.entries()); + (async () => { + await Promise.all( + entries.map(async ([optimisticMessageDiscussionId, messages]) => { + if (!optimisticFeedItems.has(optimisticMessageDiscussionId)) { + const newMessagesWithFiles = await uploadFilesAndImages(messages); + await sendMessages({ + newMessagesWithFiles, + updateChatMessage: chatMessagesData.updateChatMessage, + chatChannel, + discussionId: optimisticMessageDiscussionId, + dispatch, + }); + + dispatch( + commonActions.clearOptimisticDiscussionMessages( + optimisticMessageDiscussionId, + ), + ); + + return messages; + } + + return messages; + }), + ); + })(); + } + }, [ + optimisticFeedItems, + optimisticDiscussionMessages, + chatChannel, + chatMessagesData.updateChatMessage, + ]); + useEffect(() => { return () => { prevFeedItemId.current = feedItemId; @@ -352,71 +396,13 @@ export default function ChatComponent({ useDebounce( async () => { - const newMessagesWithFiles = await Promise.all( - newMessages.map(async (payload) => { - const [uploadedFiles, uploadedImages] = await Promise.all([ - FileService.uploadFiles( - (payload.filesPreview ?? []).map((file) => - FileService.convertFileInfoToUploadFile(file), - ), - ), - FileService.uploadFiles( - (payload.imagesPreview ?? []).map((file) => - FileService.convertFileInfoToUploadFile(file), - ), - ), - ]); - - const updatedPayload = omit(payload, [ - "filesPreview", - "imagesPreview", - ]); - - return { - ...updatedPayload, - images: uploadedImages, - files: uploadedFiles, - }; - }), - ); - - newMessagesWithFiles.map(async (payload, index) => { - delay(async () => { - const pendingMessageId = payload.pendingMessageId as string; - - if (chatChannel) { - const response = await ChatService.sendChatMessage({ - id: pendingMessageId, - chatChannelId: chatChannel.id, - text: payload.text || "", - images: payload.images, - files: payload.files, - mentions: payload.tags?.map((tag) => tag.value), - parentId: payload.parentId, - hasUncheckedItems: checkUncheckedItemsInTextEditorValue( - parseStringToTextEditorValue(payload.text), - ), - linkPreviews: payload.linkPreviews, - }); - chatMessagesData.updateChatMessage(response); - - return; - } - - const response = await DiscussionMessageService.createMessage({ - ...payload, - id: pendingMessageId, - }); - - dispatch( - cacheActions.updateDiscussionMessageWithActualId({ - discussionId, - pendingMessageId, - actualId: response.id, - }), - ); - }, 2000 * (index || 1)); - return payload; + const newMessagesWithFiles = await uploadFilesAndImages(newMessages); + await sendMessages({ + newMessagesWithFiles, + updateChatMessage: chatMessagesData.updateChatMessage, + chatChannel, + discussionId, + dispatch, }); if (newMessages.length > 0) { @@ -581,13 +567,17 @@ export default function ChatComponent({ }); } - setMessages((prev) => { - if (isFilesMessageWithoutTextAndImages) { - return [...prev, ...filePreviewPayload]; - } + if (isOptimisticChat) { + dispatch(commonActions.setOptimisticDiscussionMessages(payload)); + } else { + setMessages((prev) => { + if (isFilesMessageWithoutTextAndImages) { + return [...prev, ...filePreviewPayload]; + } - return [...prev, ...filePreviewPayload, payload]; - }); + return [...prev, ...filePreviewPayload, payload]; + }); + } if (isChatChannel) { pendingMessages.forEach((pendingMessage) => { @@ -624,6 +614,7 @@ export default function ChatComponent({ discussionMessages, isChatChannel, linkPreviewData, + isOptimisticChat, ], ); diff --git a/src/pages/common/components/ChatComponent/utils/index.ts b/src/pages/common/components/ChatComponent/utils/index.ts index babe1dcca9..e2ec04042b 100644 --- a/src/pages/common/components/ChatComponent/utils/index.ts +++ b/src/pages/common/components/ChatComponent/utils/index.ts @@ -1 +1,3 @@ export * from "./getLastNonUserMessage"; +export * from "./uploadFilesAndImages"; +export * from "./sendMessages"; \ No newline at end of file diff --git a/src/pages/common/components/ChatComponent/utils/sendMessages.ts b/src/pages/common/components/ChatComponent/utils/sendMessages.ts new file mode 100644 index 0000000000..5becdbac2d --- /dev/null +++ b/src/pages/common/components/ChatComponent/utils/sendMessages.ts @@ -0,0 +1,49 @@ +import { ChatService, DiscussionMessageService } from "@/services"; +import { parseStringToTextEditorValue } from "@/shared/ui-kit"; +import { checkUncheckedItemsInTextEditorValue } from "@/shared/ui-kit/TextEditor/utils"; +import { cacheActions } from "@/store/states"; +import { delay } from "lodash"; + +export const sendMessages = async ({ + newMessagesWithFiles, + updateChatMessage, + chatChannel, + discussionId, + dispatch +}) => { + newMessagesWithFiles.map(async (payload, index) => { + delay(async () => { + const pendingMessageId = payload.pendingMessageId as string; + + if (chatChannel) { + const response = await ChatService.sendChatMessage({ + id: pendingMessageId, + chatChannelId: chatChannel.id, + text: payload.text || "", + images: payload.images, + files: payload.files, + mentions: payload.tags?.map((tag) => tag.value), + parentId: payload.parentId, + hasUncheckedItems: checkUncheckedItemsInTextEditorValue( + parseStringToTextEditorValue(payload.text), + ), + linkPreviews: payload.linkPreviews, + }); + updateChatMessage(response); + } else { + const response = await DiscussionMessageService.createMessage({ + ...payload, + id: pendingMessageId, + }); + + dispatch( + cacheActions.updateDiscussionMessageWithActualId({ + discussionId, + pendingMessageId, + actualId: response.id, + }), + ); + } + }, 2000 * (index || 1)); + }); +}; \ No newline at end of file diff --git a/src/pages/common/components/ChatComponent/utils/uploadFilesAndImages.ts b/src/pages/common/components/ChatComponent/utils/uploadFilesAndImages.ts new file mode 100644 index 0000000000..247dac5655 --- /dev/null +++ b/src/pages/common/components/ChatComponent/utils/uploadFilesAndImages.ts @@ -0,0 +1,29 @@ +import { FileService } from "@/services"; +import { omit } from "lodash"; + +export const uploadFilesAndImages = async (newMessages) => { + return await Promise.all( + newMessages.map(async (payload) => { + const [uploadedFiles, uploadedImages] = await Promise.all([ + FileService.uploadFiles( + (payload.filesPreview ?? []).map((file) => + FileService.convertFileInfoToUploadFile(file), + ), + ), + FileService.uploadFiles( + (payload.imagesPreview ?? []).map((file) => + FileService.convertFileInfoToUploadFile(file), + ), + ), + ]); + + const updatedPayload = omit(payload, ["filesPreview", "imagesPreview"]); + + return { + ...updatedPayload, + images: uploadedImages, + files: uploadedFiles, + }; + }), + ); +}; \ No newline at end of file diff --git a/src/pages/common/components/FeedItem/FeedItem.tsx b/src/pages/common/components/FeedItem/FeedItem.tsx index c423e5b41f..52169af8ef 100644 --- a/src/pages/common/components/FeedItem/FeedItem.tsx +++ b/src/pages/common/components/FeedItem/FeedItem.tsx @@ -22,8 +22,8 @@ import { } from "@/shared/models"; import { checkIsItemVisibleForUser, InternalLinkData } from "@/shared/utils"; import { useFeedItemSubscription } from "../../hooks"; -import { OptimisticDiscussionFeedCard } from "../OptimisticDiscussionFeedCard"; import { DiscussionFeedCard } from "../DiscussionFeedCard"; +import { OptimisticDiscussionFeedCard } from "../OptimisticDiscussionFeedCard"; import { ProposalFeedCard } from "../ProposalFeedCard"; import { ProjectFeedItem } from "./components"; import { useFeedItemContext } from "./context"; @@ -182,7 +182,12 @@ const FeedItem = forwardRef((props, ref) => { }; if (item.data.type === CommonFeedType.OptimisticDiscussion) { - return + return ( + + ); } if (item.data.type === CommonFeedType.Discussion) { diff --git a/src/store/states/common/actions.ts b/src/store/states/common/actions.ts index d9ccf72a23..3195757470 100644 --- a/src/store/states/common/actions.ts +++ b/src/store/states/common/actions.ts @@ -16,7 +16,6 @@ import { CommonFeed, CommonMember, Discussion, - DiscussionMessage, Governance, OptimisticFeedItemState, Proposal, @@ -29,6 +28,7 @@ import { FeedItemsPayload, PinnedFeedItems, } from "./types"; +import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages"; export const resetCommon = createStandardAction( CommonActionType.RESET_COMMON, @@ -239,7 +239,11 @@ export const removeOptimisticFeedItemState = createStandardAction( export const setOptimisticDiscussionMessages = createStandardAction( CommonActionType.SET_OPTIMISTIC_DISCUSSION_MESSAGES, -)(); +)(); + +export const clearOptimisticDiscussionMessages = createStandardAction( + CommonActionType.CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES, +)(); export const setRecentStreamId = createStandardAction( CommonActionType.SET_RECENT_STREAM_ID, diff --git a/src/store/states/common/constants.ts b/src/store/states/common/constants.ts index e379c3b8f2..75b4acc3d2 100644 --- a/src/store/states/common/constants.ts +++ b/src/store/states/common/constants.ts @@ -61,6 +61,7 @@ export enum CommonActionType { REMOVE_OPTIMISTIC_FEED_ITEM = "@COMMON/REMOVE_OPTIMISTIC_FEED_ITEM", SET_OPTIMISTIC_DISCUSSION_MESSAGES = "@COMMON/SET_OPTIMISTIC_DISCUSSION_MESSAGES", + CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES = "@COMMON/CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES", SET_RECENT_STREAM_ID = "@COMMON/SET_RECENT_STREAM_ID", diff --git a/src/store/states/common/reducer.ts b/src/store/states/common/reducer.ts index f2453ff31a..adf5f49e45 100644 --- a/src/store/states/common/reducer.ts +++ b/src/store/states/common/reducer.ts @@ -751,10 +751,22 @@ export const reducer = createReducer(initialState) produce(state, (nextState) => { const updatedMap = new Map(nextState.optimisticDiscussionMessages); + const discussionMessages = updatedMap.get(payload.discussionId) ?? []; + discussionMessages.push(payload); // Add the new item to the Map - updatedMap.set(payload.id, payload); + updatedMap.set(payload.discussionId, discussionMessages); // Assign the new Map back to the state nextState.optimisticDiscussionMessages = updatedMap; }), + ) + .handleAction(actions.clearOptimisticDiscussionMessages, (state, { payload }) => + produce(state, (nextState) => { + const updatedMap = new Map(nextState.optimisticDiscussionMessages); + + updatedMap.delete(payload); + + // Assign the new Map back to the state + nextState.optimisticDiscussionMessages = updatedMap; + }), ); diff --git a/src/store/states/common/selectors.ts b/src/store/states/common/selectors.ts index 308be00ad4..52ede5e078 100644 --- a/src/store/states/common/selectors.ts +++ b/src/store/states/common/selectors.ts @@ -51,7 +51,7 @@ export const selectRecentStreamId = (state: AppState) => export const selectOptimisticFeedItems = (state: AppState) => state.common.optimisticFeedItems; -export const setOptimisticDiscussionMessages = (state: AppState) => +export const selectOptimisticDiscussionMessages = (state: AppState) => state.common.optimisticDiscussionMessages; export const selectRecentAssignedCircle = diff --git a/src/store/states/common/types.ts b/src/store/states/common/types.ts index 5e59759575..71990236c2 100644 --- a/src/store/states/common/types.ts +++ b/src/store/states/common/types.ts @@ -4,7 +4,8 @@ import { NewDiscussionCreationFormValues, NewProposalCreationFormValues, } from "@/shared/interfaces"; -import { Circle, CommonMember, DiscussionMessage, Governance, Timestamp } from "@/shared/models"; +import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages"; +import { Circle, CommonMember, Governance, Timestamp } from "@/shared/models"; export type EntityCreation = { data: T | null; @@ -45,7 +46,7 @@ export interface CommonState { sharedFeedItemId: string | null; sharedFeedItem: FeedItemFollowLayoutItem | null; optimisticFeedItems: Map; - optimisticDiscussionMessages: Map; + optimisticDiscussionMessages: Map; commonAction: CommonAction | null; discussionCreation: EntityCreation; proposalCreation: EntityCreation; From 5a1d7ad1ea57977feb745e6ad3ee300e8a373774 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Fri, 30 Aug 2024 18:55:54 +0300 Subject: [PATCH 08/38] CW-optimistic-streams Added generation of proposalid --- .../AddProposalComponent/AddProposalComponent.tsx | 7 ++++++- .../AddProposalComponent/AddProposalForm.tsx | 7 ++++++- .../AssignCircleStage/AssignCircleStage.tsx | 5 +++++ .../DeleteCommonStage/DeleteCommonStage.tsx | 5 +++++ .../FundsAllocationStage/FundsAllocationStage.tsx | 5 +++++ .../RemoveCircleStage/RemoveCircleStage.tsx | 5 +++++ .../CreateProposalModal/SurveyStage/SurveyStage.tsx | 6 ++++++ .../MembershipRequestModal/MembershipRequestCreating.tsx | 5 +++++ .../MemberAdmittanceForProjectStep.tsx | 9 +++++++++ .../MemberAdmittanceStep/MemberAdmittanceStep.tsx | 5 +++++ .../components/PopoverItem/PopoverItem.tsx | 3 +++ .../NewProposalCreation/NewProposalCreation.tsx | 7 ++++++- .../FeedTab/components/NewProposalCreation/util.ts | 8 ++++++++ .../components/JoinProjectModal/JoinProjectModal.tsx | 2 ++ src/pages/common/hooks/useJoinProjectAutomatically.ts | 5 +++++ .../models/governance/proposals/BasicArgsProposal.ts | 4 ++++ 16 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalComponent.tsx b/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalComponent.tsx index 4f842c14d3..1336f47986 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalComponent.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalComponent.tsx @@ -6,6 +6,7 @@ import React, { useState, } from "react"; import { useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import classNames from "classnames"; import { Modal } from "@/shared/components"; import { AllocateFundsTo, ScreenSize } from "@/shared/constants"; @@ -67,6 +68,8 @@ export const AddProposalComponent = ({ const [fundingRequest, setFundingRequest] = useState({ args: { + id: "", + discussionId: "", title: "", description: "", links: [], @@ -125,8 +128,10 @@ export const AddProposalComponent = ({ const saveProposalState = useCallback( (payload: Partial) => { + const proposalId = uuidv4(); + const discussionId = uuidv4(); const fundingRequestData = { - args: { ...fundingRequest.args, ...payload }, + args: { ...fundingRequest.args, ...payload, id: proposalId, discussionId }, }; setFundingRequest(fundingRequestData); if (!payload?.amount) { diff --git a/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalForm.tsx b/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalForm.tsx index 1034da0162..ab3029f59e 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalForm.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalForm.tsx @@ -3,6 +3,7 @@ import { useDispatch } from "react-redux"; import classNames from "classnames"; import { Formik } from "formik"; import * as Yup from "yup"; +import { v4 as uuidv4 } from "uuid"; import { getBankDetails } from "@/pages/OldCommon/store/actions"; import { Button, ButtonIcon, Loader, ModalFooter } from "@/shared/components"; import { @@ -101,6 +102,8 @@ export const AddProposalForm = ({ }, [dispatch, hidden]); const [formValues] = useState({ + id: "", + discussionId: "", title: "", description: "", links: [{ title: "", value: "" }], @@ -168,7 +171,9 @@ export const AddProposalForm = ({ validationSchema={schema} onSubmit={(values, { setSubmitting }) => { setSubmitting(false); - saveProposalState({ ...values, images: uploadedFiles }); + const proposalId = uuidv4(); + const discussionId = uuidv4(); + saveProposalState({ ...values, images: uploadedFiles, id: proposalId, discussionId }); }} initialValues={formValues} validateOnChange={true} diff --git a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/AssignCircleStage/AssignCircleStage.tsx b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/AssignCircleStage/AssignCircleStage.tsx index cd71256a31..5861938c45 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/AssignCircleStage/AssignCircleStage.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/AssignCircleStage/AssignCircleStage.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { useCommonMembers } from "@/pages/OldCommon/hooks"; import { CreateProposal } from "@/pages/OldCommon/interfaces"; @@ -79,11 +80,15 @@ const AssignCircleStage: FC = (props) => { } setIsProposalCreating(true); + const proposalId = uuidv4(); + const discussionId = uuidv4(); const payload: Omit< CreateProposal[ProposalsTypes.ASSIGN_CIRCLE]["data"], "type" > = { args: { + id: proposalId, + discussionId, commonId: common.id, // TODO: Use here name of common member title: `Request to join ${ diff --git a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/DeleteCommonStage/DeleteCommonStage.tsx b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/DeleteCommonStage/DeleteCommonStage.tsx index f5251cde2b..145aa579af 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/DeleteCommonStage/DeleteCommonStage.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/DeleteCommonStage/DeleteCommonStage.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { CreateProposal } from "@/pages/OldCommon/interfaces"; import { createDeleteCommonProposal } from "@/pages/OldCommon/store/actions"; @@ -68,11 +69,15 @@ const DeleteCommonStage: FC = (props) => { } setIsProposalCreating(true); + const proposalId = uuidv4(); + const discussionId = uuidv4(); const payload: Omit< CreateProposal[ProposalsTypes.DELETE_COMMON]["data"], "type" > = { args: { + id: proposalId, + discussionId, commonId: common.id, title: `Delete common proposal from ${getUserName(user)}`, description: deleteCommonData.description, diff --git a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/FundsAllocationStage/FundsAllocationStage.tsx b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/FundsAllocationStage/FundsAllocationStage.tsx index 5843b5e3d0..6c893cbe42 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/FundsAllocationStage/FundsAllocationStage.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/FundsAllocationStage/FundsAllocationStage.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { useCommonMembers } from "@/pages/OldCommon/hooks"; import { CreateProposal } from "@/pages/OldCommon/interfaces"; import { @@ -133,12 +134,16 @@ const FundsAllocationStage: FC = (props) => { : { otherMemberId: fundsAllocationData.otherMemberId }; setIsProposalCreating(true); + const proposalId = uuidv4(); + const discussionId = uuidv4(); const description = `${fundsAllocationData.description}\n\nGoal of Payment:\n${fundsAllocationData.goalOfPayment}`; const payload: Omit< CreateProposal[ProposalsTypes.FUNDS_ALLOCATION]["data"], "type" > = { args: { + id: proposalId, + discussionId, description, amount: { amount: fundsAllocationData.amount * 100, diff --git a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/RemoveCircleStage/RemoveCircleStage.tsx b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/RemoveCircleStage/RemoveCircleStage.tsx index 9dd62f4b8e..a379ed02df 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/RemoveCircleStage/RemoveCircleStage.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/RemoveCircleStage/RemoveCircleStage.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { useCommonMembers } from "@/pages/OldCommon/hooks"; import { CreateProposal } from "@/pages/OldCommon/interfaces"; @@ -74,11 +75,15 @@ const RemoveCircleStage: FC = (props) => { } setIsProposalCreating(true); + const proposalId = uuidv4(); + const discussionId = uuidv4(); const payload: Omit< CreateProposal[ProposalsTypes.REMOVE_CIRCLE]["data"], "type" > = { args: { + id: proposalId, + discussionId, commonId: common.id, title: `Remove circle proposal for ${getUserName( removeCircleData.commonMember.user, diff --git a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/SurveyStage/SurveyStage.tsx b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/SurveyStage/SurveyStage.tsx index 3b7bfa2dd6..76b7652a7d 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/SurveyStage/SurveyStage.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/SurveyStage/SurveyStage.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { CreateProposal } from "@/pages/OldCommon/interfaces"; import { createSurvey } from "@/pages/OldCommon/store/actions"; import { Loader, Modal } from "@/shared/components"; @@ -68,9 +69,14 @@ const SurveyStage: FC = (props) => { } setIsProposalCreating(true); + + const proposalId = uuidv4(); + const discussionId = uuidv4(); const payload: Omit = { args: { + id: proposalId, + discussionId, description: surveyData.description, commonId: common.id, title: surveyData.title, diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestCreating.tsx b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestCreating.tsx index 945356a834..be4c456f23 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestCreating.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestCreating.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { Loader } from "@/shared/components"; import { ContributionSourceType, Currency } from "@/shared/models"; @@ -39,10 +40,14 @@ export default function MembershipRequestCreating( return; } + const proposalId = uuidv4(); + const discussionId = uuidv4(); dispatch( createMemberAdmittanceProposal.request({ payload: { args: { + id: proposalId, + discussionId, commonId: common.id, title: `Membership request from ${userName}`, description: userData.intro, diff --git a/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceForProjectStep/MemberAdmittanceForProjectStep.tsx b/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceForProjectStep/MemberAdmittanceForProjectStep.tsx index 7abf6cdf6a..2904635115 100644 --- a/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceForProjectStep/MemberAdmittanceForProjectStep.tsx +++ b/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceForProjectStep/MemberAdmittanceForProjectStep.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { DeadSeaUserDetailsFormValuesWithoutUserDetails } from "@/pages/OldCommon/components"; import { useSupportersDataContext } from "@/pages/OldCommon/containers/SupportersContainer/context"; @@ -106,10 +107,14 @@ const MemberAdmittanceForProjectStep: FC< const title = `Membership request from ${userName}`; + const proposalId = uuidv4(); + const discussionId = uuidv4(); dispatch( createMemberAdmittanceProposal.request({ payload: { args: { + id: proposalId, + discussionId, commonId: parentCommonId, title, description: data.supportPlan || title, @@ -180,6 +185,8 @@ const MemberAdmittanceForProjectStep: FC< return; } + const proposalId = uuidv4(); + const discussionId = uuidv4(); try { const title = `${userName} joins and supports ${circleName}`; const payload: Omit< @@ -187,6 +194,8 @@ const MemberAdmittanceForProjectStep: FC< "type" > = { args: { + id: proposalId, + discussionId, commonId: parentCommonId, title, description: data.supportPlan || title, diff --git a/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceStep/MemberAdmittanceStep.tsx b/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceStep/MemberAdmittanceStep.tsx index 6bffe509ea..0fee7adbfd 100644 --- a/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceStep/MemberAdmittanceStep.tsx +++ b/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceStep/MemberAdmittanceStep.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { DeadSeaUserDetailsFormValuesWithoutUserDetails } from "@/pages/OldCommon/components"; import { useSupportersDataContext } from "@/pages/OldCommon/containers/SupportersContainer/context"; @@ -64,10 +65,14 @@ const MemberAdmittanceStep: FC = (props) => { const title = `Membership request from ${userName}`; + const proposalId = uuidv4(); + const discussionId = uuidv4(); dispatch( createMemberAdmittanceProposal.request({ payload: { args: { + id: proposalId, + discussionId, commonId, title, description: data.supportPlan || title, diff --git a/src/pages/common/components/CommonMemberInfo/components/PopoverItem/PopoverItem.tsx b/src/pages/common/components/CommonMemberInfo/components/PopoverItem/PopoverItem.tsx index 04275c28bd..cc8450e069 100644 --- a/src/pages/common/components/CommonMemberInfo/components/PopoverItem/PopoverItem.tsx +++ b/src/pages/common/components/CommonMemberInfo/components/PopoverItem/PopoverItem.tsx @@ -1,5 +1,6 @@ import React, { FC, useCallback } from "react"; import classNames from "classnames"; +import { v4 as uuidv4 } from "uuid"; import { useCommonDataContext } from "@/pages/common/providers"; import { Circle } from "@/shared/models"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui-kit"; @@ -103,6 +104,8 @@ export const PopoverItem: FC = (props) => { onJoinCircle( { args: { + id: uuidv4(), + discussionId: uuidv4(), commonId, title: `Request to join ${circleName} by ${userName}`, description: `Join request: ${circleName}`, diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx index 19c12752ff..898491c60e 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx @@ -1,5 +1,6 @@ import React, { FC, ReactNode, useCallback, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { PROPOSAL_TYPE_SELECT_OPTIONS, @@ -82,12 +83,16 @@ const NewProposalCreation: FC = (props) => { return; } + const proposalId = uuidv4(); + const discussionId = uuidv4(); switch (values.proposalType.value) { case ProposalsTypes.FUNDS_ALLOCATION: { const fundingProposalPayload = getFundingProposalPayload( values, commonId, userId, + proposalId, + discussionId, ); if (!fundingProposalPayload) { @@ -103,7 +108,7 @@ const NewProposalCreation: FC = (props) => { case ProposalsTypes.SURVEY: { dispatch( commonActions.createSurveyProposal.request({ - payload: getSurveyProposalPayload(values, commonId), + payload: getSurveyProposalPayload(values, commonId, proposalId, discussionId), }), ); break; diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/util.ts b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/util.ts index 75a7003982..b1a4ff5d4f 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/util.ts +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/util.ts @@ -10,6 +10,8 @@ export const getFundingProposalPayload = ( values: NewProposalCreationFormValues, commonId: string, userId: string, + proposalId: string, + discussionId: string, ): CreateProposalWithFiles | null => { if (!values.recipientInfo) { return null; @@ -21,6 +23,8 @@ export const getFundingProposalPayload = ( : AllocateFundsTo.OtherMember; return { + id: proposalId, + discussionId, title: values.title, description: JSON.stringify(values.content), images: values.images, @@ -48,8 +52,12 @@ export const getFundingProposalPayload = ( export const getSurveyProposalPayload = ( values: NewProposalCreationFormValues, commonId: string, + proposalId: string, + discussionId: string, ): CreateProposalWithFiles => { return { + id: proposalId, + discussionId, title: values.title, description: JSON.stringify(values.content), images: values.images, diff --git a/src/pages/common/components/JoinProjectModal/JoinProjectModal.tsx b/src/pages/common/components/JoinProjectModal/JoinProjectModal.tsx index ec09c5738a..335f001e02 100644 --- a/src/pages/common/components/JoinProjectModal/JoinProjectModal.tsx +++ b/src/pages/common/components/JoinProjectModal/JoinProjectModal.tsx @@ -1,5 +1,6 @@ import React, { FC, PropsWithChildren, useState, useCallback } from "react"; import { useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { GovernanceService, ProposalService } from "@/services"; import { Modal } from "@/shared/components"; @@ -132,6 +133,7 @@ const JoinProjectModal: FC> = ( )?.name; const payload = { + id: uuidv4(), commonId, description: message, images: [], diff --git a/src/pages/common/hooks/useJoinProjectAutomatically.ts b/src/pages/common/hooks/useJoinProjectAutomatically.ts index 31b51fbbd8..060ce3e4e2 100644 --- a/src/pages/common/hooks/useJoinProjectAutomatically.ts +++ b/src/pages/common/hooks/useJoinProjectAutomatically.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { ProposalService } from "@/services"; import { ProposalsTypes, SUPPORT_EMAIL } from "@/shared/constants"; @@ -135,9 +136,13 @@ export const useJoinProjectAutomatically = ( setIsJoinPending(true); + const proposalId = uuidv4(); + const discussionId = uuidv4(); try { await ProposalService.createAssignProposal({ args: { + id: proposalId, + discussionId, commonId: parentCommon.id, userId: user?.uid, circleId, diff --git a/src/shared/models/governance/proposals/BasicArgsProposal.ts b/src/shared/models/governance/proposals/BasicArgsProposal.ts index 76f615e772..4078d30ddb 100644 --- a/src/shared/models/governance/proposals/BasicArgsProposal.ts +++ b/src/shared/models/governance/proposals/BasicArgsProposal.ts @@ -1,6 +1,10 @@ import { CommonLink } from "../../Common"; export interface BasicArgsProposal { + id: string; + + discussionId: string; + readonly commonId: string; readonly proposerId: string; From ba461e8b3b44fc36a7afe9e61c5293b28a4577bf Mon Sep 17 00:00:00 2001 From: matanfield Date: Fri, 30 Aug 2024 22:27:24 +0300 Subject: [PATCH 09/38] Adjust sidenav width to 15rem for better space utilization --- src/shared/layouts/SidenavLayout/SidenavLayout.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/layouts/SidenavLayout/SidenavLayout.module.scss b/src/shared/layouts/SidenavLayout/SidenavLayout.module.scss index 014271ce7f..3770f017f7 100644 --- a/src/shared/layouts/SidenavLayout/SidenavLayout.module.scss +++ b/src/shared/layouts/SidenavLayout/SidenavLayout.module.scss @@ -4,7 +4,7 @@ .container { --main-pl: unset; --sb-max-width: unset; - --sb-width: 18.75rem; + --sb-width: 15rem; --sb-content-max-width: 100%; --sb-content-width: 100%; --sb-content-pb: 0; From 8f85746db22ccc46cbb1714c49e91daee229e3ee Mon Sep 17 00:00:00 2001 From: matanfield Date: Fri, 30 Aug 2024 23:04:11 +0300 Subject: [PATCH 10/38] Standardize sidebar width to 15rem across all layouts --- .../layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss | 2 +- .../MultipleSpacesLayout/MultipleSpacesLayout.module.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss b/src/shared/layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss index 23b4c9cdf1..4bb72194db 100644 --- a/src/shared/layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss +++ b/src/shared/layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss @@ -5,7 +5,7 @@ --main-mw: 100%; --main-pl: unset; --sb-max-width: unset; - --sb-width: 21rem; + --sb-width: 15rem; --sb-content-max-width: 100%; --sb-content-width: 100%; --sb-content-pb: 0; diff --git a/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss b/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss index 1f259c1b07..d6df035e7c 100644 --- a/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss +++ b/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss @@ -5,7 +5,7 @@ --main-mw: calc(120rem + var(--sb-h-indent, 0)); --main-pl: calc(var(--sb-h-indent, 0)); --sb-max-width: unset; - --sb-width: 21rem; + --sb-width: 15rem; --sb-content-max-width: 100%; --sb-content-width: 100%; --sb-content-pb: 0; From a6eaa743ef200d1633ec829da364d65dc7c4c1e1 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Mon, 2 Sep 2024 01:54:14 +0300 Subject: [PATCH 11/38] CW-Optimistic-stream Remove unnecessary comments --- .../ChatComponent/ChatComponent.tsx | 2 + .../NewProposalCreation.tsx | 74 ++++++++++++++++++- .../common/components/FeedItems/FeedItems.tsx | 37 +++++----- 3 files changed, 90 insertions(+), 23 deletions(-) diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 023ab4ea4a..4a71557936 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -267,6 +267,8 @@ export default function ChatComponent({ const timeoutId = useRef | null>(); const optimisticFeedItems = useSelector(selectOptimisticFeedItems); + + console.log("--optimisticFeedItems", optimisticFeedItems); const optimisticDiscussionMessages = useSelector( selectOptimisticDiscussionMessages, ); diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx index 898491c60e..14a887527f 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx @@ -1,8 +1,10 @@ import React, { FC, ReactNode, useCallback, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { Timestamp as FirestoreTimestamp } from "firebase/firestore"; import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { + DiscussionMessageOwnerType, PROPOSAL_TYPE_SELECT_OPTIONS, ProposalsTypes, } from "@/shared/constants"; @@ -10,10 +12,13 @@ import { NewProposalCreationFormValues } from "@/shared/interfaces"; import { CirclesPermissions, Common, + CommonFeedType, CommonMember, Governance, + OptimisticFeedItemState, } from "@/shared/models"; import { parseStringToTextEditorValue } from "@/shared/ui-kit/TextEditor"; +import { getUserName } from "@/shared/utils"; import { selectIsProposalCreationLoading, selectProposalCreationData, @@ -85,6 +90,53 @@ const NewProposalCreation: FC = (props) => { const proposalId = uuidv4(); const discussionId = uuidv4(); + const currentDate = FirestoreTimestamp.now(); + + const optimisticFeedItemId = uuidv4(); + dispatch( + commonActions.setOptimisticFeedItem({ + id: optimisticFeedItemId, + createdAt: currentDate, + updatedAt: currentDate, + isDeleted: false, + userId, + commonId: common.id, + data: { + type: CommonFeedType.OptimisticDiscussion, + id: discussionId, + discussionId: null, + lastMessage: { + userName: getUserName(user), + ownerId: userId, + content: JSON.stringify(values.content), + ownerType: DiscussionMessageOwnerType.User, + }, + hasFiles: false, + hasImages: false, + }, + optimisticData: { + id: discussionId, + title: values.title, + message: JSON.stringify(values.content), + ownerId: userId, + commonId: common.id, + lastMessage: currentDate, + updatedAt: currentDate, + createdAt: currentDate, + messageCount: 0, + followers: [], + files: [], + images: [], + discussionMessages: [], + isDeleted: false, + circleVisibility: userCircleIds, + circleVisibilityByCommon: null, + linkedCommonIds: [], + state: OptimisticFeedItemState.loading, + }, + circleVisibility: userCircleIds, + }), + ); switch (values.proposalType.value) { case ProposalsTypes.FUNDS_ALLOCATION: { const fundingProposalPayload = getFundingProposalPayload( @@ -106,11 +158,25 @@ const NewProposalCreation: FC = (props) => { break; } case ProposalsTypes.SURVEY: { - dispatch( - commonActions.createSurveyProposal.request({ - payload: getSurveyProposalPayload(values, commonId, proposalId, discussionId), - }), + console.log( + "----getSurveyProposalPayload", + getSurveyProposalPayload( + values, + commonId, + proposalId, + discussionId, + ), ); + // dispatch( + // commonActions.createSurveyProposal.request({ + // payload: getSurveyProposalPayload( + // values, + // commonId, + // proposalId, + // discussionId, + // ), + // }), + // ); break; } } diff --git a/src/pages/common/components/FeedItems/FeedItems.tsx b/src/pages/common/components/FeedItems/FeedItems.tsx index 80f514cdac..5e71b1043f 100644 --- a/src/pages/common/components/FeedItems/FeedItems.tsx +++ b/src/pages/common/components/FeedItems/FeedItems.tsx @@ -71,25 +71,24 @@ const FeedItems: FC = (props) => { (pinnedItem) => pinnedItem.feedObjectId === item.feedItem.id, ); - return <>; - // return ( - // - // ); + return ( + + ); })} From ec6d41d554f551bb66af73f6ab12c247230a31fd Mon Sep 17 00:00:00 2001 From: matanfield Date: Mon, 2 Sep 2024 02:14:50 +0300 Subject: [PATCH 12/38] changed width to 18.75rem --- .../layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss | 2 +- .../MultipleSpacesLayout/MultipleSpacesLayout.module.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss b/src/shared/layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss index 4bb72194db..c5b7f52a08 100644 --- a/src/shared/layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss +++ b/src/shared/layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss @@ -5,7 +5,7 @@ --main-mw: 100%; --main-pl: unset; --sb-max-width: unset; - --sb-width: 15rem; + --sb-width: 18.75rem; --sb-content-max-width: 100%; --sb-content-width: 100%; --sb-content-pb: 0; diff --git a/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss b/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss index d6df035e7c..edbb5f0e99 100644 --- a/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss +++ b/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss @@ -5,7 +5,7 @@ --main-mw: calc(120rem + var(--sb-h-indent, 0)); --main-pl: calc(var(--sb-h-indent, 0)); --sb-max-width: unset; - --sb-width: 15rem; + --sb-width: 18.75rem; --sb-content-max-width: 100%; --sb-content-width: 100%; --sb-content-pb: 0; From a4476a6a999c754e2ac9109396d7d8b8d51993b2 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Mon, 2 Sep 2024 15:11:36 +0300 Subject: [PATCH 13/38] CW-Optimistic-streams Full support of proposal optimistic streams Clean up code --- .../ChatComponent/ChatComponent.tsx | 2 +- .../NewDiscussionCreation.tsx | 56 ++------ .../NewProposalCreation.tsx | 83 +++--------- .../common/components/FeedItem/FeedItem.tsx | 12 +- .../LinkStreamModal.module.scss | 70 ---------- .../LinkStreamModal/LinkStreamModal.tsx | 113 ---------------- .../LinkStreamProjects/LinkStreamProjects.tsx | 46 ------- .../NameRightContent.module.scss | 11 -- .../NameRightContent/NameRightContent.tsx | 25 ---- .../components/NameRightContent/index.ts | 1 - .../LinkStreamProjects/components/index.ts | 1 - .../components/LinkStreamProjects/index.ts | 1 - .../LinkStreamModal/components/index.ts | 1 - .../components/LinkStreamModal/index.ts | 1 - .../MoveStreamModal.module.scss | 70 ---------- .../MoveStreamModal/MoveStreamModal.tsx | 110 --------------- .../MoveStreamProjects/MoveStreamProjects.tsx | 42 ------ .../components/MoveStreamProjects/index.ts | 1 - .../MoveStreamModal/components/index.ts | 1 - .../components/MoveStreamModal/index.ts | 1 - .../index.ts | 1 - .../components/Projects/Projects.module.scss | 71 ---------- .../components/Projects/Projects.tsx | 86 ------------ .../components/Projects/hooks/index.ts | 1 - .../Projects/hooks/useProjectsData.ts | 127 ------------------ .../components/Projects/index.ts | 2 - .../UnlinkStreamModal.module.scss | 39 ------ .../UnlinkStreamModal/UnlinkStreamModal.tsx | 116 ---------------- .../components/UnlinkStreamModal/index.ts | 1 - .../components/index.ts | 4 - .../OptimisticDiscussionFeedCard/index.ts | 1 - .../utils/checkIsEditItemAllowed.ts | 13 -- .../utils/checkIsLinkToAllowed.ts | 32 ----- .../utils/checkIsMoveToAllowed.ts | 28 ---- .../utils/checkIsPinUnpinAllowed.ts | 44 ------ .../utils/checkIsRemoveDiscussionAllowed.ts | 38 ------ .../OptimisticFeedCard.tsx} | 16 ++- .../OptimisticFeedCardContent.tsx} | 13 +- .../OptimisticFeedCardContent/index.ts | 1 + .../OptimisticFeedCard/components/index.ts | 1 + .../hooks/index.ts | 0 .../hooks/useMenuItems.tsx | 0 .../components/OptimisticFeedCard/index.ts | 1 + .../utils/getAllowedItems.ts | 0 .../utils/index.ts | 0 src/pages/common/components/index.ts | 2 +- .../hooks/useCases/useCommonFeedItems.ts | 3 +- src/shared/models/CommonFeed.tsx | 1 + .../utils/generateOptimisticFeedItem.ts | 63 +++++++++ src/shared/utils/index.tsx | 1 + src/store/states/common/reducer.ts | 5 +- 51 files changed, 130 insertions(+), 1229 deletions(-) delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.module.scss delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.tsx delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/LinkStreamProjects.tsx delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.module.scss delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.tsx delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.module.scss delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.tsx delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/MoveStreamProjects.tsx delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.module.scss delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.tsx delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/useProjectsData.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.module.scss delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.tsx delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/components/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/index.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsEditItemAllowed.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsLinkToAllowed.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsMoveToAllowed.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsPinUnpinAllowed.ts delete mode 100644 src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsRemoveDiscussionAllowed.ts rename src/pages/common/components/{OptimisticDiscussionFeedCard/OptimisticDiscussionFeedCard.tsx => OptimisticFeedCard/OptimisticFeedCard.tsx} (96%) rename src/pages/common/components/{OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/OptimisticDiscussionFeedCardContent.tsx => OptimisticFeedCard/components/OptimisticFeedCardContent/OptimisticFeedCardContent.tsx} (89%) create mode 100644 src/pages/common/components/OptimisticFeedCard/components/OptimisticFeedCardContent/index.ts create mode 100644 src/pages/common/components/OptimisticFeedCard/components/index.ts rename src/pages/common/components/{OptimisticDiscussionFeedCard => OptimisticFeedCard}/hooks/index.ts (100%) rename src/pages/common/components/{OptimisticDiscussionFeedCard => OptimisticFeedCard}/hooks/useMenuItems.tsx (100%) create mode 100644 src/pages/common/components/OptimisticFeedCard/index.ts rename src/pages/common/components/{OptimisticDiscussionFeedCard => OptimisticFeedCard}/utils/getAllowedItems.ts (100%) rename src/pages/common/components/{OptimisticDiscussionFeedCard => OptimisticFeedCard}/utils/index.ts (100%) create mode 100644 src/shared/utils/generateOptimisticFeedItem.ts diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 4a71557936..197571f67e 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -268,7 +268,7 @@ export default function ChatComponent({ const optimisticFeedItems = useSelector(selectOptimisticFeedItems); - console.log("--optimisticFeedItems", optimisticFeedItems); + // console.log("--optimisticFeedItems", optimisticFeedItems); const optimisticDiscussionMessages = useSelector( selectOptimisticDiscussionMessages, ); diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx index 63098068cc..06d8ca5998 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx @@ -1,9 +1,7 @@ import React, { FC, useCallback, useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { Timestamp as FirestoreTimestamp } from "firebase/firestore"; import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; -import { DiscussionMessageOwnerType } from "@/shared/constants"; import { NewDiscussionCreationFormValues, UploadFile, @@ -15,13 +13,12 @@ import { CommonFeedType, CommonMember, Governance, - OptimisticFeedItemState, } from "@/shared/models"; import { TextEditorValue, parseStringToTextEditorValue, } from "@/shared/ui-kit/TextEditor"; -import { getUserName } from "@/shared/utils"; +import { generateOptimisticFeedItem } from "@/shared/utils"; import { selectDiscussionCreationData, selectIsDiscussionCreationLoading, @@ -123,52 +120,19 @@ const NewDiscussionCreation: FC = (props) => { ); } else { const discussionId = uuidv4(); - const currentDate = FirestoreTimestamp.now(); - const optimisticFeedItemId = uuidv4(); dispatch( - commonActions.setOptimisticFeedItem({ - id: optimisticFeedItemId, - createdAt: currentDate, - updatedAt: currentDate, - isDeleted: false, - userId, - commonId: common.id, - data: { - type: CommonFeedType.OptimisticDiscussion, - id: discussionId, - discussionId: null, - lastMessage: { - userName: getUserName(user), - ownerId: userId, - content: JSON.stringify(values.content), - ownerType: DiscussionMessageOwnerType.User, - }, - hasFiles: false, - hasImages: false, - }, - optimisticData: { - id: discussionId, - title: values.title, - message: JSON.stringify(values.content), - ownerId: userId, + commonActions.setOptimisticFeedItem( + generateOptimisticFeedItem({ + userId, commonId: common.id, - lastMessage: currentDate, - updatedAt: currentDate, - createdAt: currentDate, - messageCount: 0, - followers: [], - files: [], - images: [], - discussionMessages: [], - isDeleted: false, + type: CommonFeedType.OptimisticDiscussion, circleVisibility, - circleVisibilityByCommon: null, - linkedCommonIds: [], - state: OptimisticFeedItemState.loading, - }, - circleVisibility, - }), + discussionId, + title: values.title, + content: JSON.stringify(values.content), + }), + ), ); dispatch( diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx index 14a887527f..40ca182df1 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx @@ -1,10 +1,8 @@ import React, { FC, ReactNode, useCallback, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { Timestamp as FirestoreTimestamp } from "firebase/firestore"; import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { - DiscussionMessageOwnerType, PROPOSAL_TYPE_SELECT_OPTIONS, ProposalsTypes, } from "@/shared/constants"; @@ -15,10 +13,9 @@ import { CommonFeedType, CommonMember, Governance, - OptimisticFeedItemState, } from "@/shared/models"; import { parseStringToTextEditorValue } from "@/shared/ui-kit/TextEditor"; -import { getUserName } from "@/shared/utils"; +import { generateOptimisticFeedItem } from "@/shared/utils"; import { selectIsProposalCreationLoading, selectProposalCreationData, @@ -90,52 +87,19 @@ const NewProposalCreation: FC = (props) => { const proposalId = uuidv4(); const discussionId = uuidv4(); - const currentDate = FirestoreTimestamp.now(); - const optimisticFeedItemId = uuidv4(); dispatch( - commonActions.setOptimisticFeedItem({ - id: optimisticFeedItemId, - createdAt: currentDate, - updatedAt: currentDate, - isDeleted: false, - userId, - commonId: common.id, - data: { - type: CommonFeedType.OptimisticDiscussion, - id: discussionId, - discussionId: null, - lastMessage: { - userName: getUserName(user), - ownerId: userId, - content: JSON.stringify(values.content), - ownerType: DiscussionMessageOwnerType.User, - }, - hasFiles: false, - hasImages: false, - }, - optimisticData: { - id: discussionId, - title: values.title, - message: JSON.stringify(values.content), - ownerId: userId, + commonActions.setOptimisticFeedItem( + generateOptimisticFeedItem({ + userId, commonId: common.id, - lastMessage: currentDate, - updatedAt: currentDate, - createdAt: currentDate, - messageCount: 0, - followers: [], - files: [], - images: [], - discussionMessages: [], - isDeleted: false, + type: CommonFeedType.OptimisticProposal, circleVisibility: userCircleIds, - circleVisibilityByCommon: null, - linkedCommonIds: [], - state: OptimisticFeedItemState.loading, - }, - circleVisibility: userCircleIds, - }), + discussionId, + title: values.title, + content: JSON.stringify(values.content), + }), + ), ); switch (values.proposalType.value) { case ProposalsTypes.FUNDS_ALLOCATION: { @@ -158,25 +122,16 @@ const NewProposalCreation: FC = (props) => { break; } case ProposalsTypes.SURVEY: { - console.log( - "----getSurveyProposalPayload", - getSurveyProposalPayload( - values, - commonId, - proposalId, - discussionId, - ), + dispatch( + commonActions.createSurveyProposal.request({ + payload: getSurveyProposalPayload( + values, + commonId, + proposalId, + discussionId, + ), + }), ); - // dispatch( - // commonActions.createSurveyProposal.request({ - // payload: getSurveyProposalPayload( - // values, - // commonId, - // proposalId, - // discussionId, - // ), - // }), - // ); break; } } diff --git a/src/pages/common/components/FeedItem/FeedItem.tsx b/src/pages/common/components/FeedItem/FeedItem.tsx index b141051a28..287e405292 100644 --- a/src/pages/common/components/FeedItem/FeedItem.tsx +++ b/src/pages/common/components/FeedItem/FeedItem.tsx @@ -20,7 +20,7 @@ import { import { checkIsItemVisibleForUser } from "@/shared/utils"; import { useFeedItemSubscription } from "../../hooks"; import { DiscussionFeedCard } from "../DiscussionFeedCard"; -import { OptimisticDiscussionFeedCard } from "../OptimisticDiscussionFeedCard"; +import { OptimisticFeedCard } from "../OptimisticFeedCard"; import { ProposalFeedCard } from "../ProposalFeedCard"; import { ProjectFeedItem } from "./components"; import { useFeedItemContext } from "./context"; @@ -145,7 +145,6 @@ const FeedItem = forwardRef((props, ref) => { onFeedItemUnfollowed, ]); - const generalProps = useMemo( () => ({ ref, @@ -207,7 +206,6 @@ const FeedItem = forwardRef((props, ref) => { ], ); - if ( shouldCheckItemVisibility && !checkIsItemVisibleForUser({ @@ -221,11 +219,15 @@ const FeedItem = forwardRef((props, ref) => { return null; } - if (item.data.type === CommonFeedType.OptimisticDiscussion) { + if ( + item.data.type === CommonFeedType.OptimisticDiscussion || + item.data.type === CommonFeedType.OptimisticProposal + ) { return ( - ); } diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.module.scss b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.module.scss deleted file mode 100644 index b99c0e1f41..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.module.scss +++ /dev/null @@ -1,70 +0,0 @@ -@import "../../../../../../constants"; -@import "../../../../../../styles/sizes"; - -.modal { - max-width: 31.875rem; - width: 100%; - max-height: 33.75rem; - min-height: 24rem; - border-radius: 0; - box-shadow: 0 0.25rem 0.9375rem var(--drop-shadow); - - :global(.modal__header-wrapper--with-modal-padding) { - .modalHeader { - justify-content: flex-start; - } - - .modalTitle { - margin: 0; - font-family: PoppinsSans, sans-serif; - font-weight: 600; - font-size: 1.25rem; - color: var(--primary-text); - text-align: left; - word-break: break-word; - } - } - - .modalContent { - width: 100%; - display: flex; - flex-direction: column; - overflow: hidden; - box-sizing: border-box; - } - - .modalCloseWrapper { - top: 1.7rem; - margin: 0; - - @include tablet { - top: 1.1rem; - } - } - - @include tablet { - max-width: unset; - max-height: unset; - } -} - -.submitButtonWrapper { - margin-top: auto; - padding-top: 1.5rem; - display: flex; - justify-content: flex-end; -} - -.submitButton { - --btn-w: 100%; - - max-width: 9.75rem; - - @include tablet { - max-width: 100%; - } -} - -.loader { - margin: 0 auto; -} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.tsx deleted file mode 100644 index b3d56b1a68..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/LinkStreamModal.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { FC, ReactElement, useEffect, useState } from "react"; -import { useSelector } from "react-redux"; -import { selectUser } from "@/pages/Auth/store/selectors"; -import { Modal } from "@/shared/components"; -import { useNotification } from "@/shared/hooks"; -import { useStreamLinking } from "@/shared/hooks/useCases"; -import { Button, ButtonVariant, Loader } from "@/shared/ui-kit"; -import { emptyFunction } from "@/shared/utils"; -import { LinkStreamProjects } from "./components"; -import styles from "./LinkStreamModal.module.scss"; - -interface LinkStreamModalProps { - isOpen: boolean; - onClose: () => void; - feedItemId: string; - title: string; - rootCommonId: string; - commonId: string; - originalCommonId: string; - linkedCommonIds?: string[]; - circleVisibility: string[]; -} - -const LinkStreamModal: FC = (props) => { - const { - isOpen, - onClose, - feedItemId, - title, - rootCommonId, - commonId, - originalCommonId, - linkedCommonIds = [], - circleVisibility, - } = props; - const { notify } = useNotification(); - const { isStreamLinking, isStreamLinked, linkStream } = useStreamLinking(); - const [activeItemId, setActiveItemId] = useState(""); - const user = useSelector(selectUser()); - const userId = user?.uid; - - const handleSubmit = () => { - if (!userId) { - return; - } - - linkStream({ - userId, - feedObjectId: feedItemId, - sourceCommonId: commonId, - targetCommonId: activeItemId, - }); - }; - - const renderContent = (): ReactElement => { - if (isStreamLinking) { - return ; - } - - return ( - <> - -
- -
- - ); - }; - - useEffect(() => { - if (isStreamLinked) { - notify("Stream is successfully linked"); - onClose(); - } - }, [isStreamLinking, isStreamLinked]); - - return ( - - {renderContent()} - - ); -}; - -export default LinkStreamModal; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/LinkStreamProjects.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/LinkStreamProjects.tsx deleted file mode 100644 index bdc8828423..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/LinkStreamProjects.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { FC, useCallback } from "react"; -import { Item } from "@/shared/layouts/SidenavLayout/components/SidenavContent/components"; -import { ProjectsStateItem } from "@/store/states"; -import { Projects, ProjectsProps } from "../../../Projects"; -import { NameRightContent } from "./components"; - -interface LinkStreamProjects - extends Omit { - originalCommonId: string; - linkedCommonIds: string[]; - circleVisibility: string[]; -} - -const LinkStreamProjects: FC = (props) => { - const { rootCommonId, originalCommonId, linkedCommonIds, circleVisibility } = - props; - - const getAdditionalItemData = useCallback( - (projectsStateItem: ProjectsStateItem): Partial => { - const isAllowedToLink = - circleVisibility.length === 0 || - projectsStateItem.commonId === rootCommonId || - projectsStateItem.rootCommonId === rootCommonId; - - return { - disabled: - !isAllowedToLink || - !projectsStateItem.hasPermissionToLinkToHere || - projectsStateItem.commonId === originalCommonId || - linkedCommonIds.includes(projectsStateItem.commonId), - nameRightContent: ( - - ), - }; - }, - [originalCommonId, linkedCommonIds, rootCommonId, circleVisibility], - ); - - return ; -}; - -export default LinkStreamProjects; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.module.scss b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.module.scss deleted file mode 100644 index e54f537a46..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.module.scss +++ /dev/null @@ -1,11 +0,0 @@ -.originalText { - margin-left: 0.5rem; - font-weight: normal; - font-size: 0.875rem; - font-style: italic; -} - -.linkIcon { - flex-shrink: 0; - margin-left: 0.5rem; -} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.tsx deleted file mode 100644 index 9d834a9512..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/NameRightContent.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { FC } from "react"; -import { Link4Icon } from "@/shared/icons"; -import { ProjectsStateItem } from "@/store/states"; -import styles from "./NameRightContent.module.scss"; - -interface NameRightContentProps { - projectsStateItem: ProjectsStateItem; - originalCommonId: string; - linkedCommonIds?: string[]; -} - -const NameRightContent: FC = (props) => { - const { projectsStateItem, originalCommonId, linkedCommonIds = [] } = props; - - if (projectsStateItem.commonId === originalCommonId) { - return original; - } - if (linkedCommonIds.includes(projectsStateItem.commonId)) { - return ; - } - - return null; -}; - -export default NameRightContent; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/index.ts deleted file mode 100644 index 3c341bfce7..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/NameRightContent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as NameRightContent } from "./NameRightContent"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/index.ts deleted file mode 100644 index f5705eab2d..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./NameRightContent"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/index.ts deleted file mode 100644 index f6931ce7fb..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/LinkStreamProjects/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as LinkStreamProjects } from "./LinkStreamProjects"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/index.ts deleted file mode 100644 index 159661f715..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./LinkStreamProjects"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/index.ts deleted file mode 100644 index 79c9f12e5d..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/LinkStreamModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as LinkStreamModal } from "./LinkStreamModal"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.module.scss b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.module.scss deleted file mode 100644 index b99c0e1f41..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.module.scss +++ /dev/null @@ -1,70 +0,0 @@ -@import "../../../../../../constants"; -@import "../../../../../../styles/sizes"; - -.modal { - max-width: 31.875rem; - width: 100%; - max-height: 33.75rem; - min-height: 24rem; - border-radius: 0; - box-shadow: 0 0.25rem 0.9375rem var(--drop-shadow); - - :global(.modal__header-wrapper--with-modal-padding) { - .modalHeader { - justify-content: flex-start; - } - - .modalTitle { - margin: 0; - font-family: PoppinsSans, sans-serif; - font-weight: 600; - font-size: 1.25rem; - color: var(--primary-text); - text-align: left; - word-break: break-word; - } - } - - .modalContent { - width: 100%; - display: flex; - flex-direction: column; - overflow: hidden; - box-sizing: border-box; - } - - .modalCloseWrapper { - top: 1.7rem; - margin: 0; - - @include tablet { - top: 1.1rem; - } - } - - @include tablet { - max-width: unset; - max-height: unset; - } -} - -.submitButtonWrapper { - margin-top: auto; - padding-top: 1.5rem; - display: flex; - justify-content: flex-end; -} - -.submitButton { - --btn-w: 100%; - - max-width: 9.75rem; - - @include tablet { - max-width: 100%; - } -} - -.loader { - margin: 0 auto; -} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.tsx deleted file mode 100644 index 46dc541967..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/MoveStreamModal.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { FC, ReactElement, useEffect, useState } from "react"; -import { useSelector } from "react-redux"; -import { selectUser } from "@/pages/Auth/store/selectors"; -import { Modal } from "@/shared/components"; -import { useNotification } from "@/shared/hooks"; -import { useStreamMoving } from "@/shared/hooks/useCases"; -import { Button, ButtonVariant, Loader } from "@/shared/ui-kit"; -import { emptyFunction } from "@/shared/utils"; -import { MoveStreamProjects } from "./components"; -import styles from "./MoveStreamModal.module.scss"; - -interface MoveStreamModalProps { - isOpen: boolean; - onClose: () => void; - feedItemId: string; - title: string; - rootCommonId: string; - commonId: string; - originalCommonId: string; - circleVisibility: string[]; -} - -const MoveStreamModal: FC = (props) => { - const { - isOpen, - onClose, - feedItemId, - title, - rootCommonId, - commonId, - originalCommonId, - circleVisibility, - } = props; - const { notify } = useNotification(); - const { isStreamMoving, isStreamMoved, moveStream } = useStreamMoving(); - const [activeItemId, setActiveItemId] = useState(""); - const user = useSelector(selectUser()); - const userId = user?.uid; - - const handleSubmit = () => { - if (!userId) { - return; - } - - moveStream({ - userId, - feedObjectId: feedItemId, - sourceCommonId: commonId, - targetCommonId: activeItemId, - }); - }; - - const renderContent = (): ReactElement => { - if (isStreamMoving) { - return ; - } - - return ( - <> - -
- -
- - ); - }; - - useEffect(() => { - if (isStreamMoved) { - notify("Stream is successfully moved"); - onClose(); - } - }, [isStreamMoving, isStreamMoved]); - - return ( - - {renderContent()} - - ); -}; - -export default MoveStreamModal; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/MoveStreamProjects.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/MoveStreamProjects.tsx deleted file mode 100644 index 78e25f4148..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/MoveStreamProjects.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, { FC, useCallback } from "react"; -import { Item } from "@/shared/layouts/SidenavLayout/components/SidenavContent/components"; -import { ProjectsStateItem } from "@/store/states"; -import { NameRightContent } from "../../../LinkStreamModal/components/LinkStreamProjects/components"; -import { Projects, ProjectsProps } from "../../../Projects"; - -interface MoveStreamProjectsProps - extends Omit { - originalCommonId: string; - circleVisibility: string[]; -} - -const MoveStreamProjects: FC = (props) => { - const { rootCommonId, originalCommonId, circleVisibility } = props; - - const getAdditionalItemData = useCallback( - (projectsStateItem: ProjectsStateItem): Partial => { - const isAllowedToMove = - circleVisibility.length === 0 || - projectsStateItem.commonId === rootCommonId || - projectsStateItem.rootCommonId === rootCommonId; - - return { - disabled: - !isAllowedToMove || - !projectsStateItem.hasPermissionToMoveToHere || - projectsStateItem.commonId === originalCommonId, - nameRightContent: ( - - ), - }; - }, - [originalCommonId, rootCommonId, circleVisibility], - ); - - return ; -}; - -export default MoveStreamProjects; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/index.ts deleted file mode 100644 index cd9ba5fc65..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/MoveStreamProjects/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as MoveStreamProjects } from "./MoveStreamProjects"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/index.ts deleted file mode 100644 index 35fef4926d..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./MoveStreamProjects"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/index.ts deleted file mode 100644 index 219e936dd5..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/MoveStreamModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as MoveStreamModal } from "./MoveStreamModal"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/index.ts deleted file mode 100644 index c38bf83825..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./OptimisticDiscussionFeedCardContent"; \ No newline at end of file diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.module.scss b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.module.scss deleted file mode 100644 index 9776f0251b..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.module.scss +++ /dev/null @@ -1,71 +0,0 @@ -@import "../../../../../../constants"; -@import "../../../../../../styles/sizes"; - -.projectsTree { - overflow-y: auto; - box-sizing: border-box; -} - -.projectsTreeItemTriggerClassName { - --item-pl-per-level: 1.25rem; - --item-arrow-pl: 0.5rem; - - height: 3rem; - border-radius: 0; - - &:hover { - --bg-color: var(--hover-fill); - --item-text-color: var(--primary-text); - } - - @media (hover: none) { - &:hover { - --bg-color: var(--primary-background); - --item-text-color: var(--primary-text); - } - } -} -.projectsTreeItemTriggerActiveClassName { - --bg-color: var(--primary-fill); - --item-text-color: #{$c-shades-white}; - - &:hover { - --bg-color: var(--primary-fill); - } - - @media (hover: none) { - &:hover { - --bg-color: var(--primary-fill); - --item-text-color: #{$c-shades-white}; - } - } -} - -.projectsTreeItemTriggerNameClassName { - font-family: PoppinsSans, sans-serif; - font-weight: 500; -} - -.projectsTreeItemTriggerImageClassName { - width: 1.5rem; - height: 1.5rem; - margin-right: 0.875rem; -} -.projectsTreeItemTriggerImageNonRoundedClassName { - border-radius: 0.375rem; -} - -.loader { - margin: 1rem auto 0; - display: block; -} - -.createCommonButton { - width: 100%; - padding-left: 2.125rem; - padding-right: 0.875rem; -} - -.commonsMenuClassName { - max-height: 15rem; -} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.tsx deleted file mode 100644 index ef60d43e99..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/Projects.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { FC, ReactNode, useEffect, useMemo, useState } from "react"; -import { LOADER_APPEARANCE_DELAY } from "@/shared/constants"; -import { TreeItemTriggerStyles } from "@/shared/layouts"; -import { ProjectsTree } from "@/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/ProjectsTree"; -import { Item } from "@/shared/layouts/SidenavLayout/components/SidenavContent/components"; -import { Loader } from "@/shared/ui-kit"; -import { ProjectsStateItem } from "@/store/states"; -import { useProjectsData } from "./hooks"; -import styles from "./Projects.module.scss"; - -export interface ProjectsProps { - rootCommonId: string; - commonId: string; - activeItemId: string; - onActiveItemId: (activeItemId: string) => void; - renderNoItemsInfo?: () => ReactNode; - getAdditionalItemData?: ( - projectsStateItem: ProjectsStateItem, - ) => Partial; -} - -const Projects: FC = (props) => { - const { - activeItemId, - onActiveItemId, - renderNoItemsInfo, - getAdditionalItemData, - } = props; - const [currentCommonId, setCurrentCommonId] = useState(props.rootCommonId); - const { - parentItem, - areCommonsLoading, - areProjectsLoading, - commons, - items, - activeItem, - parentItemIds, - } = useProjectsData({ - currentCommonId, - activeItemId, - getAdditionalItemData, - }); - const treeItemTriggerStyles = useMemo( - () => ({ - container: styles.projectsTreeItemTriggerClassName, - containerActive: styles.projectsTreeItemTriggerActiveClassName, - name: styles.projectsTreeItemTriggerNameClassName, - image: styles.projectsTreeItemTriggerImageClassName, - imageNonRounded: styles.projectsTreeItemTriggerImageNonRoundedClassName, - }), - [], - ); - - useEffect(() => { - onActiveItemId(""); - }, [currentCommonId]); - - if (!parentItem) { - return areCommonsLoading ? ( - - ) : ( - <>{renderNoItemsInfo?.() || null} - ); - } - - return ( - - ); -}; - -export default Projects; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/index.ts deleted file mode 100644 index ced5d7bb9c..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./useProjectsData"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/useProjectsData.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/useProjectsData.ts deleted file mode 100644 index 1bb8ed7c8e..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/hooks/useProjectsData.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { useEffect, useMemo, useRef } from "react"; -import { useSelector } from "react-redux"; -import { selectUser } from "@/pages/Auth/store/selectors"; -import { useLoadingState } from "@/shared/hooks"; -import { - generateProjectsTreeItems, - getItemById, - getItemFromProjectsStateItem, - getParentItemIds, - Item, -} from "@/shared/layouts/SidenavLayout/components/SidenavContent/components"; -import { - ProjectsStateItem, - selectCommonLayoutCommonsState, -} from "@/store/states"; -import { getProjects as getProjectsUtil } from "@/store/states/commonLayout/saga/utils"; - -interface ProjectsInfo { - currentCommonId: string; - activeItemId: string; - getAdditionalItemData?: ( - projectsStateItem: ProjectsStateItem, - ) => Partial; -} - -interface Return { - parentItem: Item | null; - areCommonsLoading: boolean; - areProjectsLoading: boolean; - commons: ProjectsStateItem[]; - items: Item[]; - activeItem: Item | null; - parentItemIds: string[]; -} - -const generateItemCommonPagePath = () => ""; - -export const useProjectsData = (projectsInfo: ProjectsInfo): Return => { - const { currentCommonId, activeItemId, getAdditionalItemData } = projectsInfo; - const currentCommonIdRef = useRef(currentCommonId); - currentCommonIdRef.current = currentCommonId; - const { commons, areCommonsLoading } = useSelector( - selectCommonLayoutCommonsState, - ); - const user = useSelector(selectUser()); - const userId = user?.uid; - const [{ data: projects, loading: areProjectsLoading }, setProjectsState] = - useLoadingState([]); - const currentCommon = commons.find( - ({ commonId }) => commonId === currentCommonId, - ); - - const parentItem = useMemo( - () => - currentCommon - ? getItemFromProjectsStateItem( - currentCommon, - generateItemCommonPagePath, - undefined, - getAdditionalItemData, - ) - : null, - [currentCommon, getAdditionalItemData], - ); - const items = useMemo(() => { - const [item] = generateProjectsTreeItems( - currentCommon ? projects.concat(currentCommon) : projects, - generateItemCommonPagePath, - getAdditionalItemData, - ); - - return item?.items || []; - }, [currentCommon, projects, getAdditionalItemData]); - const activeItem = getItemById( - activeItemId, - parentItem ? [parentItem, ...items] : items, - ); - const parentItemIds = getParentItemIds( - activeItemId, - currentCommon ? projects.concat(currentCommon) : projects, - ); - - useEffect(() => { - let isRelevantLoading = true; - - (async () => { - try { - setProjectsState({ - data: [], - loading: true, - fetched: false, - }); - const projectsData = await getProjectsUtil(currentCommonId, userId); - - if (isRelevantLoading) { - setProjectsState({ - data: projectsData, - loading: false, - fetched: true, - }); - } - } catch (err) { - if (isRelevantLoading) { - setProjectsState({ - data: [], - loading: false, - fetched: true, - }); - } - } - })(); - - return () => { - isRelevantLoading = false; - }; - }, [currentCommonId]); - - return { - parentItem, - areCommonsLoading, - areProjectsLoading, - commons, - items, - activeItem, - parentItemIds, - }; -}; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/index.ts deleted file mode 100644 index 6ca2075a54..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/Projects/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Projects } from "./Projects"; -export type { ProjectsProps } from "./Projects"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.module.scss b/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.module.scss deleted file mode 100644 index 3cf05ba9e8..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.module.scss +++ /dev/null @@ -1,39 +0,0 @@ -@import "../../../../../../constants"; -@import "../../../../../../styles/sizes"; - -.modal { - max-width: 31.875rem; -} - -.buttonsWrapper { - display: flex; - margin-top: 1rem; - justify-content: flex-end; - - @include tablet { - flex-direction: column; - margin-top: 100%; - } -} - -.button { - width: 10rem; - - @include tablet { - width: 100%; - } -} - -.unlinkButton { - margin-left: 1.5rem; - - @include tablet { - margin-left: unset; - margin-top: 1rem; - } -} - -.errorText { - font-size: $xsmall; - color: $red; -} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.tsx b/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.tsx deleted file mode 100644 index 76f0886970..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/UnlinkStreamModal.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React, { FC, useRef, useState } from "react"; -import { useSelector } from "react-redux"; -import { CancelTokenSource } from "axios"; -import classNames from "classnames"; -import { isError } from "lodash"; -import { selectUser } from "@/pages/Auth/store/selectors"; -import { - CommonFeedService, - Logger, - getCancelTokenSource, - isRequestCancelled, -} from "@/services"; -import { Modal } from "@/shared/components"; -import { useNotification } from "@/shared/hooks"; -import { Button, ButtonVariant } from "@/shared/ui-kit"; -import { emptyFunction } from "@/shared/utils"; -import styles from "./UnlinkStreamModal.module.scss"; - -interface UnlinkStreamModalProps { - isOpen: boolean; - onClose: () => void; - feedItemId: string; - title: string; - commonId: string; - commonName: string; -} - -const UnlinkStreamModal: FC = (props) => { - const { isOpen, onClose, feedItemId, title, commonId, commonName } = props; - const { notify } = useNotification(); - const [isUnlinking, setIsUnlinking] = useState(false); - const [errorText, setErrorText] = useState(""); - const cancelTokenRef = useRef(null); - const user = useSelector(selectUser()); - const userId = user?.uid; - - const handleSubmit = async () => { - if (!userId) { - return; - } - - setErrorText(""); - setIsUnlinking(true); - - try { - if (cancelTokenRef.current) { - cancelTokenRef.current.cancel(); - } - - cancelTokenRef.current = getCancelTokenSource(); - - await CommonFeedService.unlinkStream( - { - feedObjectId: feedItemId, - commonId: commonId, - userId: userId, - }, - { - cancelToken: cancelTokenRef.current.token, - }, - ); - - cancelTokenRef.current = null; - - notify("Stream is successfully unlinked"); - setIsUnlinking(false); - onClose(); - } catch (error) { - if (!isRequestCancelled(error)) { - Logger.error(error); - cancelTokenRef.current = null; - } - setIsUnlinking(false); - setErrorText(isError(error) ? error.message : "Something went wrong..."); - } - }; - - return ( - -
- This stream is linked in multiple spaces.
- If you proceed, it will be removed from "{commonName}" only and will - remain in other locations. Do you want to continue? -
- - -
- {errorText && {errorText}} -
-
- ); -}; - -export default UnlinkStreamModal; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/index.ts deleted file mode 100644 index a8902e6f40..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/UnlinkStreamModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as UnlinkStreamModal } from "./UnlinkStreamModal"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/components/index.ts deleted file mode 100644 index 605eb3dc70..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./LinkStreamModal"; -export * from "./MoveStreamModal"; -export * from "./UnlinkStreamModal"; -export * from "./OptimisticDiscussionFeedCardContent"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/index.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/index.ts deleted file mode 100644 index e3e2245f56..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as OptimisticDiscussionFeedCard } from "./OptimisticDiscussionFeedCard"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsEditItemAllowed.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsEditItemAllowed.ts deleted file mode 100644 index 085874a155..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsEditItemAllowed.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { CommonFeedType, PredefinedTypes } from "@/shared/models"; -import { GetAllowedItemsOptions } from "../../FeedItem"; - -export function checkIsEditItemAllowed(options: GetAllowedItemsOptions) { - if (!options.commonMember) return false; - if (options.discussion?.predefinedType === PredefinedTypes.General) - return false; - - return ( - options.commonMember.userId === options.discussion?.ownerId && - options.feedItem?.data.type === CommonFeedType.Discussion - ); -} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsLinkToAllowed.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsLinkToAllowed.ts deleted file mode 100644 index 7bc6b04502..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsLinkToAllowed.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { GovernanceActions } from "@/shared/constants"; -import { getCirclesWithLowestTier, hasPermission } from "@/shared/utils"; -import { GetAllowedItemsOptions } from "../../FeedItem"; - -export const checkIsLinkToAllowed = ( - options: GetAllowedItemsOptions, -): boolean => { - if (!options.commonMember) { - return false; - } - - const circlesWithLowestTier = getCirclesWithLowestTier( - Object.values(options.governanceCircles || {}), - ); - const discussionCircleVisibility = - options.discussion?.circleVisibilityByCommon?.[options.commonId || ""] || - []; - - return ( - (discussionCircleVisibility.length === 0 || - discussionCircleVisibility.some((circleId) => - circlesWithLowestTier.some((circle) => circle.id === circleId), - )) && - hasPermission({ - commonMember: options.commonMember, - governance: { - circles: options.governanceCircles || {}, - }, - action: GovernanceActions.LINK_FROM_HERE, - }) - ); -}; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsMoveToAllowed.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsMoveToAllowed.ts deleted file mode 100644 index 1175daaeeb..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsMoveToAllowed.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { GovernanceActions } from "@/shared/constants"; -import { PredefinedTypes } from "@/shared/models"; -import { hasPermission } from "@/shared/utils"; -import { GetAllowedItemsOptions } from "../../FeedItem"; - -export const checkIsMoveToAllowed = ( - options: GetAllowedItemsOptions, -): boolean => { - if ( - !options.commonMember || - options.discussion?.predefinedType === PredefinedTypes.General - ) { - return false; - } - - const linkedCommonIds = options.discussion?.linkedCommonIds || []; - - return ( - linkedCommonIds.length === 0 && - hasPermission({ - commonMember: options.commonMember, - governance: { - circles: options.governanceCircles || {}, - }, - action: GovernanceActions.MOVE_FROM_HERE, - }) - ); -}; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsPinUnpinAllowed.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsPinUnpinAllowed.ts deleted file mode 100644 index ee5d572477..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsPinUnpinAllowed.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { GovernanceActions } from "@/shared/constants"; -import { PredefinedTypes } from "@/shared/models"; -import { hasPermission } from "@/shared/utils"; -import { FeedItemPinAction, GetAllowedItemsOptions } from "../../FeedItem"; - -const PINNED_ITEMS_LIMIT = 7; - -export function checkIsPinUnpinAllowed( - action: FeedItemPinAction, - options: GetAllowedItemsOptions, -) { - const { feedItem, commonMember, discussion, pinnedFeedItems = [] } = options; - const isDiscussionPinned = pinnedFeedItems.some( - (pinnedFeedItem) => pinnedFeedItem.feedObjectId === feedItem?.id, - ); - - if (!commonMember) { - return false; - } - - if (action === FeedItemPinAction.Pin) { - const hasReachedPinLimit = pinnedFeedItems.length >= PINNED_ITEMS_LIMIT; - - if (isDiscussionPinned || hasReachedPinLimit) { - return false; - } - } else if ( - action === FeedItemPinAction.Unpin && - (!isDiscussionPinned || - discussion?.predefinedType === PredefinedTypes.General) - ) { - return false; - } - - const isAllowed = hasPermission({ - commonMember, - governance: { - circles: options.governanceCircles || {}, - }, - action: GovernanceActions.PIN_OR_UNPIN_FEED_ITEMS, - }); - - return isAllowed; -} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsRemoveDiscussionAllowed.ts b/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsRemoveDiscussionAllowed.ts deleted file mode 100644 index 4977af6973..0000000000 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/checkIsRemoveDiscussionAllowed.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { GovernanceActions } from "@/shared/constants"; -import { PredefinedTypes } from "@/shared/models"; -import { getCirclesWithHighestTier, hasPermission } from "@/shared/utils"; -import { GetAllowedItemsOptions } from "../../FeedItem"; - -export function checkIsRemoveDiscussionAllowed( - options: GetAllowedItemsOptions, -): boolean { - const { commonMember } = options; - - if ( - !commonMember || - options.discussion?.predefinedType === PredefinedTypes.General - ) { - return false; - } - - const circles = options.governanceCircles || {}; - const isDiscussionOwner = commonMember.userId === options.discussion?.ownerId; - const hasPermissionToRemoveDiscussion = - hasPermission({ - commonMember, - governance: { circles }, - action: GovernanceActions.HIDE_OR_UNHIDE_DISCUSSION, - }) || isDiscussionOwner; - - if (!options.discussion?.proposalId) { - return hasPermissionToRemoveDiscussion; - } - - const circlesWithHighestTier = getCirclesWithHighestTier( - Object.values(circles), - ); - - return circlesWithHighestTier.some((circle) => - commonMember.circleIds.includes(circle.id), - ); -} diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/OptimisticDiscussionFeedCard.tsx b/src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx similarity index 96% rename from src/pages/common/components/OptimisticDiscussionFeedCard/OptimisticDiscussionFeedCard.tsx rename to src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx index 9b774f85c3..f0128b4fb3 100644 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/OptimisticDiscussionFeedCard.tsx +++ b/src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx @@ -21,6 +21,7 @@ import { FeedLayoutItemChangeData } from "@/shared/interfaces"; import { Common, CommonFeed, + CommonFeedType, CommonMember, CommonNotion, DirectParent, @@ -36,9 +37,9 @@ import { GetLastMessageOptions, GetNonAllowedItemsOptions, } from "../FeedItem"; -import { OptimisticDiscussionFeedCardContent } from "./components"; +import { OptimisticFeedCardContent } from "./components"; -interface OptimisticDiscussionFeedCardProps { +interface OptimisticFeedCardProps { item: CommonFeed; governanceCircles?: Governance["circles"]; isMobileVersion?: boolean; @@ -65,11 +66,12 @@ interface OptimisticDiscussionFeedCardProps { onUserClick?: (userId: string) => void; onFeedItemClick: (feedItemId: string) => void; onInternalLinkClick: (data: InternalLinkData) => void; + type: CommonFeedType; } -const OptimisticDiscussionFeedCard = forwardRef< +const OptimisticFeedCard = forwardRef< FeedItemRef, - OptimisticDiscussionFeedCardProps + OptimisticFeedCardProps >((props, ref) => { const { setChatItem, @@ -98,6 +100,7 @@ const OptimisticDiscussionFeedCard = forwardRef< onUserClick, onFeedItemClick, onInternalLinkClick, + type, } = props; const { fetchUser: fetchDiscussionCreator, data: discussionCreator } = @@ -299,7 +302,7 @@ const OptimisticDiscussionFeedCard = forwardRef< linkedCommonIds={[]} > {(isExpanded || isActive) && ( - )} @@ -323,4 +327,4 @@ const OptimisticDiscussionFeedCard = forwardRef< ); }); -export default OptimisticDiscussionFeedCard; +export default OptimisticFeedCard; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/OptimisticDiscussionFeedCardContent.tsx b/src/pages/common/components/OptimisticFeedCard/components/OptimisticFeedCardContent/OptimisticFeedCardContent.tsx similarity index 89% rename from src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/OptimisticDiscussionFeedCardContent.tsx rename to src/pages/common/components/OptimisticFeedCard/components/OptimisticFeedCardContent/OptimisticFeedCardContent.tsx index 8bab55824e..8bd1e086a7 100644 --- a/src/pages/common/components/OptimisticDiscussionFeedCard/components/OptimisticDiscussionFeedCardContent/OptimisticDiscussionFeedCardContent.tsx +++ b/src/pages/common/components/OptimisticFeedCard/components/OptimisticFeedCardContent/OptimisticFeedCardContent.tsx @@ -3,6 +3,7 @@ import { ContextMenuItem } from "@/shared/interfaces"; import { Common, CommonFeed, + CommonFeedType, DirectParent, DiscussionNotion, Governance, @@ -17,7 +18,7 @@ import { getVisibilityString, } from "../../../FeedCard"; -interface OptimisticDiscussionFeedCardContentProps { +interface OptimisticFeedCardContentProps { item: CommonFeed; governanceCircles?: Governance["circles"]; isMobileVersion?: boolean; @@ -34,10 +35,11 @@ interface OptimisticDiscussionFeedCardContentProps { handleOpenChat: () => void; onHover: (isMouseEnter: boolean) => void; isLoading: boolean; + type?: CommonFeedType; } -export function OptimisticDiscussionFeedCardContent( - props: OptimisticDiscussionFeedCardContentProps, +export function OptimisticFeedCardContent( + props: OptimisticFeedCardContentProps, ) { const { item, @@ -56,6 +58,7 @@ export function OptimisticDiscussionFeedCardContent( isLoading, discussionMessage, discussionImages, + type, } = props; if (isLoading || !commonId) { @@ -80,7 +83,9 @@ export function OptimisticDiscussionFeedCardContent( /> } - type={isHome ? "Home" : "Discussion"} + type={ + type === CommonFeedType.OptimisticProposal ? "Proposal" : "Discussion" + } circleVisibility={circleVisibility} menuItems={menuItems} isMobileVersion={isMobileVersion} diff --git a/src/pages/common/components/OptimisticFeedCard/components/OptimisticFeedCardContent/index.ts b/src/pages/common/components/OptimisticFeedCard/components/OptimisticFeedCardContent/index.ts new file mode 100644 index 0000000000..45174ea033 --- /dev/null +++ b/src/pages/common/components/OptimisticFeedCard/components/OptimisticFeedCardContent/index.ts @@ -0,0 +1 @@ +export * from "./OptimisticFeedCardContent"; \ No newline at end of file diff --git a/src/pages/common/components/OptimisticFeedCard/components/index.ts b/src/pages/common/components/OptimisticFeedCard/components/index.ts new file mode 100644 index 0000000000..b2b3ef2850 --- /dev/null +++ b/src/pages/common/components/OptimisticFeedCard/components/index.ts @@ -0,0 +1 @@ +export * from "./OptimisticFeedCardContent"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/hooks/index.ts b/src/pages/common/components/OptimisticFeedCard/hooks/index.ts similarity index 100% rename from src/pages/common/components/OptimisticDiscussionFeedCard/hooks/index.ts rename to src/pages/common/components/OptimisticFeedCard/hooks/index.ts diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/hooks/useMenuItems.tsx b/src/pages/common/components/OptimisticFeedCard/hooks/useMenuItems.tsx similarity index 100% rename from src/pages/common/components/OptimisticDiscussionFeedCard/hooks/useMenuItems.tsx rename to src/pages/common/components/OptimisticFeedCard/hooks/useMenuItems.tsx diff --git a/src/pages/common/components/OptimisticFeedCard/index.ts b/src/pages/common/components/OptimisticFeedCard/index.ts new file mode 100644 index 0000000000..8671496d94 --- /dev/null +++ b/src/pages/common/components/OptimisticFeedCard/index.ts @@ -0,0 +1 @@ +export { default as OptimisticFeedCard } from "./OptimisticFeedCard"; diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/getAllowedItems.ts b/src/pages/common/components/OptimisticFeedCard/utils/getAllowedItems.ts similarity index 100% rename from src/pages/common/components/OptimisticDiscussionFeedCard/utils/getAllowedItems.ts rename to src/pages/common/components/OptimisticFeedCard/utils/getAllowedItems.ts diff --git a/src/pages/common/components/OptimisticDiscussionFeedCard/utils/index.ts b/src/pages/common/components/OptimisticFeedCard/utils/index.ts similarity index 100% rename from src/pages/common/components/OptimisticDiscussionFeedCard/utils/index.ts rename to src/pages/common/components/OptimisticFeedCard/utils/index.ts diff --git a/src/pages/common/components/index.ts b/src/pages/common/components/index.ts index 5007e04803..5222245c9a 100644 --- a/src/pages/common/components/index.ts +++ b/src/pages/common/components/index.ts @@ -3,7 +3,7 @@ export * from "./CommonMobileModal"; export * from "./CommonTabPanels"; export * from "./CommonTopNavigation"; export * from "./DiscussionFeedCard"; -export * from "./OptimisticDiscussionFeedCard"; +export * from "./OptimisticFeedCard"; export * from "./FeedCard"; export * from "./FeedItem"; export * from "./FeedItems"; diff --git a/src/shared/hooks/useCases/useCommonFeedItems.ts b/src/shared/hooks/useCases/useCommonFeedItems.ts index 76c3d94640..97d6c1a023 100644 --- a/src/shared/hooks/useCases/useCommonFeedItems.ts +++ b/src/shared/hooks/useCases/useCommonFeedItems.ts @@ -62,13 +62,14 @@ export const useCommonFeedItems = ( } ); data.forEach((item) => { - const discussionId = item.commonFeedItem.data.id; + const discussionId = item.commonFeedItem.data.discussionId ?? item.commonFeedItem.data.id; if(optItemIds.includes(discussionId)) { dispatch(commonActions.removeOptimisticFeedItemState({id: discussionId})) } }) + console.log('-----data',data); const finalData = idsForNotListeningRef.current.length > 0 ? data.filter( diff --git a/src/shared/models/CommonFeed.tsx b/src/shared/models/CommonFeed.tsx index aeb4660d50..3f56f79986 100644 --- a/src/shared/models/CommonFeed.tsx +++ b/src/shared/models/CommonFeed.tsx @@ -7,6 +7,7 @@ export enum CommonFeedType { Proposal = "Proposal", Discussion = "Discussion", OptimisticDiscussion = "OptimisticDiscussion", + OptimisticProposal = "OptimisticProposal", Project = "Project", PayIn = "PayIn", ProjectCreation = "ProjectCreation", diff --git a/src/shared/utils/generateOptimisticFeedItem.ts b/src/shared/utils/generateOptimisticFeedItem.ts new file mode 100644 index 0000000000..615c42779e --- /dev/null +++ b/src/shared/utils/generateOptimisticFeedItem.ts @@ -0,0 +1,63 @@ +import { Timestamp as FirestoreTimestamp } from "firebase/firestore"; +import { v4 as uuidv4 } from "uuid"; +import { CommonFeed, CommonFeedType, OptimisticFeedItemState } from "../models"; + +interface GenerateOptimisticFeedItemPayload { + userId: string; + discussionId: string; + commonId: string; + type: CommonFeedType, + title: string; + content: string; + circleVisibility: string[]; +} + +export const generateOptimisticFeedItem = ({ + userId, + discussionId, + commonId, + type, + title, + content, + circleVisibility +}: GenerateOptimisticFeedItemPayload): CommonFeed => { + + const optimisticFeedItemId = uuidv4(); + const currentDate = FirestoreTimestamp.now(); + return { + id: optimisticFeedItemId, + createdAt: currentDate, + updatedAt: currentDate, + isDeleted: false, + userId, + commonId, + data: { + type, + id: discussionId, + discussionId: null, + hasFiles: false, + hasImages: false, + }, + optimisticData: { + id: discussionId, + title: title, + message: content, + ownerId: userId, + commonId, + lastMessage: currentDate, + updatedAt: currentDate, + createdAt: currentDate, + messageCount: 0, + followers: [], + files: [], + images: [], + discussionMessages: [], + isDeleted: false, + circleVisibility, + circleVisibilityByCommon: null, + linkedCommonIds: [], + state: OptimisticFeedItemState.loading, + }, + circleVisibility, + } +} \ No newline at end of file diff --git a/src/shared/utils/index.tsx b/src/shared/utils/index.tsx index af54f66cf5..ade45e0565 100755 --- a/src/shared/utils/index.tsx +++ b/src/shared/utils/index.tsx @@ -45,3 +45,4 @@ export * from "./joinWithLast"; export * from "./getResizedFileUrl"; export * from "./areTimestampsEqual"; export * from "./parseMessageLink"; +export * from "./generateOptimisticFeedItem"; diff --git a/src/store/states/common/reducer.ts b/src/store/states/common/reducer.ts index adf5f49e45..1fd39d71f0 100644 --- a/src/store/states/common/reducer.ts +++ b/src/store/states/common/reducer.ts @@ -701,8 +701,9 @@ export const reducer = createReducer(initialState) produce(state, (nextState) => { const updatedMap = new Map(nextState.optimisticFeedItems); + const optimisticItemId = payload.data.discussionId ?? payload.data.id; // Add the new item to the Map - updatedMap.set(payload.data.id, { + updatedMap.set(optimisticItemId, { type: InboxItemType.FeedItemFollow, itemId: payload.id, feedItem: payload, @@ -710,7 +711,7 @@ export const reducer = createReducer(initialState) // Assign the new Map back to the state nextState.optimisticFeedItems = updatedMap; - nextState.recentStreamId = payload.data.id; + nextState.recentStreamId = optimisticItemId; }), ) .handleAction(actions.updateOptimisticFeedItemState, (state, { payload }) => From bd8fd512a11732a548efab2b266c5bd856695e62 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Mon, 2 Sep 2024 15:18:13 +0300 Subject: [PATCH 14/38] CW-Optimistic-streams Remove unnecessary code --- .../OptimisticFeedCard/hooks/index.ts | 1 - .../OptimisticFeedCard/hooks/useMenuItems.tsx | 226 ------------------ .../utils/getAllowedItems.ts | 100 -------- .../OptimisticFeedCard/utils/index.ts | 1 - 4 files changed, 328 deletions(-) delete mode 100644 src/pages/common/components/OptimisticFeedCard/hooks/index.ts delete mode 100644 src/pages/common/components/OptimisticFeedCard/hooks/useMenuItems.tsx delete mode 100644 src/pages/common/components/OptimisticFeedCard/utils/getAllowedItems.ts delete mode 100644 src/pages/common/components/OptimisticFeedCard/utils/index.ts diff --git a/src/pages/common/components/OptimisticFeedCard/hooks/index.ts b/src/pages/common/components/OptimisticFeedCard/hooks/index.ts deleted file mode 100644 index f0a637aef4..0000000000 --- a/src/pages/common/components/OptimisticFeedCard/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./useMenuItems"; diff --git a/src/pages/common/components/OptimisticFeedCard/hooks/useMenuItems.tsx b/src/pages/common/components/OptimisticFeedCard/hooks/useMenuItems.tsx deleted file mode 100644 index c7120aa2e3..0000000000 --- a/src/pages/common/components/OptimisticFeedCard/hooks/useMenuItems.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import React from "react"; -import { useDispatch } from "react-redux"; -import { animateScroll } from "react-scroll"; -import { CommonFeedService } from "@/services"; -import { CommonAction, FollowFeedItemAction } from "@/shared/constants"; -import { - Edit3Icon, - Pin2Icon, - Report2Icon, - Share3Icon, - FollowIcon, - Trash2Icon, - UnfollowIcon, - UnpinIcon, - Link4Icon as LinkIcon, - Message3Icon, - MoveItemIcon, -} from "@/shared/icons"; -import { - ContextMenuItem as Item, - MarkCommonFeedItemAsSeenPayload, - MarkCommonFeedItemAsUnseenPayload, - UploadFile, -} from "@/shared/interfaces"; -import { parseStringToTextEditorValue } from "@/shared/ui-kit"; -import { - getCirclesWithLowestTier, - getFilteredByIdCircles, -} from "@/shared/utils"; -import { notEmpty } from "@/shared/utils/notEmpty"; -import { commonActions } from "@/store/states"; -import { FeedItemMenuItem, GetAllowedItemsOptions } from "../../FeedItem"; -import { getAllowedItems } from "../utils"; - -interface Actions { - report: () => void; - share: () => void; - remove?: () => void; - linkStream?: () => void; - moveStream?: () => void; - unlinkStream?: () => void; - markFeedItemAsSeen: (payload: MarkCommonFeedItemAsSeenPayload) => void; - markFeedItemAsUnseen: (payload: MarkCommonFeedItemAsUnseenPayload) => void; -} - -export const useMenuItems = ( - options: GetAllowedItemsOptions, - actions: Actions, -): Item[] => { - const dispatch = useDispatch(); - - if (options.withoutMenu) { - return []; - } - - const { - discussion, - commonId, - feedItem, - feedItemFollow, - feedItemUserMetadata, - } = options; - const { - report, - share, - remove, - linkStream, - moveStream, - unlinkStream, - markFeedItemAsSeen, - markFeedItemAsUnseen, - } = actions; - const allowedMenuItems = getAllowedItems({ ...options, feedItemFollow }); - const items: Item[] = [ - { - id: FeedItemMenuItem.Pin, - text: "Pin", - onClick: async () => { - if (!commonId || !feedItem) return; - await CommonFeedService.pinItem(commonId, feedItem.id); - }, - icon: , - }, - { - id: FeedItemMenuItem.Unpin, - text: "Unpin", - onClick: async () => { - if (!commonId || !feedItem) return; - await CommonFeedService.unpinItem(commonId, feedItem.id); - }, - icon: , - }, - { - id: FeedItemMenuItem.Share, - text: "Share", - onClick: share, - icon: , - }, - { - id: FeedItemMenuItem.MarkUnread, - text: "Mark as unread", - onClick: () => { - if (!commonId || !feedItem) { - return; - } - - markFeedItemAsUnseen({ - commonId, - feedObjectId: feedItem.id, - }); - }, - icon: , - }, - { - id: FeedItemMenuItem.MarkRead, - text: "Mark as read", - onClick: () => { - if (!commonId || !feedItem) { - return; - } - - markFeedItemAsSeen({ - commonId, - feedObjectId: feedItem.id, - lastSeenId: feedItemUserMetadata?.lastSeen?.id, - type: feedItemUserMetadata?.lastSeen?.type, - }); - }, - icon: , - }, - { - id: FeedItemMenuItem.Report, - text: "Report", - onClick: report, - icon: , - }, - { - id: FeedItemMenuItem.Edit, - text: "Edit", - onClick: () => { - if (!discussion) { - return; - } - - const files: UploadFile[] = discussion.images.map((file, index) => ({ - id: index.toString(), - title: file.title, - file: file.value, - })); - const circleVisibility = - discussion.circleVisibilityByCommon?.[options.commonId || ""] || []; - const filteredByIdCircles = getFilteredByIdCircles( - options.governanceCircles - ? Object.values(options.governanceCircles) - : null, - circleVisibility, - ); - const circles = getCirclesWithLowestTier(filteredByIdCircles); - - dispatch( - commonActions.setDiscussionCreationData({ - circle: circles[0] || null, - title: discussion.title, - content: parseStringToTextEditorValue(discussion.message), - images: files, - id: discussion.id, - }), - ); - dispatch(commonActions.setCommonAction(CommonAction.EditDiscussion)); - animateScroll.scrollToTop({ containerId: document.body, smooth: true }); - }, - icon: , - }, - { - id: FeedItemMenuItem.Follow, - text: "Follow", - onClick: () => feedItemFollow.onFollowToggle(FollowFeedItemAction.Follow), - icon: , - }, - { - id: FeedItemMenuItem.Unfollow, - text: "Unfollow", - onClick: () => - feedItemFollow.onFollowToggle(FollowFeedItemAction.Unfollow), - icon: , - }, - linkStream - ? { - id: FeedItemMenuItem.LinkTo, - text: "Link to...", - onClick: linkStream, - icon: , - } - : undefined, - moveStream - ? { - id: FeedItemMenuItem.MoveTo, - text: "Move to...", - onClick: moveStream, - icon: , - } - : undefined, - unlinkStream - ? { - id: FeedItemMenuItem.Unlink, - text: "Unlink", - onClick: unlinkStream, - withWarning: true, - icon: , - } - : undefined, - remove - ? { - id: FeedItemMenuItem.Remove, - text: "Delete", - onClick: remove, - withWarning: true, - icon: , - } - : undefined, - ].filter(notEmpty); - - return items.filter((item) => - allowedMenuItems.includes(item.id as FeedItemMenuItem), - ); -}; diff --git a/src/pages/common/components/OptimisticFeedCard/utils/getAllowedItems.ts b/src/pages/common/components/OptimisticFeedCard/utils/getAllowedItems.ts deleted file mode 100644 index 27f3e77cb0..0000000000 --- a/src/pages/common/components/OptimisticFeedCard/utils/getAllowedItems.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { CommonFeedType } from "@/shared/models"; -import { notEmpty } from "@/shared/utils/notEmpty"; -import { FeedItemMenuItem, FeedItemPinAction } from "../../FeedItem/constants"; -import { GetAllowedItemsOptions } from "../../FeedItem/types"; -import { checkIsEditItemAllowed } from "./checkIsEditItemAllowed"; -import { checkIsLinkToAllowed } from "./checkIsLinkToAllowed"; -import { checkIsMoveToAllowed } from "./checkIsMoveToAllowed"; -import { checkIsPinUnpinAllowed } from "./checkIsPinUnpinAllowed"; -import { checkIsRemoveDiscussionAllowed } from "./checkIsRemoveDiscussionAllowed"; - -const MENU_ITEM_TO_CHECK_FUNCTION_MAP: Record< - FeedItemMenuItem, - (options: GetAllowedItemsOptions) => boolean -> = { - [FeedItemMenuItem.Share]: () => true, - [FeedItemMenuItem.Report]: () => false, - [FeedItemMenuItem.Edit]: (options) => checkIsEditItemAllowed(options), - [FeedItemMenuItem.Remove]: checkIsRemoveDiscussionAllowed, - [FeedItemMenuItem.Pin]: (options) => - checkIsPinUnpinAllowed(FeedItemPinAction.Pin, options), - [FeedItemMenuItem.Unpin]: (options) => - checkIsPinUnpinAllowed(FeedItemPinAction.Unpin, options), - [FeedItemMenuItem.Follow]: (options) => { - return ( - !options.feedItemFollow.isDisabled && !options.feedItemFollow.isFollowing - ); - }, - [FeedItemMenuItem.Unfollow]: (options) => { - return ( - !options.feedItemFollow.isDisabled && options.feedItemFollow.isFollowing - ); - }, - [FeedItemMenuItem.MarkUnread]: ({ feedItemUserMetadata }) => { - const { count, seen, isSeenUpdating } = feedItemUserMetadata || {}; - - if (!feedItemUserMetadata) { - return true; - } - - if (isSeenUpdating) { - return false; - } - - return notEmpty(count) && notEmpty(seen) && count === 0 && seen; - }, - [FeedItemMenuItem.MarkRead]: ({ feedItemUserMetadata }) => { - const { count, seenOnce, seen, isSeenUpdating } = - feedItemUserMetadata || {}; - - if (isSeenUpdating) { - return false; - } - - return ( - Boolean(count) || - (notEmpty(seen) && !seen) || - (notEmpty(seenOnce) && !seenOnce) - ); - }, - [FeedItemMenuItem.LinkTo]: checkIsLinkToAllowed, - [FeedItemMenuItem.MoveTo]: checkIsMoveToAllowed, - [FeedItemMenuItem.Unlink]: (options) => { - return Boolean( - options.discussion?.linkedCommonIds && - options.discussion?.linkedCommonIds.length > 0 && - checkIsRemoveDiscussionAllowed(options), - ); - }, -}; - -export const getAllowedItems = ( - options: GetAllowedItemsOptions, -): FeedItemMenuItem[] => { - const orderedItems = [ - FeedItemMenuItem.Follow, - FeedItemMenuItem.Unfollow, - FeedItemMenuItem.Pin, - FeedItemMenuItem.Unpin, - FeedItemMenuItem.Share, - FeedItemMenuItem.MarkUnread, - FeedItemMenuItem.MarkRead, - FeedItemMenuItem.Report, - FeedItemMenuItem.Edit, - FeedItemMenuItem.LinkTo, - FeedItemMenuItem.MoveTo, - FeedItemMenuItem.Unlink, - FeedItemMenuItem.Remove, - ]; - const nonAllowedItems = - options.getNonAllowedItems?.( - options?.feedItem?.data.type ?? CommonFeedType.Discussion, - options, - ) || []; - - return orderedItems.filter( - (item) => - !nonAllowedItems.includes(item) && - MENU_ITEM_TO_CHECK_FUNCTION_MAP[item](options), - ); -}; diff --git a/src/pages/common/components/OptimisticFeedCard/utils/index.ts b/src/pages/common/components/OptimisticFeedCard/utils/index.ts deleted file mode 100644 index cd586fc395..0000000000 --- a/src/pages/common/components/OptimisticFeedCard/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./getAllowedItems"; From c69907c7fa33a84ec5a6c35cbce7a28e98a8ccfe Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Wed, 4 Sep 2024 09:39:22 +0300 Subject: [PATCH 15/38] CW-hotfix-firebase-settings Added error handling for firestore quota exceed --- src/shared/utils/firebase.tsx | 56 +++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/src/shared/utils/firebase.tsx b/src/shared/utils/firebase.tsx index a7f790cf01..258dc83c8e 100644 --- a/src/shared/utils/firebase.tsx +++ b/src/shared/utils/firebase.tsx @@ -14,7 +14,52 @@ interface FirebaseError extends Error { } const app = firebase.initializeApp(config.firebase); +const db = firebase.firestore(); +// Set Firestore settings with unlimited cache size +const settings = { + cacheSizeBytes: firebase.firestore.CACHE_SIZE_UNLIMITED, +}; +db.settings(settings); + +// Function to clear Firestore cache and re-enable persistence +function clearFirestoreCache() { + db.clearPersistence() + .then(() => { + console.log("Cache cleared successfully."); + enableUnlimitedCachePersistence(); // Re-enable persistence after clearing cache + return; + }) + .catch((err) => { + console.error("Error clearing persistence cache:", err); + }); +} + +// Function to handle Firestore persistence errors +function handlePersistenceError(err: any) { + if (err.code === "failed-precondition") { + console.log("Multiple tabs open or other conflict."); + } else if (err.code === "unimplemented") { + console.log("Persistence is not supported in this browser."); + } else if (err.name === "QuotaExceededError") { + console.log("Storage quota exceeded. Consider clearing cache."); + clearFirestoreCache(); + } else { + console.error("Error enabling persistence:", err); + } +} + +// Enable Firestore persistence with unlimited cache size and error handling +function enableUnlimitedCachePersistence() { + const settings = { + cacheSizeBytes: firebase.firestore.CACHE_SIZE_UNLIMITED, + }; + db.settings(settings); + + db.enablePersistence({ synchronizeTabs: true }).catch(handlePersistenceError); +} + +// Enable persistence in the local environment (with Firestore and Auth emulators) if (REACT_APP_ENV === Environment.Local) { firebase.auth().useEmulator(local.firebase.authDomain); firebase @@ -24,15 +69,8 @@ if (REACT_APP_ENV === Environment.Local) { Number(local.firebase.databaseURL.split(/:/g)[2]), ); } else { - firebase - .firestore() - .enablePersistence({ - synchronizeTabs: true, - experimentalForceOwningTab: false, - }) - .catch((error) => { - console.error("Error enabling persistence", error); - }); + // Enable persistence for non-local environments + db.enablePersistence({ synchronizeTabs: true }).catch(handlePersistenceError); } let perf; From bef5f568654830ce182c56cdbd259f8cb1b1e412 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Wed, 4 Sep 2024 15:18:16 +0300 Subject: [PATCH 16/38] CW-hotfix-firebase-firestore-settings Added testing functionality --- .../components/MenuItems/MenuItems.tsx | 7 +- src/shared/utils/firebase.tsx | 69 ++++++++++++------- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.tsx b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.tsx index 825d64116a..13df07a6ec 100644 --- a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.tsx +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.tsx @@ -13,6 +13,7 @@ import { } from "@/shared/icons"; import ThemeIcon from "@/shared/icons/theme.icon"; import { toggleTheme } from "@/shared/store/actions"; +import { clearFirestoreCache } from "@/shared/utils/firebase"; import { MenuItem } from "./components"; import { Item, ItemType } from "./types"; import styles from "./MenuItems.module.scss"; @@ -58,7 +59,11 @@ const MenuItems: FC = (props) => { key: "my-profile", text: "My profile", icon: , - to: getProfilePagePath(), + type: ItemType.Button, + // to: getProfilePagePath(), + onClick: () => { + clearFirestoreCache(); + }, }, { key: "settings", diff --git a/src/shared/utils/firebase.tsx b/src/shared/utils/firebase.tsx index 258dc83c8e..3047b18997 100644 --- a/src/shared/utils/firebase.tsx +++ b/src/shared/utils/firebase.tsx @@ -9,32 +9,16 @@ import { local } from "@/config"; import { Environment, REACT_APP_ENV } from "@/shared/constants"; import config from "../../config"; +const CACHE_SIZE_LIMIT = 104857600; // 100 MB + interface FirebaseError extends Error { code: string; } const app = firebase.initializeApp(config.firebase); -const db = firebase.firestore(); - -// Set Firestore settings with unlimited cache size -const settings = { - cacheSizeBytes: firebase.firestore.CACHE_SIZE_UNLIMITED, -}; -db.settings(settings); - -// Function to clear Firestore cache and re-enable persistence -function clearFirestoreCache() { - db.clearPersistence() - .then(() => { - console.log("Cache cleared successfully."); - enableUnlimitedCachePersistence(); // Re-enable persistence after clearing cache - return; - }) - .catch((err) => { - console.error("Error clearing persistence cache:", err); - }); -} +let db = firebase.firestore(); +enableUnlimitedCachePersistence(); // Function to handle Firestore persistence errors function handlePersistenceError(err: any) { if (err.code === "failed-precondition") { @@ -49,13 +33,53 @@ function handlePersistenceError(err: any) { } } +function reinitializeFirestoreWithPersistence() { + db = firebase.firestore(); // Reinitialize Firestore instance + const settings = { cacheSizeBytes: CACHE_SIZE_LIMIT }; + db.settings(settings); + + db.enablePersistence({ synchronizeTabs: true }) + .then(() => { + console.log("Persistence re-enabled."); + return; + }) + .catch(handlePersistenceError); +} + +// Function to clear Firestore cache and re-enable persistence +export function clearFirestoreCache() { + db.terminate() + .then(() => { + console.log("Firestore instance terminated."); + return db.clearPersistence(); // Safe to clear persistence now + }) + .then(() => { + console.log("Persistence cleared. Waiting before reinitializing..."); + return new Promise((resolve) => setTimeout(resolve, 4000)); // Wait 1 second + }) + .then(() => { + console.log("Cache cleared successfully."); + reinitializeFirestoreWithPersistence(); // Reinitialize Firestore + window.location.reload(); + return; + }) + .catch((err) => { + if (err.code === "failed-precondition") { + console.log("Cannot clear persistence: Firestore is still running."); + } else { + console.error("Error clearing persistence cache:", err); + } + }); +} + // Enable Firestore persistence with unlimited cache size and error handling function enableUnlimitedCachePersistence() { const settings = { - cacheSizeBytes: firebase.firestore.CACHE_SIZE_UNLIMITED, + cacheSizeBytes: CACHE_SIZE_LIMIT, }; db.settings(settings); + console.log("-enabled"); db.enablePersistence({ synchronizeTabs: true }).catch(handlePersistenceError); } @@ -68,9 +92,6 @@ if (REACT_APP_ENV === Environment.Local) { "localhost", Number(local.firebase.databaseURL.split(/:/g)[2]), ); -} else { - // Enable persistence for non-local environments - db.enablePersistence({ synchronizeTabs: true }).catch(handlePersistenceError); } let perf; From fb785d2ebf70b1d3bdd22ff5b65e19cddb67b19c Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Thu, 5 Sep 2024 11:29:33 +0300 Subject: [PATCH 17/38] CW-hotfix-firebase-firestore-settings Added feature flag --- src/shared/constants/featureFlags.ts | 1 + .../components/MenuItems/MenuItems.tsx | 85 ++++++++++++------- src/shared/utils/firebase.tsx | 3 +- 3 files changed, 54 insertions(+), 35 deletions(-) diff --git a/src/shared/constants/featureFlags.ts b/src/shared/constants/featureFlags.ts index 9efdb4f72c..429aa64a56 100644 --- a/src/shared/constants/featureFlags.ts +++ b/src/shared/constants/featureFlags.ts @@ -3,6 +3,7 @@ export enum FeatureFlags { AiBot = "AiBot", AiBotPro = "AiBotPro", UpdateRoles = "UpdateRoles", + HavingAnIssue = "HavingAnIssue" } export enum FeatureFlagVisibility { diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.tsx b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.tsx index 13df07a6ec..e86ada241f 100644 --- a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.tsx +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/UserInfo/components/MenuItems/MenuItems.tsx @@ -1,16 +1,19 @@ -import React, { FC } from "react"; +import React, { FC, useMemo } from "react"; import { useDispatch } from "react-redux"; import { useLocation } from "react-router"; import classNames from "classnames"; import { Menu } from "@headlessui/react"; import { logOut } from "@/pages/Auth/store/actions"; +import { FeatureFlags } from "@/shared/constants"; import { useRoutesContext } from "@/shared/contexts"; +import { useFeatureFlag } from "@/shared/hooks/useFeatureFlag"; import { Avatar3Icon, BillingIcon, LogoutIcon, NotificationsIcon, } from "@/shared/icons"; +import ReportIcon from "@/shared/icons/report.icon"; import ThemeIcon from "@/shared/icons/theme.icon"; import { toggleTheme } from "@/shared/store/actions"; import { clearFirestoreCache } from "@/shared/utils/firebase"; @@ -44,6 +47,9 @@ const MenuItems: FC = (props) => { const { pathname } = useLocation(); const isV04 = pathname.includes("-v04"); + const featureFlags = useFeatureFlag(); + const isHavingAnIssueEnabled = featureFlags?.get(FeatureFlags.HavingAnIssue); + const toggleThemeMenuItem = { key: "theme", type: ItemType.Button, @@ -54,40 +60,53 @@ const MenuItems: FC = (props) => { }, }; - const items: Item[] = [ - { - key: "my-profile", - text: "My profile", - icon: , - type: ItemType.Button, - // to: getProfilePagePath(), - onClick: () => { - clearFirestoreCache(); + const items: Item[] = useMemo(() => { + const menuItems = [ + { + key: "my-profile", + text: "My profile", + icon: , + type: ItemType.Button, + to: getProfilePagePath(), }, - }, - { - key: "settings", - text: "Notifications", - icon: , - to: getSettingsPagePath(), - }, - { - key: "billing", - text: "Billing", - icon: , - to: getBillingPagePath(), - }, - ...insertIf(!isV04, toggleThemeMenuItem), - { - key: "log-out", - type: ItemType.Button, - text: "Log out", - icon: , - onClick: () => { - dispatch(logOut()); + { + key: "settings", + text: "Notifications", + icon: , + to: getSettingsPagePath(), }, - }, - ]; + { + key: "billing", + text: "Billing", + icon: , + to: getBillingPagePath(), + }, + ...insertIf(!isV04, toggleThemeMenuItem), + { + key: "log-out", + type: ItemType.Button, + text: "Log out", + icon: , + onClick: () => { + dispatch(logOut()); + }, + }, + ]; + + if (isHavingAnIssueEnabled) { + menuItems.push({ + key: "issue", + text: "Having an issue?", + icon: , + type: ItemType.Button, + onClick: () => { + clearFirestoreCache(); + }, + }); + } + + return menuItems; + }, [isHavingAnIssueEnabled, isV04, toggleThemeMenuItem]); return ( diff --git a/src/shared/utils/firebase.tsx b/src/shared/utils/firebase.tsx index 3047b18997..80147a0c8f 100644 --- a/src/shared/utils/firebase.tsx +++ b/src/shared/utils/firebase.tsx @@ -55,7 +55,7 @@ export function clearFirestoreCache() { }) .then(() => { console.log("Persistence cleared. Waiting before reinitializing..."); - return new Promise((resolve) => setTimeout(resolve, 4000)); // Wait 1 second + return new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 2 second }) .then(() => { console.log("Cache cleared successfully."); @@ -79,7 +79,6 @@ function enableUnlimitedCachePersistence() { }; db.settings(settings); - console.log("-enabled"); db.enablePersistence({ synchronizeTabs: true }).catch(handlePersistenceError); } From b0681565207da2d24c9b0627316be2d0718f8e78 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Thu, 5 Sep 2024 13:13:10 +0300 Subject: [PATCH 18/38] CW-optimistic-streams Added instant close on creation modal --- .../components/NewDiscussionCreation/NewDiscussionCreation.tsx | 2 ++ .../components/NewProposalCreation/NewProposalCreation.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx index 06d8ca5998..8348f064c4 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx @@ -149,6 +149,8 @@ const NewDiscussionCreation: FC = (props) => { }), ); } + + handleCancel(); }, [governanceCircles, userCircleIds, userId, common.id, edit], ); diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx index 40ca182df1..840d838332 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx @@ -135,6 +135,8 @@ const NewProposalCreation: FC = (props) => { break; } } + + handleCancel(); }, [governance.circles, userCircleIds, userId, commonId], ); From 1a36c9aad08f04bcfc8305220ccdc9df0c8307d0 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Thu, 5 Sep 2024 19:34:55 +0300 Subject: [PATCH 19/38] CW-service-worker-performance Added check for registered service-workers --- .../NotificationsHandler.tsx | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx index de144355b3..44f26f4f59 100644 --- a/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx +++ b/src/pages/App/handlers/NotificationsHandler/NotificationsHandler.tsx @@ -8,37 +8,60 @@ const NotificationsHandler: FC = () => { const userId = user?.uid; const [isRegistered, setIsRegistered] = useState(false); + function initServiceWorker() { + navigator.serviceWorker + .register("/firebase-messaging-sw.js") + .then((registration) => { + setIsRegistered(true); + return registration; + }) + .catch((err) => { + console.log("ServiceWorker registration failed: ", err); + }); + } + + // Check if the service worker is already registered or register a new one useEffect(() => { if ("serviceWorker" in navigator) { navigator.serviceWorker - .register("/firebase-messaging-sw.js") - .then((registration) => { - setIsRegistered(true); - return registration; + .getRegistration("/firebase-messaging-sw.js") + .then((existingRegistration) => { + if (existingRegistration) { + setIsRegistered(true); + } else { + initServiceWorker(); + } + + return; }) .catch((err) => { - console.log("ServiceWorker registration failed: ", err); + console.log("Error checking service worker registration: ", err); }); } }, []); + // Handle notification permissions and foreground message listener useEffect(() => { - if (!userId && !isRegistered) { + if (!userId || !isRegistered) { return; } let unsubscribeOnMessage; (async () => { const hasPermissions = await NotificationService.requestPermissions(); - if (hasPermissions) { - await NotificationService.saveFCMToken(); - - unsubscribeOnMessage = NotificationService.onForegroundMessage(); + if (!hasPermissions) { + console.log("Notification permissions denied"); + return; } + + await NotificationService.saveFCMToken(); + unsubscribeOnMessage = NotificationService.onForegroundMessage(); })(); return () => { - unsubscribeOnMessage && unsubscribeOnMessage(); + if (unsubscribeOnMessage) { + unsubscribeOnMessage(); + } }; }, [userId, isRegistered]); From 11771614d36ca2f532c838b134bccb2c6afdcceb Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Tue, 10 Sep 2024 11:49:37 +0300 Subject: [PATCH 20/38] CW-service-worker-performance Added firefox permissions --- src/shared/components/Header/Header.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/shared/components/Header/Header.tsx b/src/shared/components/Header/Header.tsx index 672bd3bff2..8d10f043e0 100755 --- a/src/shared/components/Header/Header.tsx +++ b/src/shared/components/Header/Header.tsx @@ -3,6 +3,7 @@ import { useDispatch, useSelector } from "react-redux"; import { Link, RouteProps, useHistory } from "react-router-dom"; import classNames from "classnames"; import { Routes } from "@/pages/MyAccount/components/Routes"; +import { NotificationService } from "@/services"; import { Loader } from "@/shared/components"; import { useAnyMandatoryRoles, @@ -82,7 +83,8 @@ const Header = () => { setShowAccountLinks(isMyAccountRoute); }, [showMenu, isMyAccountRoute]); - const handleLogIn = useCallback(() => { + const handleLogIn = useCallback(async () => { + await NotificationService.requestPermissions(); dispatch(setLoginModalState({ isShowing: true })); setShowMenu(false); }, [dispatch]); @@ -116,7 +118,8 @@ const Header = () => { dispatch(logOut()); }; - const handleLaunchApp = () => { + const handleLaunchApp = async () => { + await NotificationService.requestPermissions(); history.push(ROUTE_PATHS.INBOX); }; From 956b5432e43a22edad105b306a7d9fdae6b1e9b1 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Tue, 10 Sep 2024 13:35:47 +0300 Subject: [PATCH 21/38] CW-service-worker-performance Fixed settings for firestore --- src/services/Notification.ts | 1 + src/shared/utils/firebase.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/services/Notification.ts b/src/services/Notification.ts index 25e7597b3c..1ccd7d3423 100644 --- a/src/services/Notification.ts +++ b/src/services/Notification.ts @@ -41,6 +41,7 @@ class NotificationService { const token = await firebase.messaging().getToken({ vapidKey: firebaseConfig.vapidKey }); if (token) { + console.log('---token',token); await Api.post( this.endpoints.setFCMToken, { diff --git a/src/shared/utils/firebase.tsx b/src/shared/utils/firebase.tsx index 80147a0c8f..6755110688 100644 --- a/src/shared/utils/firebase.tsx +++ b/src/shared/utils/firebase.tsx @@ -30,6 +30,7 @@ function handlePersistenceError(err: any) { clearFirestoreCache(); } else { console.error("Error enabling persistence:", err); + reinitializeFirestoreWithPersistence(); } } From dc5b99a8df066d09af6b120ddd6159e53fd0931e Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Tue, 10 Sep 2024 14:30:32 +0300 Subject: [PATCH 22/38] CW-service-worker-performance Remove console.log --- src/services/Notification.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/Notification.ts b/src/services/Notification.ts index 1ccd7d3423..25e7597b3c 100644 --- a/src/services/Notification.ts +++ b/src/services/Notification.ts @@ -41,7 +41,6 @@ class NotificationService { const token = await firebase.messaging().getToken({ vapidKey: firebaseConfig.vapidKey }); if (token) { - console.log('---token',token); await Api.post( this.endpoints.setFCMToken, { From a80e26990b45c12481862750fb64747328a5f810 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Tue, 17 Sep 2024 15:07:15 +0300 Subject: [PATCH 23/38] CW-optimistic-streams Improvements --- .../OptimisticFeedCard/OptimisticFeedCard.tsx | 98 +++++++------------ src/store/states/common/reducer.ts | 6 +- 2 files changed, 35 insertions(+), 69 deletions(-) diff --git a/src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx b/src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx index f0128b4fb3..d2aca2d7de 100644 --- a/src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx +++ b/src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx @@ -37,7 +37,6 @@ import { GetLastMessageOptions, GetNonAllowedItemsOptions, } from "../FeedItem"; -import { OptimisticFeedCardContent } from "./components"; interface OptimisticFeedCardProps { item: CommonFeed; @@ -81,7 +80,6 @@ const OptimisticFeedCard = forwardRef< } = useChatContext(); const { item, - governanceCircles, isMobileVersion = false, commonId, commonName, @@ -94,13 +92,11 @@ const OptimisticFeedCard = forwardRef< isExpanded, getLastMessage, onActiveItemDataChange, - directParent, feedItemFollow, shouldPreLoadMessages, onUserClick, onFeedItemClick, onInternalLinkClick, - type, } = props; const { fetchUser: fetchDiscussionCreator, data: discussionCreator } = @@ -124,10 +120,6 @@ const OptimisticFeedCard = forwardRef< }); const menuItems = []; const user = useSelector(selectUser()); - const [isHovering, setHovering] = useState(false); - const onHover = (isMouseEnter: boolean): void => { - setHovering(isMouseEnter); - }; const userId = user?.uid; const cardTitle = discussion?.title; const commonNotion = outerCommonNotion ?? common?.notion; @@ -267,63 +259,39 @@ const OptimisticFeedCard = forwardRef< ]); return ( - <> - - {(isExpanded || isActive) && ( - onUserClick(item.userId))} - discussionCreator={discussionCreator} - isHome={isHome} - menuItems={menuItems} - discussionImages={discussion?.images ?? []} - discussionMessage={discussion?.message} - common={common} - discussionNotion={discussionNotion} - handleOpenChat={handleOpenChat} - onHover={onHover} - isLoading={false} - type={type} - /> - )} - - + ); }); diff --git a/src/store/states/common/reducer.ts b/src/store/states/common/reducer.ts index 1fd39d71f0..13c3527e4f 100644 --- a/src/store/states/common/reducer.ts +++ b/src/store/states/common/reducer.ts @@ -358,13 +358,12 @@ export const reducer = createReducer(initialState) }; }), ) - .handleAction(actions.createDiscussion.success, (state, { payload }) => + .handleAction(actions.createDiscussion.success, (state) => produce(state, (nextState) => { nextState.discussionCreation = { loading: false, data: null, }; - nextState.recentStreamId = payload.id; }), ) .handleAction(actions.createDiscussion.failure, (state) => @@ -417,13 +416,12 @@ export const reducer = createReducer(initialState) actions.createSurveyProposal.success, actions.createFundingProposal.success, ], - (state, { payload }) => + (state) => produce(state, (nextState) => { nextState.proposalCreation = { loading: false, data: null, }; - nextState.recentStreamId = payload.id; }), ) .handleAction( From 6c332c770ffe9aaca14762f4b79083a907af4b40 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Mon, 30 Sep 2024 23:00:33 +0300 Subject: [PATCH 24/38] CW-optimistic-streams Improvements to optimistic: 1. Fixed glitched in card while transitioning from Optimistic to Real 2. Added focuses for ChatComponent and DiscussionCreation 3. Remove unnecessary proposalCreation modal --- .../ChatComponent/ChatComponent.tsx | 10 +++- .../ChatInput/ChatInput.module.scss | 2 + .../components/ChatInput/ChatInput.tsx | 11 +++-- .../NewDiscussionCreation.tsx | 11 ++++- .../DiscussionForm/DiscussionForm.tsx | 11 ++++- .../NewProposalCreation.tsx | 10 +++- .../DiscussionFeedCard/DiscussionFeedCard.tsx | 41 ++++++++++++++--- .../common/components/FeedCard/FeedCard.tsx | 2 +- .../components/FeedCardTags/FeedCardTags.tsx | 9 ++-- .../common/components/FeedItem/FeedItem.tsx | 4 +- .../OptimisticFeedCard/OptimisticFeedCard.tsx | 43 ++--------------- src/pages/commonFeed/CommonFeed.tsx | 4 ++ .../components/FeedLayout/FeedLayout.tsx | 15 +++--- .../utils/checkShouldAutoOpenPreview.ts | 2 +- .../NewStreamButton/NewStreamButton.tsx | 18 ++++++++ .../Form/Formik/TextField/TextField.tsx | 9 ++-- src/shared/components/Form/Input/Input.tsx | 8 +++- .../hooks/useCases/useCommonFeedItems.ts | 1 - src/shared/models/CommonFeed.tsx | 23 ++++++---- .../ui-kit/TextEditor/BaseTextEditor.tsx | 46 ++++++++++++++----- src/shared/utils/generateFirstMessage.ts | 3 ++ .../utils/generateOptimisticFeedItem.ts | 7 ++- src/shared/utils/index.tsx | 1 + src/store/states/common/actions.ts | 4 ++ src/store/states/common/constants.ts | 2 + src/store/states/common/reducer.ts | 14 ++++++ src/store/states/common/selectors.ts | 3 ++ src/store/states/common/types.ts | 1 + 28 files changed, 216 insertions(+), 99 deletions(-) create mode 100644 src/shared/utils/generateFirstMessage.ts diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 197571f67e..925bb707c1 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -6,6 +6,7 @@ import React, { ChangeEvent, useRef, ReactNode, + useLayoutEffect, } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useDebounce, useMeasure, useScroll } from "react-use"; @@ -83,6 +84,7 @@ import { uploadFilesAndImages, } from "./utils"; import styles from "./ChatComponent.module.scss"; +import { BaseTextEditorHandles } from "@/shared/ui-kit/TextEditor/BaseTextEditor"; const BASE_CHAT_INPUT_HEIGHT = 48; @@ -163,6 +165,7 @@ export default function ChatComponent({ queryParams[QueryParamKey.Unchecked] === "true"; const { checkImageSize } = useImageSizeCheck(); useZoomDisabling(); + const textInputRef = useRef(null); const editorRef = useRef(null); const [inputContainerRef, { height: chatInputHeight }] = useMeasure(); @@ -268,7 +271,6 @@ export default function ChatComponent({ const optimisticFeedItems = useSelector(selectOptimisticFeedItems); - // console.log("--optimisticFeedItems", optimisticFeedItems); const optimisticDiscussionMessages = useSelector( selectOptimisticDiscussionMessages, ); @@ -718,6 +720,11 @@ export default function ChatComponent({ } }, [discussionMessageReply, currentFilesPreview]); + useLayoutEffect(() => { + textInputRef?.current?.clear?.(); + textInputRef?.current?.focus?.(); + },[discussionId]); + useEffect(() => { if (isFetchedDiscussionMessages) { onMessagesAmountChange?.(discussionMessages.length); @@ -852,6 +859,7 @@ export default function ChatComponent({ })} > void; inputContainerRef?: - | MutableRefObject - | RefCallback; + | MutableRefObject + | RefCallback; editorRef?: MutableRefObject | RefCallback; renderChatInputOuter?: () => ReactElement; isAuthorized?: boolean; } -export const ChatInput = (props: ChatInputProps): ReactElement | null => { +export const ChatInput = React.memo(forwardRef((props, ref): ReactElement | null => { const { inputContainerRef, editorRef, @@ -93,6 +95,7 @@ export const ChatInput = (props: ChatInputProps): ReactElement | null => { accept={FILES_ACCEPTED_EXTENSIONS} /> { ); -}; +})); diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx index 8348f064c4..e30f9ff74c 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx @@ -18,13 +18,14 @@ import { TextEditorValue, parseStringToTextEditorValue, } from "@/shared/ui-kit/TextEditor"; -import { generateOptimisticFeedItem } from "@/shared/utils"; +import { generateFirstMessage, generateOptimisticFeedItem, getUserName } from "@/shared/utils"; import { selectDiscussionCreationData, selectIsDiscussionCreationLoading, } from "@/store/states"; import { commonActions } from "@/store/states"; import { DiscussionCreationCard, DiscussionCreationModal } from "./components"; +import { DiscussionMessageOwnerType } from "@/shared/constants"; interface NewDiscussionCreationProps { common: Common; @@ -120,7 +121,7 @@ const NewDiscussionCreation: FC = (props) => { ); } else { const discussionId = uuidv4(); - + const userName = getUserName(user); dispatch( commonActions.setOptimisticFeedItem( generateOptimisticFeedItem({ @@ -131,6 +132,12 @@ const NewDiscussionCreation: FC = (props) => { discussionId, title: values.title, content: JSON.stringify(values.content), + lastMessageContent: { + ownerId: userId, + userName, + ownerType: DiscussionMessageOwnerType.System, + content: generateFirstMessage({userName, userId}), + } }), ), ); diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/components/DiscussionForm/DiscussionForm.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/components/DiscussionForm/DiscussionForm.tsx index 06ff7aa9cf..3f65312069 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/components/DiscussionForm/DiscussionForm.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/components/DiscussionForm/DiscussionForm.tsx @@ -1,4 +1,4 @@ -import React, { FC } from "react"; +import React, { FC, useEffect, useRef } from "react"; import classNames from "classnames"; import { TextEditor, @@ -17,9 +17,18 @@ interface DiscussionFormProps { const DiscussionForm: FC = (props) => { const { className, disabled = false } = props; + const textEditorRef = useRef(null); + + useEffect(() => { + if (textEditorRef.current) { + textEditorRef.current.focus(); + } + }, []); + return (
= (props) => { const proposalId = uuidv4(); const discussionId = uuidv4(); + const userName = getUserName(user); dispatch( commonActions.setOptimisticFeedItem( @@ -98,6 +100,12 @@ const NewProposalCreation: FC = (props) => { discussionId, title: values.title, content: JSON.stringify(values.content), + lastMessageContent: { + ownerId: userId, + userName, + ownerType: DiscussionMessageOwnerType.System, + content: generateFirstMessage({userName, userId}), + } }), ), ); diff --git a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx index db2a949399..0ecf31b59d 100644 --- a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx +++ b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx @@ -5,13 +5,13 @@ import React, { useMemo, useState, } from "react"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { useUpdateEffect } from "react-use"; import { debounce } from "lodash"; import { selectUser } from "@/pages/Auth/store/selectors"; import { DiscussionService } from "@/services"; import { DeletePrompt, GlobalOverlay, ReportModal } from "@/shared/components"; -import { EntityTypes, InboxItemType } from "@/shared/constants"; +import { DiscussionMessageOwnerType, EntityTypes, InboxItemType } from "@/shared/constants"; import { useModal, useNotification } from "@/shared/hooks"; import { FeedItemFollowState, @@ -33,7 +33,7 @@ import { PredefinedTypes, } from "@/shared/models"; import { TextEditorValue } from "@/shared/ui-kit"; -import { StaticLinkType, getUserName, InternalLinkData } from "@/shared/utils"; +import { StaticLinkType, getUserName, InternalLinkData, generateFirstMessage } from "@/shared/utils"; import { useChatContext } from "../ChatComponent"; import { FeedCard } from "../FeedCard"; import { FeedCardShare } from "../FeedCard"; @@ -49,6 +49,7 @@ import { DiscussionFeedCardContent, } from "./components"; import { useMenuItems } from "./hooks"; +import { commonActions } from "@/store/states"; interface DiscussionFeedCardProps { item: CommonFeed; @@ -76,6 +77,7 @@ interface DiscussionFeedCardProps { onUserClick?: (userId: string) => void; onFeedItemClick: (feedItemId: string) => void; onInternalLinkClick: (data: InternalLinkData) => void; + isOptimisticallyCreated?: boolean; } function DiscussionFeedCard(props, ref) { @@ -86,6 +88,7 @@ function DiscussionFeedCard(props, ref) { nestedItemData, } = useChatContext(); const { notify } = useNotification(); + const dispatch = useDispatch(); const { item, governanceCircles, @@ -112,6 +115,7 @@ function DiscussionFeedCard(props, ref) { onUserClick, onFeedItemClick, onInternalLinkClick, + isOptimisticallyCreated, } = props; const { isShowing: isReportModalOpen, @@ -213,6 +217,12 @@ function DiscussionFeedCard(props, ref) { const cardTitle = discussion?.title; const commonNotion = outerCommonNotion ?? common?.notion; + // const ownerId = useMemo(() => { + // if(item.userId) { + // return item.userId + // } + // },[item.userId]) + const handleOpenChat = useCallback(() => { if (discussion && !isPreviewMode) { setChatItem({ @@ -274,6 +284,13 @@ function DiscussionFeedCard(props, ref) { [preloadDiscussionMessagesData.preloadDiscussionMessages], ); + useEffect(() => { + if(item.data.lastMessage?.content && discussion?.id && isOptimisticallyCreated) { + // markFeedItemAsSeen({feedObjectId: item.id, commonId}) + dispatch(commonActions.clearCreatedOptimisticFeedItem(discussion?.id)); + } + },[item.id, item.data.lastMessage?.content, discussion?.id, isOptimisticallyCreated, commonId]) + useEffect(() => { fetchDiscussionCreator(item.userId); }, [item.userId]); @@ -348,12 +365,21 @@ function DiscussionFeedCard(props, ref) { }, [item.data.lastMessage?.content]); const lastMessage = useMemo(() => { + const userName = getUserName(discussionCreator); + + const optimisticMessage = { + userName, + ownerId: userId, + content: generateFirstMessage({userName, userId: userId ?? ""}), + ownerType: DiscussionMessageOwnerType.System, + } + return getLastMessage({ commonFeedType: item.data.type, - lastMessage: item.data.lastMessage, + lastMessage: isOptimisticallyCreated ? optimisticMessage : item.data.lastMessage, discussion, currentUserId: userId, - feedItemCreatorName: getUserName(discussionCreator), + feedItemCreatorName: userName, commonName, isProject, hasFiles: item.data.hasFiles, @@ -369,6 +395,7 @@ function DiscussionFeedCard(props, ref) { isProject, item.data.hasFiles, item.data.hasImages, + isOptimisticallyCreated, ]); return ( @@ -391,13 +418,13 @@ function DiscussionFeedCard(props, ref) { image={commonImage} imageAlt={`${commonName}'s image`} isProject={isProject} - isFollowing={feedItemFollow.isFollowing} + isFollowing={isOptimisticallyCreated || feedItemFollow.isFollowing} isLoading={isLoading} menuItems={menuItems} seenOnce={ feedItemUserMetadata?.seenOnce ?? !isFeedItemUserMetadataFetched } - seen={feedItemUserMetadata?.seen ?? !isFeedItemUserMetadataFetched} + seen={(isOptimisticallyCreated || feedItemUserMetadata?.seen) ?? !isFeedItemUserMetadataFetched} ownerId={item.userId} discussionPredefinedType={discussion?.predefinedType} notion={discussionNotion && commonNotion} diff --git a/src/pages/common/components/FeedCard/FeedCard.tsx b/src/pages/common/components/FeedCard/FeedCard.tsx index 8a786f1bdd..28860dc8a3 100644 --- a/src/pages/common/components/FeedCard/FeedCard.tsx +++ b/src/pages/common/components/FeedCard/FeedCard.tsx @@ -243,7 +243,7 @@ const FeedCard = (props, ref) => { onClick: handleClick, onExpand: handleExpand, title, - lastMessage: !isLoading ? lastMessage : undefined, + lastMessage, menuItems, commonName, commonId, diff --git a/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx b/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx index 1a170e718e..5be69171b8 100644 --- a/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx +++ b/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx @@ -19,7 +19,7 @@ interface FeedCardTagsProps { hasUnseenMention?: boolean; } -export const FeedCardTags: FC = (props) => { +export const MemoizedFeedCardTags: FC = (props) => { const { unreadMessages, type, @@ -35,9 +35,8 @@ export const FeedCardTags: FC = (props) => { const isOwner = ownerId === user?.uid; const isNewTagVisible = notEmpty(seenOnce) && notEmpty(isOwner) && !seenOnce && !isOwner; - const isUnseenTagVisible = - !isNewTagVisible && !unreadMessages && notEmpty(seen) && !seen; - + const isUnseenTagVisible = + !isNewTagVisible && !unreadMessages && notEmpty(seen) && !seen && !isOwner; return ( <> {type === CommonFeedType.Proposal && ( @@ -86,3 +85,5 @@ export const FeedCardTags: FC = (props) => { ); }; + +export const FeedCardTags = React.memo(MemoizedFeedCardTags); \ No newline at end of file diff --git a/src/pages/common/components/FeedItem/FeedItem.tsx b/src/pages/common/components/FeedItem/FeedItem.tsx index 287e405292..15a8d10276 100644 --- a/src/pages/common/components/FeedItem/FeedItem.tsx +++ b/src/pages/common/components/FeedItem/FeedItem.tsx @@ -56,6 +56,7 @@ interface FeedItemProps { withoutMenu?: boolean; onFeedItemUpdate?: (item: CommonFeed, isRemoved: boolean) => void; getNonAllowedItems?: GetNonAllowedItemsOptions; + isOptimisticallyCreated?: boolean; } const FeedItem = forwardRef((props, ref) => { @@ -86,6 +87,7 @@ const FeedItem = forwardRef((props, ref) => { level, onFeedItemUpdate: outerOnFeedItemUpdate, getNonAllowedItems: outerGetNonAllowedItems, + isOptimisticallyCreated = false, } = props; const { onFeedItemUpdate, @@ -233,7 +235,7 @@ const FeedItem = forwardRef((props, ref) => { } if (item.data.type === CommonFeedType.Discussion) { - return ; + return ; } if (item.data.type === CommonFeedType.Proposal) { diff --git a/src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx b/src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx index d2aca2d7de..458e664088 100644 --- a/src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx +++ b/src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx @@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useMemo, - useState, } from "react"; import { useSelector } from "react-redux"; import { useUpdateEffect } from "react-use"; @@ -15,7 +14,6 @@ import { useCommon, useFeedItemUserMetadata, usePreloadDiscussionMessagesById, - useUserById, } from "@/shared/hooks/useCases"; import { FeedLayoutItemChangeData } from "@/shared/interfaces"; import { @@ -25,11 +23,11 @@ import { CommonMember, CommonNotion, DirectParent, - Discussion, + DiscussionWithOptimisticData, Governance, } from "@/shared/models"; import { TextEditorValue } from "@/shared/ui-kit"; -import { getUserName, InternalLinkData } from "@/shared/utils"; +import { InternalLinkData } from "@/shared/utils"; import { useChatContext } from "../ChatComponent"; import { FeedCard } from "../FeedCard"; import { @@ -56,7 +54,7 @@ interface OptimisticFeedCardProps { getLastMessage: (options: GetLastMessageOptions) => TextEditorValue; getNonAllowedItems?: GetNonAllowedItemsOptions; onActiveItemDataChange?: (data: FeedLayoutItemChangeData) => void; - discussion?: Discussion; + discussion?: DiscussionWithOptimisticData; directParent?: DirectParent | null; rootCommonId?: string; feedItemFollow: FeedItemFollowState; @@ -92,15 +90,12 @@ const OptimisticFeedCard = forwardRef< isExpanded, getLastMessage, onActiveItemDataChange, - feedItemFollow, shouldPreLoadMessages, onUserClick, onFeedItemClick, onInternalLinkClick, } = props; - const { fetchUser: fetchDiscussionCreator, data: discussionCreator } = - useUserById(); const isHome = false; const discussionNotion = undefined; const { @@ -170,10 +165,6 @@ const OptimisticFeedCard = forwardRef< [preloadDiscussionMessagesData.preloadDiscussionMessages], ); - useEffect(() => { - fetchDiscussionCreator(item.userId); - }, [item.userId]); - useEffect(() => { if (commonId) { fetchFeedItemUserMetadata({ @@ -234,30 +225,6 @@ const OptimisticFeedCard = forwardRef< } }, [item.data.lastMessage?.content]); - const lastMessage = useMemo(() => { - return getLastMessage({ - commonFeedType: item.data.type, - lastMessage: item.data.lastMessage, - discussion, - currentUserId: userId, - feedItemCreatorName: getUserName(discussionCreator), - commonName, - isProject, - hasFiles: item.data.hasFiles, - hasImages: item.data.hasImages, - }); - }, [ - item.data.type, - item.data.lastMessage, - discussion, - userId, - discussionCreator, - commonName, - isProject, - item.data.hasFiles, - item.data.hasImages, - ]); - return ( = (props) => { sharedFeedItemIdQueryParam) || null; const commonAction = useSelector(selectCommonAction); + const createdOptimisticFeedItems = useSelector(selectCreatedOptimisticFeedItems); const optimisticFeedItems = useSelector(selectOptimisticFeedItems); const { data: commonData, @@ -466,6 +468,7 @@ const CommonFeedComponent: FC = (props) => { ) { feedLayoutRef?.setActiveItem({ feedItemId: firstItem.feedItem.id, + discussion: createdOptimisticFeedItems.get(recentStreamId)?.feedItem.optimisticData }); dispatch(commonActions.setRecentStreamId("")); } else if ( @@ -474,6 +477,7 @@ const CommonFeedComponent: FC = (props) => { ) { feedLayoutRef?.setActiveItem({ feedItemId: optimisticFeedItems.get(recentStreamId)!.feedItem.id, + discussion: optimisticFeedItems.get(recentStreamId)?.feedItem.optimisticData, }); } }, [feedLayoutRef, recentStreamId, firstItem, optimisticFeedItems]); diff --git a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx index c2b8f3a7fa..64ba269785 100644 --- a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx +++ b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx @@ -75,7 +75,7 @@ import { getParamsFromOneOfRoutes, getUserName, } from "@/shared/utils"; -import { selectOptimisticFeedItems } from "@/store/states"; +import { selectCreatedOptimisticFeedItems, selectRecentStreamId } from "@/store/states"; import { MIN_CONTENT_WIDTH } from "../../constants"; import { DesktopChat, @@ -199,6 +199,8 @@ const FeedLayout: ForwardRefRenderFunction = ( const queryParams = useQueryParams(); const isTabletView = useIsTabletView(); const user = useSelector(selectUser()); + const createdOptimisticFeedItems = useSelector(selectCreatedOptimisticFeedItems); + const recentStreamId = useSelector(selectRecentStreamId); const userId = user?.uid; const [chatItem, setChatItem] = useState(); const [isShowFeedItemDetailsModal, setIsShowFeedItemDetailsModal] = @@ -400,11 +402,6 @@ const FeedLayout: ForwardRefRenderFunction = ( const setActiveChatItem = useCallback((nextChatItem: ChatItem | null) => { setShouldAllowChatAutoOpen(false); - setExpandedFeedItemId((currentExpandedFeedItemId) => - currentExpandedFeedItemId === nextChatItem?.feedItemId - ? currentExpandedFeedItemId - : null, - ); setChatItem(nextChatItem); }, []); @@ -422,7 +419,6 @@ const FeedLayout: ForwardRefRenderFunction = ( const setActiveItem = useCallback((item: ChatItem) => { setShouldAllowChatAutoOpen(false); setChatItem(item); - setExpandedFeedItemId(item.feedItemId); }, []); const handleMessagesAmountChange = useCallback( @@ -718,7 +714,9 @@ const FeedLayout: ForwardRefRenderFunction = ( return; } - setActiveChatItem(null); + if(!recentStreamId) { + setActiveChatItem(null); + } if (!isTabletView) { setShouldAllowChatAutoOpen(true); @@ -865,6 +863,7 @@ const FeedLayout: ForwardRefRenderFunction = ( = (props) => { const { getProjectCreationPagePath } = useRoutesContext(); const handleNewSpace = () => history.push(getProjectCreationPagePath(commonId)); + const dispatch = useDispatch(); + + const onNewDiscussion = () => { + dispatch(commonActions.setCommonAction(CommonAction.NewDiscussion)); + animateScroll.scrollToTop({ containerId: document.body, smooth: true }); + }; const items = useMenuItems({ commonMember, governance, @@ -39,6 +49,14 @@ const NewStreamButton: FC = (props) => { return null; } + if(items.length === 2) { + return ( + + + + ) + } + const triggerEl = ( diff --git a/src/shared/components/Form/Formik/TextField/TextField.tsx b/src/shared/components/Form/Formik/TextField/TextField.tsx index c176f14fee..4763f9e6a3 100644 --- a/src/shared/components/Form/Formik/TextField/TextField.tsx +++ b/src/shared/components/Form/Formik/TextField/TextField.tsx @@ -1,14 +1,14 @@ -import React, { FC } from "react"; +import React, { forwardRef } from "react"; import { useField } from "formik"; import { useZoomDisabling } from "@/shared/hooks"; -import { Input, InputProps } from "../../Input"; +import { Input, InputProps, InputRef } from "../../Input"; export type TextFieldProps = InputProps & { isRequired?: boolean; value?: string; }; -const TextField: FC = (props) => { +const TextField = forwardRef((props, ref) => { const { isRequired, ...restProps } = props; const [field, { touched, error }] = useField(restProps); const hintToShow = restProps.hint || (isRequired ? "Required" : ""); @@ -16,12 +16,13 @@ const TextField: FC = (props) => { return ( ); -}; +}); export default TextField; diff --git a/src/shared/components/Form/Input/Input.tsx b/src/shared/components/Form/Input/Input.tsx index 9b1651be04..4c299b78ba 100644 --- a/src/shared/components/Form/Input/Input.tsx +++ b/src/shared/components/Form/Input/Input.tsx @@ -79,6 +79,8 @@ const Input: ForwardRefRenderFunction = ( ...restProps } = props; const innerInputRef = useRef(null); + const innerRef = useRef(null); + const [inputLengthRef, setInputLengthRef] = useState( null, ); @@ -133,7 +135,8 @@ const Input: ForwardRefRenderFunction = ( inputRef, () => ({ focus: () => { - innerInputRef.current?.focus(); + innerInputRef?.current?.focus(); + innerRef?.current?.focus(); }, }), [], @@ -189,10 +192,11 @@ const Input: ForwardRefRenderFunction = ( )} {restProps.isTextarea && (