From 373a12a3b53c07598b9f26b23dbd7027f5206246 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 1 Nov 2024 14:11:36 +0000 Subject: [PATCH] First PoC for reactions --- public/locales/en-GB/app.json | 1 + src/button/RaisedHandToggleButton.module.css | 40 +++++ src/button/RaisedHandToggleButton.tsx | 151 ++++++++++++++++++- src/reactions/index.ts | 74 +++++++++ src/room/InCallView.tsx | 23 ++- src/sound/reactions/dog.mp3 | Bin 0 -> 11702 bytes src/sound/reactions/dog.ogg | Bin 0 -> 7104 bytes src/tile/GridTile.tsx | 6 +- src/tile/MediaView.tsx | 5 +- src/useReactions.tsx | 51 ++++++- 10 files changed, 339 insertions(+), 12 deletions(-) create mode 100644 src/button/RaisedHandToggleButton.module.css create mode 100644 src/reactions/index.ts create mode 100644 src/sound/reactions/dog.mp3 create mode 100644 src/sound/reactions/dog.ogg diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index ea849bf0..54525f1a 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -11,6 +11,7 @@ "no": "No", "register": "Register", "remove": "Remove", + "send_reaction": "Send reaction", "sign_in": "Sign in", "sign_out": "Sign out", "submit": "Submit", diff --git a/src/button/RaisedHandToggleButton.module.css b/src/button/RaisedHandToggleButton.module.css new file mode 100644 index 00000000..1dd2766c --- /dev/null +++ b/src/button/RaisedHandToggleButton.module.css @@ -0,0 +1,40 @@ +.reactionPopupMenu { + padding: 1em; + position: absolute; + z-index: 99; + background: var(--cpd-color-bg-canvas-default); + top: -8em; + border-radius: var(--cpd-space-4x); + display: flex; +} + +.reactionPopupMenu menu { + margin: 0; + padding: 0; + display: flex; +} + +.reactionPopupMenu section { + height: fit-content; + margin-top: auto; + margin-bottom: auto; +} + +.reactionPopupMenuItem { + list-style: none; + gap: 1em; +} + +.reactionButton { + width: 2em; + height: 2em; + border-radius: 2em; +} + +.verticalSeperator { + background-color: var(--cpd-color-gray-400); + width: 1px; + height: auto; + margin-left: var(--cpd-separator-spacing); + margin-right: var(--cpd-separator-spacing); +} diff --git a/src/button/RaisedHandToggleButton.tsx b/src/button/RaisedHandToggleButton.tsx index 277817de..cae7aa51 100644 --- a/src/button/RaisedHandToggleButton.tsx +++ b/src/button/RaisedHandToggleButton.tsx @@ -5,12 +5,20 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { Button as CpdButton, Tooltip } from "@vector-im/compound-web"; import { + Button as CpdButton, + Tooltip, + Separator, + Search, + Form, +} from "@vector-im/compound-web"; +import { + ChangeEventHandler, ComponentPropsWithoutRef, FC, ReactNode, useCallback, + useMemo, useState, } from "react"; import { useTranslation } from "react-i18next"; @@ -21,6 +29,12 @@ import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; import { useReactions } from "../useReactions"; import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships"; +import styles from "./RaisedHandToggleButton.module.css"; +import { + ECallReactionEventContent, + ReactionOption, + ReactionSet, +} from "../reactions"; interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> { raised: boolean; @@ -30,7 +44,7 @@ const InnerButton: FC = ({ raised, ...props }) => { const { t } = useTranslation(); return ( - + = ({ raised, ...props }) => { ); }; +export function ReactionPopupMenu({ + sendRelation, + toggleRaisedHand, + isHandRaised, + canReact, +}: { + sendRelation: (reaction: ReactionOption) => void; + toggleRaisedHand: () => void; + isHandRaised: boolean; + canReact: boolean; +}): ReactNode { + const { t } = useTranslation(); + const [searchText, setSearchText] = useState(""); + const onSearch = useCallback>((ev) => { + ev.preventDefault(); + setSearchText(ev.target.value.trim().toLocaleLowerCase()); + }, []); + + const filteredReactionSet = useMemo( + () => + ReactionSet.filter( + (reaction) => + reaction.name.startsWith(searchText) || + reaction.alias?.some((a) => a.startsWith(searchText)), + ).slice(0, 6), + [searchText], + ); + return ( +
+
+ + toggleRaisedHand()} + > + 🖐️ + + +
+
+
+ e.preventDefault()}> + + + + + {filteredReactionSet.map((reaction) => ( +
  • + + sendRelation(reaction)} + > + {reaction.emoji} + + +
  • + ))} +
    +
    +
    + ); +} + interface RaisedHandToggleButtonProps { rtcSession: MatrixRTCSession; client: MatrixClient; @@ -62,11 +149,49 @@ export function RaiseHandToggleButton({ client, rtcSession, }: RaisedHandToggleButtonProps): ReactNode { - const { raisedHands, myReactionId } = useReactions(); + const { raisedHands, myReactionId, reactions } = useReactions(); const [busy, setBusy] = useState(false); const userId = client.getUserId()!; const isHandRaised = !!raisedHands[userId]; const memberships = useMatrixRTCSessionMemberships(rtcSession); + const [showReactionsMenu, setShowReactionsMenu] = useState(true); + + const canReact = !reactions[userId]; + + const sendRelation = useCallback( + async (reaction: ReactionOption) => { + const myMembership = memberships.find((m) => m.sender === userId); + if (!myMembership?.eventId) { + logger.error("Cannot find own membership event"); + return; + } + const parentEventId = myMembership.eventId; + try { + setBusy(true); + // XXX: Trying to send a unspec'd event seems to miss the 3rd overload, need to come back to this. + // @ts-expect-error + await client.sendEvent( + rtcSession.room.roomId, + null, + "io.element.call.reaction", + { + "m.relates_to": { + rel_type: RelationType.Reference, + event_id: parentEventId, + }, + emoji: reaction.emoji, + name: reaction.name, + } as ECallReactionEventContent, + ); + setShowReactionsMenu(false); + } catch (ex) { + logger.error("Failed to send reaction", ex); + } finally { + setBusy(false); + } + }, + [memberships, client], + ); const toggleRaisedHand = useCallback(() => { const raiseHand = async (): Promise => { @@ -124,10 +249,20 @@ export function RaiseHandToggleButton({ ]); return ( - + <> + setShowReactionsMenu((show) => !show)} + raised={isHandRaised} + /> + {showReactionsMenu && ( + + )} + ); } diff --git a/src/reactions/index.ts b/src/reactions/index.ts new file mode 100644 index 00000000..9138f10d --- /dev/null +++ b/src/reactions/index.ts @@ -0,0 +1,74 @@ +import dogSoundOgg from "../sound/reactions/dog.ogg?url"; +import dogSoundMp3 from "../sound/reactions/dog.mp3?url"; +import { RelationType } from "matrix-js-sdk/src/types"; + +export interface ReactionOption { + emoji: string; + name: string; + alias?: string[]; + sound?: { + mp3?: string; + ogg: string; + }; +} + +export interface ECallReactionEventContent { + "m.relates_to": { + rel_type: RelationType.Reference; + event_id: string; + }; + emoji: string; + name: string; +} + +export const GenericReaction: ReactionOption = { + name: "generic", + emoji: "", // Filled in by user +}; + +export const ReactionSet: ReactionOption[] = [ + { + emoji: "🐶", + name: "dog", + alias: ["doggo", "pupper", "woofer"], + sound: { + ogg: dogSoundOgg, + mp3: dogSoundMp3, + }, + }, + { + emoji: "👍", + name: "thumbsup", + alias: ["+1", "yes", "thumbs up"], + }, + { + emoji: "👎", + name: "thumbsdown", + alias: ["-1", "no", "thumbs no"], + }, + { + emoji: "🎉", + name: "party", + alias: ["hurray", "success"], + }, + { + emoji: "🦗", + name: "crickets", + alias: ["awkward", "silence"], + }, + { + emoji: "🐱", + name: "cat", + alias: ["meow", "kitty"], + }, + { + emoji: "😵‍💫", + name: "dizzy", + alias: ["dazed", "confused"], + }, + { + emoji: "👌", + name: "ok", + alias: ["okay", "cool"], + }, +]; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 63e16d12..1982fa26 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -175,13 +175,23 @@ export const InCallView: FC = ({ connState, onShareClick, }) => { - const { supportsReactions, raisedHands } = useReactions(); + const { supportsReactions, raisedHands, reactions } = useReactions(); const raisedHandCount = useMemo( () => Object.keys(raisedHands).length, [raisedHands], ); const previousRaisedHandCount = useDeferredValue(raisedHandCount); + // TODO: This may need to ensure we don't change the value if a duplicate reaction comes down. + const reactionsSet = useMemo( + () => [...new Set([...Object.values(reactions)])], + [reactions], + ); + + useEffect(() => { + console.log("Got reaction change", reactionsSet); + }, [reactionsSet]); + useWakeLock(); useEffect(() => { @@ -634,6 +644,17 @@ export const InCallView: FC = ({ + {reactionsSet.map( + (r) => + r.sound && ( + + ), + )} {footer} {!noControls && } jgnB}u$!Wja^LxMFzkYwbe|)d^`n}in-m~`F>t6d_Ywzn`_vc<~-D`mL zes75IYOQnqjfKmhp|fAnyVD!-rfK8huOR}p3ZHhQEb>7h5OIBxkbV66rF!B>!NhTB zD^C|jXoc$4=0M;*H0qAvhv=Ud_I}x~?B06RG_mBv#fIbvUG&Zu4u=sI*RtnfYY7Um zvr(>!qn&GyDC<8*)JBFfrULL#SjQK0whTK<}Kg|e0)PTcJazd zE&zOEc+#^!-rOZPsTU7>NhXIyJ^uY-#{a?v-#j!%WS8CV#$SY>FzQ+FQy*$ugt2>M zi?Z6o=`HBP`)&#dP`3j1L83yY<&6#?uEu*wRZh8OcGd2FajM$6wq#ysa#*VNyj!X6 zZ^&*@Vli(Z?M~sb)Sfg|VXHeO3SKcnQeP2fQ=Oi4&-4Mh`Fu!BO+691FXxSD*QUAa z%M#a%5`ttcD?f(7+MD#R2d>l9pNw^^cKlUzS z>x8|%)t$Z5pHKMM=nw{~Ua@^1JqhWQ34Ntr2c}LLupGk8{}<){iymwLL-&VcL5gE+ zIv#I7{vEZ$^8Vw>Gt~e9{GBvEP`A4O)DLt->Ui{{mA@+i;G=C3v{7*QF)$I32TV-V&W=`j|r^5@m7mQOxx)1y3_(m z3}j8d8imL|B`xFDA$;Ysl2`ZBcFYQ8%R_W*yCEaSXt+oNAqr;RZ?c^cqG-W?5Pa#6 z@F-(>%re~Y>M$V+%0GlP0q}Jgs4U0vSAE!0V+%Xrw+R&YpW&4f^_z;0UQOeL2>KKxIYC!?i^l6 zq}PC5jr*G-Smynppa&wonPvVNCCg|5#o;2wv^A*gky*SU0Q`ZWE21HM@qBOJWtb+6 z(=i>({crr%3I-9iyT>B&?cs&g8n(g*>)>R}I9DMyFMWTJi)q4o>73*ab#r5N^9pQx z=vjMdN{abhWp1T}(`CEfH~4$lGLOBxLHWkhrqql5NH6`YAZKM#zuhCSgL zsj)NB%Cvl`xzFO1mQHLvp)xLsh@eo32;(Q<-9}w3%hPRVAiVNzja0o#H0K5rNi%he z$$pcEQF)HFm*eE2RAMB8P);pAT5NK6fK5xcCl#u4R|{{y%|TV-v{3Bq7Ec?*b_N&k z`QL4F$ladc(``9QoEFwz4@U*NAh4lGe-k#$${$BniS{A7L6J0^jY^4VKHT1lMqq>Q zy#W{RC*^~UH2>+0FOKxjhXj-I?2VN)S=&-9t8)O$%8#Q2iuCbJ*o^IKGi5EO3}W=a ztu@Ja*pv!zROxV)qShq(29|9E@QBo?>ziX6c=jPITaQQ$PRfI;q<_cQ6THAeY6Ou= z9^&LN#aoEf_T+N!5VQ}p8v>ap?2-yGv*437@TE0gEk80;GQ-$P1=h0p=SfR1eYNoP z3_`}blkP*oPCUzES?7Ao^kS!N`(4rOmwt}wuecgAZ>=5G`x8YoI$@$2oFf7#jI(Qv6fY7ZpT1MY20qJ|R{k7iQd& zxaERjcmP*{Tt$34D^!Mc_Em7?asq}?-lE6 zhRr?ehROURG_Z30zmnSao4UKM;VsgCwu*XNiaMF(WbQS|l-+W{N7k4y{+tkX!6UoU zK??;5>YnARRe3AiV54^MxUgC#w(H&$4`BsBxd+dEv9`1;6Ya~qpZL9c5^YK z6TAC1a8Jiy_q7(JPwej9t~Y`Qo#vUJXZ>|J_TVkd=oV@;s$_S&!c)${1oIHyp06Q6 zn9<<0uWPTl`4V-P17}Xq!i;P_)y2H5cef6aO1s%MDU$MPN;YBTp)GmVP%8LCw=H?& zTs(T#=w1I1S}KfNC>8eF$=2kxv$jhPMQo>*8hs~0d=NRRMwby+)D%D{9#Og(Y+sqp zc}y63t5r!QCeYRNdgyv7bSwb8LZ18MN!2D&e?BHuQq}AaZD&?PeQNfMS33tzgadEt z7JzkNI*w4o&d+1-5+AfbL$A#4(-%IXmXT(h(cEjCv8}8{gR@#V8@N3oRZGDJF-XZC z-c8Emw_&paio-t8c9RNNRI;xo+N2#rgraC|SX@Fo+>6MU*W=6tSjw zW>BRF*eaa!J9Z2h=-*lS7y!n*&mjhNz?ZaUk8R+naC>8|9-Iv;pe9uZ#in%!q1fTZ zS}i^x!wQIzqLB(FqG9-NP%T+zY(znw(BgkpO zREHPoNLw1QmtRlwPR~l3C-y$C_3baiN`lTc?)Cfo$G z_j=~eSh*S^kjD-AlS0SQCN&ehG*hP3BEXRVQ2^R%k>5a(`2dH1_wwV0wZ3J;;3l+rx zi-@MbThnXk-#8lJhqR_Q8FL9vrz>*N^6+yFNq#$Qz-~mn&*l6;zTt zxq0@JAMO+Gb-uNE^iG^tBH2m)WDuAZRcP&g&F+kAFn!Wd3kUv&Vm*%~&5b;G%h5!v<`PA-=vcr*^aO{_==q z`tiY4;C?hpmUm@`gomR#zk#O)zn&L6Dc_qsK_bzbP#ns|SV8a5yhdDDv>NAhCtB)O1N`tcHkifD~rf8T7=k!jNrV(J8H zf_cs8U`^_^^EO#c!>GX23}W4R2)AVfnsbMS3Y2Lf*6Hj}0^pJW5OaNU3#BL*5RtKk zQREYELRpV<$nct8f9!~Uv7feQOa-wHt^;quj!2`4H9CAu8eRwNX&5TaPe26%Jvr+XViA?f#ob(@)+kI;8#SH?whtlYkDB1Bee zj8p1Ha8|-kTWYKh_4tw~&ln!gE1TCnxRnLkRzZGGN5A2@QwE=4yZ?d*Q3|4OO@$2x z`%X$UEVyU}OEhTKI87l(1LA@l-dNpj%!CJFY&_i(oMtB6;_T8tYJs!}7ji1xZmGZZySNr=`mmJS%`A{u2v9uOV(QWO$Rgqm2l3?RUM6opTL4Bl8d&r}R* za@%M^+9N212}Y&FX?A5wu3mO(P<;a}6MQy1_#-lpn^t~fm z{j@a28%HI!F%&7no;+j<-F(nra3 zd0K^|I{|EHSr^bhk}Z+mDCBPN#woF*JU|ZBarE&}Db7z9?nT+Du7>k_7d#DXUBbmi z4BAPQJ{qLhl-v3$_SUZ9 z11}af^X>4#XMR$rTBCFM<)hW1b2Qi;K6ODcel3|cL1fdf>5~a!QYVF&$XR}S7?$5& z^pUHA{?6ZRHw%y1)lN~>A!S@uAP?|)REzULsI-p*=h%;!Kn&6Z;+G5HV|%d*=+--q{4S2JB{nid-R1-`ofHj z3O#@o09B!dmBRvBvx~bK<3Td4lS~-~;x0#plDI>~Q#?Ar4k{W`Y8_K*1)u2SnRfDs zeTY&->7p0e%LCjuuQ3KQ583J}TMk~#Oe{W{8Pb|Pbf>)dt}WsX1vQiT6NB#QA88Tu zvvriS%6i~F*7y!{w~@~wVQ+$?>ZgkQPnN6zMNDbsuD@J|6)XelP+7=IsQ`B81LR!+ zJ4Z!P#GuU$UDuKFXT@3B%*vobvz$_4m7)9bTa4-5)XU}W()FkAf4r|#j5X3-XE+L5 zALQBGC+vtu5>z2Ego7}0fPfYA~Bqp|Fw%cgEVX14eRg%m+^RBp9MAr7+h*!zosUY5=N$4?1&Ss>;chow2>fwawn?gV=2V>Qo&^RXB>e*V2JR)FB?mB>P57%agr2 zD6!EUNa!wQe72CPZ4%kb#Z=Bzj;Ji9wT1dP44yp)_7*wxT7dG=AP5o zDj1Gf+B^2ZZ(UBH&Z_VLI}H{5YI~@v{|29t%;Afoc z-Rs0SI_0M{4}C0+3U{&UTe9mt^LOG{qS+V2f)`GIDNtNu_Yz5tGo9Dd*qs^e9o|;C z&_Xrg;Pwv!5j|&!W!!yTh@o}^80Uv@aO*cDtp*lDLwmKDnqget@0Ie`gak+E5$2!< zyCFkJw83Pw5V;6(FIH6qnqfi7!pg)R;gC>5#>H^wllYPi6x$xELWosTHUXUoUv?7B zhm=9M$Fd^RGrI_(7@3$fem+bWc02@;M($LIri}~X_2I&D^q3YL^&VWjHBJKny^b6y z^BBu2e9@DN3f5zp8v*M^&EJf*55GGt{*3=5qRK%fL@w#eghWfnc*lB#>1D?K%6UZ9 z{2dQh-8U83rw1M@zL+0yE5)2@u#6&id^_#Plw?X)VNPP)IoF@>ZNNXrzc6+CUH7a` zb<}RoYa{%2$H$qUEmd1N8V7%rZT+g~*ODMc`UmD6X=MPLN3B67VnnOR)$iLco^+CK zV@~teXHis&jj84VB7t!KKSqQg@(DF4p=FIvz2!aM^R_hjcw4H$p?>elFKu$D z=+*=Lwrwit4Q?Rxd#66>%lI^D{OuRgm$puU+14T0^Ol4iauuSJE#B}wgkxqngh6~= z4V`&6X3*t?ULG?sQ7@qgP&biize%OEx9`G7R>{?(Loho-loGNUn2F$gew!~LPfA{l zC#3{V)++#j04r``Ou)bb8l1>uZXY+|nPS#>L{W@zYs|2T0bE9y2`vI=h^iLbP8bB% z#d=RmcdnB^;EGvzW_$>-sPKv5d!f1Y=ER;AB@g>8)B=Nb8F&C)Fk&7A;D5FJ=|#Rct2h;0lNr|)DB&*Ae@|i@&P7>-qk(@6Irc_r93(@ zXC>7z^TOT9PbOmg>W4N8n?;G3(UZ%L7e!~sdD&sJZ`&1wjuzoMbLw}-k5?J1$Xk8U z%qRtu(bfj8Vf=NASssebRLMIEuIl*)1ESB>_Y+qWh^ycp2kL%cB{=E(-p0kGg~guFY@=HOxQ@Nb9}F!9W9*Z8>(6`w#__WPGFKMp=Trq@`Gfzdbwa*vGBd2{mEXfR>n?V`xEuz(eZl-9_2&A(4gp-fjrrkp7wjFcR{GI zHfbES576Ak(0!5VxB7<6&4>m=?spr1bB(Z!guUm?K6ayZ{Ibb(TlF`M`~Yuq<}4=hTw|}l z40sF^(I$8NV!h|6A*xw7u}wR!zqHVVIlppBQgfZ#sZmJoSj2ejs*AOrK65l2o&P5| z|4%v9tLI*&r_!Q(X%;U--Q}En>z~1yQb$T$gJZl|!s(3J;Hrr;249e>M?b7s(R4zu z@BbWkyq+#DbS>&L=ieCmkIfDLw>u*0ynLff=;`gMw|#ersMm80Z_7D#P;QNKBjNpJ zD@3Og*83IOeq(5htk}=*{{7{Zur#HE>YrTOk17d7R9x@5&br#8V@{}%Hgr%*8VZ+V{1`sRoFxI7sJvVXh z7yT^E*_dzeaGv=+03ME5W?iBb>S3h&2zpDPUE2Eme}!{qea zLmi(VrIaDAcf28qBh}kKi1~A|c!zb|m={VfSvRVHLuqGp?a(a`A@pwmYCC{&~txQ&h@aEsv7ElhLE!te2e zn;Qy$pc`AXKJM0HvL6R@1AfrJC;Jgk82>2gH?V~XjCtaA02N3{1$784Y;NmKcI@ZD zt@LF^vHwj=@j=?PNdZG`_A5k_;)w{;rN3DoV)sQOPmOnWHV)%?*APv5rb}Q1s%1=i zl9oILHzXuaoy7A#znzUU)WY(f-Q$@qJ;(DhY4j;?F+`JG@>Fp06rxF&MxTrzEu7Ct zKlR4Gn%qJOX)E8P(I@+aPiDIQzmK8+2D1c@_Ih==7Gv=2E=No|cUCB)`>gKsvcQ1M zjgjUXvOiU17z&AZ(zyAE^w#3&ja_K+%gAjou_`PeLlXU|Vfb5!GD?yNQ=Fi^gN2i0 zDq_u!Ff~4d&J?N$2^D~tk@@8_po-#rSrG55<{Q`&FilAC1KjDfU(xYBvc*`Ib7`HSOKphkgrG=Qm+IWD(Ulp4vTaza- z9*~Y-)%lO}DL=#(^~6vAap37$4Apd9TeW>(pF2X#y|AU1l=ZhqssAUMsq)NI?@;#X z|G5~_N^46c9pSwoy6p8;E!^X0j%@b{6%GzZ49WSPyb*euU#**d^Fg;~Co|NmR&pXy9B0P4C%GwkxaAWKzO zxqJ4dXM=d}V*9>qG>+^Zm6U&k7!ahU1E92!VNV*8JLFzcs7DHUDC5znvm>7G#O$dT^Exl zSsvn8UQBvY1(x^1Q?dhYcnHf|yDHenKz=JZCb{Xj_KfQ8^M5ZO4WD)yEbF?p%XmVqaw2h&<^y%}5^}x6sjWIiY zoc{?yQtszFk+s*c5x4Eh=o6HTBx5zJBZ`WpYQgqb6eg7;&jBQ3MSQD#P8QD7Bx%SR zc*W0kiZo?TsTq>MNZgVv#?z!30J?{cbq`ronxtjSp{g>HhN!m_lac$j?lNWfjKK}{ zq<9{XGt-fv?Gq!%JAUzqpz(JRBO|d*MqoGv)RBA9gH5Nc?pa{%&&AWCON}}3h*yU{ z3Pg07b9Y9JA(tMoBF`Q`Ek*ZXV?WhYOCgsk6s8>Jl=@xgj19F*jHFOYNsPFNHrDgg z@1aXgL`UeMvtyS8-89P-ra%GCb5F`Ct-l3$ANWp@P)ixrI04>USfqJ=Dh)i+C+V^1(ImZ?PQ)ZC4XsUL z^Ci&{<YKwMuW zZFt1Q0ZhR~97AIm9L+6^y)Ms`8hQPX>~gfHEU4Tj)Y2`~QiC~{W2pHNRNHu;6lN*U zoU4K04Qq0P(^COfq*tt=ZWaGj|PwRBTqvbaw=$mzD6vjZ;|uV`A4({6fp(vG7rf3WPl{#r9M90ifHz860? zH!#tgmTsU!3iD5y<6q90U9-M()m+@*uER zdeaT`GG?a?4$W?g7#wm63n`Qd3(>sOA)IFK>wOes0A5;7CUYmm;!P%1?|R$d#7d8D zUl}7e4r*qcD5$XODgEAk!Ozzu*VnEmPW_+rp|9!jdefVdZKRa$L*ZSJcv%Ysl;=_Wevy)3n{TkA+-t;wjzJb5X zci9qVzY6T$bMI{gZs4oAuad3io!&IPC@DSl*>6tbVd1f9X-Z*EeFBBMt;QTfR4fj&sz>pQF%(D$|pE`!Vi?jJ1{k8wY^81Zy+!1A` z4Q%ZX*Ywsxblvb%qkBi{>Q(ba9XcLd5qg&?V0|f*8hd-t;k9{2HdU#v^6<1Z_=(^; ziMRbyT@u;*zPn1gN-W%hJo0=F+V+9lGu0`mEp$8QM%U{bS0sMg{s?_Y zBw>&F+q8YEOZ7PN$y| zXWPh&&dvy-yEvCAPaVtM$}4(&p^?_fi%;dLM~}H&x3+wrI5`?e-p>u5WboyS)!9YK z->1yG>D#kZQiZ%y9jB}!j+b1YcKIYAK!z#YthqK#DeXD0b@T2cV@tI&2Tm1qW<4%< zIF@jHh|A&}6AbZsDCku#e!#{c{+3p9rX4aHSnoI4N}C?HY`C$|{&S#gHNy`#biy)i z$nSZ08>aLFb@foN;6fl#Q_{}rSlWR<;sN?MD^h$O`-uCO+zHD{FWJ}>vK>jxM?Q9q z&sVmLGOLxG&~eOg2vppm*VEIzz0U4=4cy=A>AAn-zX?s(8jBMQ+BuO7L3U+EE)ty?WK&yttv~Nk1J;)jWep*L$<= zY6|7KSrt#RGKGxLxnJkSF6vmBmj>r4#JTF43%2FiB!+aS8YS3O+vWva-lj8HX_IcQTpx%$eoP`OSIH zoO%5>Zw>-T@F@xHcq^3TFa0|_5X%tTzTO(0kgjAPnf1yC0Q^>j>UkRBuN?VDQjS!% zZ0Y=Bv);Oo|DG19EzE?59eksAjJ+m?4zp zc{{u_5o~tLGFp&9aATF^X}a~|1fFhi1)9#Mv4R1&S`z`u8MR!1r^OnA5&)`eY(+J; z+8C2&ZO{tY%xkFR09A6K*4@2k(5<@pCu-Qimg@o;cf+apjnme zRl`{T1O%cz(y)tE<6;3oRXd4fAtG74Xhv9!5){-U0UrQ$D2C=9$8t&ze;lD66&S&XgY-?1q$o&+P3=%pYA; z{Zt@Bed(BjJyAarQNuIwcu&1(6Z=`Ww~r`VCV7>NxuHk#(oz?cby*sRyaX1fu4hmjx;US{0Q^32dvJHp5FBi`y}Sfo->9{?Az6;W(S6i+)96TIp1y(B=_RKAg- zd$z^XPGW+$*g8SHIlH{}pk(%76-lA$0x-%h$|RC!iR!XMwOPfFgyQ&v<+XK^mHqN3 z?I#}pr@UBY-~<3N2wqzWUL1l42NEiD`Di7&W*4ahkFDf^VcLR@Lk4RhpkHPzIE_ym z006qL5j!ws=BIBx%CX}G*^P2MMuT|&Dfx}i{0tzlTA&00N-#loc0CfULPGR#;_>Pj zH0Oz|iACDnda9qAk8AzPWLIe*=DRvVjL7YZopg|B(=f(m$r7EC{f{Z3Hg@-mqbiCbaysNemmR z6EGfpDQt*0`QsgeJ+9L%L>P|>o6gmKLQAHeT};!CU87r2bQ&Kzek2G4K(o7v>}TL1mL^D6hzQ;zuKw5r$xL=tLf0!-rP?*|5{yS6 z&96e9pdeBFc!n@uxub+VPk!kh$SP>n zvSE$_FXeEEh5{~|WPP-hlSt%YIK;YAZ3fA@j03=UjvD;u2gLWX0e%VKQ#_T2_IgVx zHs+f13O$a9I#aoyO??Q%Aa9~EJk4jMrK*sJ!&dT~i858(Q_5k(NNGKrXf3$rwnA1q z4C5mM0Jti&kM=%Pw{la#%|QTkaf}jfh*UX2wOh6lxsCV21}OS|$FmG>qY`=r>82r! z3;cMzh-R`1%ei)n5;h3S0%+~4lEam##TmCXz#V~@ZMc~kcdI;yeD=hXiDm$vt1kl8 zNYh|1C!)z>GA-6xd0x_Sc_lC;kHavuEd-?;6nrcGV*dJo1x>!FX-pYVjnOy50#f`d zs_LHwN7?#k^?yX!!Qf04q5Y-M0PlHD;N2JFjV@i@Lm`oHM-hBz?wvRpE*;Gv%D5=F z0~y?~!X}20Izy1&2utoxzvgCZQ8pG$u1eQs5Q*KvF@Zz@hr_mO3n^qDIV$F`8%9Hl zsCCk@u)>C(=pt`o%Z;!?s*zQ)UtQ~X;_)V2OcKei1s7A)joKKSY$u;c)agN4g_G?@ z!;&aHxT)|$S(~Jgy6)WshfE#Y0KhkSYJi!$XLUW{uwk+0Qj=zV0CB@(ppihEfmR{q z6gfky&}E>Ga3N1cS(Fwjtw(kL;jkf3MU`;oK2aDp@vw4)@`^;1<*8aRY+IQW`{~A1 zCoNS{90t@DZj`E@wG>9UN*?M6+&nfh9}bh1LY0X!eYTdhK%YY-%CKyd1#E;oE?et} zKpz^X5_>oSn+SK;%n1g^IJXtXJfoaw7KO~e3se;n|T>FPu2AVNvmg!5>v@63gdT`S5tv*N6b)i>u1AKl70BY(Q zt3l@-a5o*3fmHW01$;bnlaP6lk(w2th0zN_(4%u1s{l7-gEV3T{QzRsqkAXREYvMf zZe-nRzPedu>P3UEv$}NF4-!e4csMTrI^a26;0B_4(cPK+SXDeiEmA|zvcT-x~aEWyU$X!CX60cvtBRh zMrvw{i{t3MVqM3xHJfPYm}!Gz%n!(@(vPngR?Q;>EuMAYcVgsj!V?Nw2a`~r-yCSw zan`2;=feYE>9y&!UDcT#$n*MK0`>I_||@G51Q#)-o zoU%<^Ej4DqLG;O$h4I6z*R@+n2kO@ZAKDOlU}eJb{lQTWwS70BK>N)fU%3Qtcs@C^ zzvvcw-r;RWvEkAWKmS6AY2*BWXL1#9n615oBZ(T01K!2q*Fw)!ESQ$jafMNghoM#Q zp}Uc_MEQ|woL;xfi$Ov_Bsu;NVXE4WOMPtq=s?w`94&Lb`RvnJCKTExO*p%@^BaL( zYvHb?;Q9BKV(~CHGfX^f8HKTox{_psNRFo04i7JFy35-W^Xlc>2T${KJ|y(wU!K-j z9*?9i3!^`2;4~F%&TQY)Mg-^Ib=>FNAU}BU*F=#;2N#y!iDhaug>(nFn?F$$OcjrE3vuc%MAkyBnwgA-Hvl#r3vorSWgP+(7~vEpv)Q&fzVxhFZCy zU%SMJEO58GacFJ&B|9@YPBZwCJJ} zC|wUf)2X}BqOhQmAI6{K$~f8D;Cn6k-47Su?>#Pf|41*_hoUp{adhVf>lG>8Ud0^$|1hsp678V^48)^e;uOco+ySPMF+p1s6a~pT#R3pD-y8Oa)TCH|1NntwlW_U<8U!JRd;pOVkQbn>RJUx^tz7~F->fRET+GtW7JfB!*o8-V~ zqKs4+)1W`d@uk_UTg5)6&<4ZvL)#7&E0DtXFLeU;-WIJoI~C2%@HsZZ40W!NSy|pg z(n)3O)$T0a;r#^da;5MJeyxJFZ;s1HcK+ix_xCrwb{~+xPd#+Y-SmuU8o;NVs>3Q~ zw<$R0*>Rv|*OtknL~uJne(QKk;o0sTYl7#v+@P$4V+S{VJ-q5?{kL}8K4y{>#EyzC z4KS#CB|FH`Hp(?@K?4xG&k}rd?&+yYhDYEP@*b1)J?d^U>F80vHKQ5dsW)l4dtljZ zX#8l~Yz7vTwFTXCz46$cA$8zAS#OB@cJc9ZMqPcHiutzMDlQ_9*YL)v;$`~Y^S3Y? z^E-6wM4wGnMU=ITW?(xj0@PU+vKX0swxEs3n$!T%XOhFOiVXeg5?A{0rF4BzW$bwH z&CdCH%F{6kweM-c@PR9hef1xcHs`0E8Y}YNwx;y9?-k$B;Nx#f5fe58j*8fTS7iu_ zt}nz|nJ(dy>LVLGu<`YEc@?@eZ6G48`wFtZ791}gBlDzhvJPOuMER!LC#8J9=Z&e2HKbKhKA@qp~J2f&@ z;@LXEm7Q3m6nI}9V5XLB`WkTz8eUXx780vn6qcp<3b`yk-!!~Ad&PsviH0G4eS<%% z{}{6DJ4wGVs@X!kky@yn2&U*DcLdjOWQ#$4bY@GfNINmiTHnRS(QSL~_-RUX zYR2d7e)~$>c5iz-Q9qK%{DK>@X`=S!)w|^+(5ZvV=f)qI=gw|h55Vy1J!}p7;t?yu zYL1Y4XHXwhIAK?UI(aGg#Kw{HY{WYpLuhOi6vwp4F$A63werb?L%FYgG2QS|w->o? z-1nE1CnHW*FB}sakMi@bmj2YUZqLYv???yt>Ad3`k#XdKq?kzk0!-1D&A?{iBRzhb zng?ZcQ-x8ZFwX;Bcy;KXTLKY_j_bg~#WdxZM1FL$gN(NJP}SjK#}t-ESb~M{RE_&` zOa$WYiGfQ&bArKV-^g+lCzd4)bFn~Ex_L5C1DqGrrZg2UGExIyJ_*_9&~a+~xed>- z^xG}K6jYCFk#_i7YW%Ez``vcipzFA+Vnz7F;~w>(kBCDD_4T$u%OyXn;G~-5dfyKrLSFTg1>dwh{JtG+o56 zbEFUq{niX~v_GeFP&2Oea znAK#LbBQi0V}WY`6iolRoZq&22?CsKnPN$`NZtBX6&-1lk1A|${A12{Jk{TK-MiJ% z`)%L9cejZ>eF5Q-LKr`I&LSZnWGq+)K;}|=BAHAFRNNvep=dWL6&G`vpuVV>X`$cV zkYOs2CwRr&aukP|lzVJ5?Z6`u#<}@%C;hOk>nrlzh{$6b^hcTg`zt7s2dmS z+my$=7ZcMpemb6Cl<{4LskQag`s>AxL|gW7S#H3J+ZBW=9K7d{rNPr;pGoF}xTDae zMhDoY;sgYtOV;)^`Js0UFyDSvaQncj7sJU2@Xka!j{*4mE*1bNCQSgn^_&LiOe3Ik zDUIO!@(%L9XjQh&Uyq*?_J_V~em&x8+4p@{Tehh3F!^v`m&mH9d%<7SR|u#gKg@Ci z9df=KZB43+V%V=8H$ZZ3s&`1$){o_%9q9#H+ilt!wPVj%YzY2mG#vofb5$@d zeof);I8>Tv2VPT`f#-8e>7E&-S2cNhCc7Gw8vGIK%0JA6p2zdmimcasF}ZZOjy`&Yxh38aN(z? zr<@;-_e@{Bxy#G%S$g-rS(q}LNY~e$U6iGL)r|~RmKu027izwv+wd-IUjl~s=U?U! fPMitq6>GX`@a4a1Bv&I(=H{tqokunPNCf`{*aW#a literal 0 HcmV?d00001 diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index 06c47382..127d87aa 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -45,6 +45,7 @@ import { Slider } from "../Slider"; import { MediaView } from "./MediaView"; import { useLatest } from "../useLatest"; import { useReactions } from "../useReactions"; +import { ReactionOption } from "../reactions"; interface TileProps { className?: string; @@ -91,7 +92,7 @@ const UserMediaTile = forwardRef( }, [vm], ); - const { raisedHands } = useReactions(); + const { raisedHands, reactions } = useReactions(); const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon; @@ -110,6 +111,8 @@ const UserMediaTile = forwardRef( ); const handRaised: Date | undefined = raisedHands[vm.member?.userId ?? ""]; + const currentReaction: ReactionOption | undefined = + reactions[vm.member?.userId ?? ""]; const showSpeaking = showSpeakingIndicators && speaking; @@ -152,6 +155,7 @@ const UserMediaTile = forwardRef( } raisedHandTime={handRaised} + currentReaction={currentReaction} {...props} /> ); diff --git a/src/tile/MediaView.tsx b/src/tile/MediaView.tsx index d8b03dc9..7ba04a7c 100644 --- a/src/tile/MediaView.tsx +++ b/src/tile/MediaView.tsx @@ -19,6 +19,7 @@ import styles from "./MediaView.module.css"; import { Avatar } from "../Avatar"; import { RaisedHandIndicator } from "../reactions/RaisedHandIndicator"; import { showHandRaisedTimer, useSetting } from "../settings/settings"; +import { ReactionOption } from "../reactions"; interface Props extends ComponentProps { className?: string; @@ -35,6 +36,7 @@ interface Props extends ComponentProps { displayName: string; primaryButton?: ReactNode; raisedHandTime?: Date; + currentReaction?: ReactionOption; } export const MediaView = forwardRef( @@ -54,6 +56,7 @@ export const MediaView = forwardRef( displayName, primaryButton, raisedHandTime, + currentReaction, ...props }, ref, @@ -101,7 +104,7 @@ export const MediaView = forwardRef(
    {nameTagLeadingIcon} - {displayName} + {displayName} {currentReaction?.emoji ?? ""} {unencryptedWarning && ( ; supportsReactions: boolean; myReactionId: string | null; + reactions: Record; } const ReactionsContext = createContext( @@ -80,6 +87,10 @@ export const ReactionsProvider = ({ const room = rtcSession.room; const myUserId = room.client.getUserId(); + const [reactions, setReactions] = useState>( + {}, + ); + // Calculate our own reaction event. const myReactionId = useMemo( (): string | null => @@ -184,7 +195,44 @@ export const ReactionsProvider = ({ return; } - if (event.getType() === EventType.Reaction) { + if (event.getType() === "io.element.call.reaction") { + // TODO: Validate content. + const content: ECallReactionEventContent = event.getContent(); + + const membershipEventId = content["m.relates_to"].event_id; + // Check to see if this reaction was made to a membership event (and the + // sender of the reaction matches the membership) + if ( + !memberships.some( + (e) => e.eventId === membershipEventId && e.sender === sender, + ) + ) { + logger.warn( + `Reaction target was not a membership event for ${sender}, ignoring`, + ); + return; + } + + // One of our custom reactions + const reaction = ReactionSet.find((r) => r.name === content.name) ?? { + ...GenericReaction, + emoji: content.emoji, + }; + setReactions((reactions) => { + if (reactions[sender]) { + // We've still got a reaction from this user, ignore it to prevent spamming + return reactions; + } + setTimeout(() => { + // Clear the reaction after some time. + setReactions(({ [sender]: _unused, ...remaining }) => remaining); + }, 3000); + return { + ...reactions, + [sender]: reaction, + }; + }); + } else if (event.getType() === EventType.Reaction) { const content = event.getContent() as ReactionEventContent; const membershipEventId = content["m.relates_to"].event_id; @@ -241,6 +289,7 @@ export const ReactionsProvider = ({ raisedHands: resultRaisedHands, supportsReactions, myReactionId, + reactions, }} > {children}