From 1a452f52ca175bb8be82dbc46190a8966489cf43 Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Thu, 5 Feb 2026 03:24:18 +1100 Subject: [PATCH] feat(call): add sound effects and incoming call notification UI - Implemented sound management for call events including mute, unmute, deafen, undeafen, call joined, call depart, and dialing sounds. - Created a new CallSounds class to handle sound playback and state management. - Added IncomingCallNotification component to display incoming call alerts with caller information and action buttons. - Styled the incoming call notification using vanilla-extract CSS for animations and layout. - Integrated sound playback for incoming calls with a 15-second threshold for dialing sound. --- public/sound/Call_Depart.ogg | Bin 0 -> 8457 bytes public/sound/Call_Dialing.ogg | Bin 0 -> 39068 bytes public/sound/Call_Joined.ogg | Bin 0 -> 9258 bytes public/sound/Mic_Mute.ogg | Bin 0 -> 8073 bytes public/sound/Mic_Unmuted.ogg | Bin 0 -> 7416 bytes public/sound/Speakers_Deafen.ogg | Bin 0 -> 9861 bytes public/sound/Speakers_Undeafen.ogg | Bin 0 -> 8528 bytes public/sound/invite.ogg | Bin 32667 -> 8054 bytes public/sound/notification.ogg | Bin 11295 -> 8508 bytes src/app/features/call/CallOverlay.css.ts | 106 +++- src/app/features/call/CallOverlay.tsx | 572 +++++++++++++++--- src/app/features/call/CallProviderWrapper.tsx | 4 +- src/app/features/call/CallService.ts | 230 ++++++- src/app/features/call/CallSounds.ts | 157 +++++ .../call/IncomingCallNotification.css.ts | 119 ++++ .../call/IncomingCallNotification.tsx | 316 ++++++++++ src/app/features/call/index.ts | 2 + src/app/features/call/types.ts | 5 + src/app/features/call/useCall.ts | 24 +- src/app/features/call/useRoomCallMembers.ts | 19 +- src/app/features/room/RoomTimeline.tsx | 222 ++++++- src/app/features/room/RoomViewHeader.tsx | 128 ++-- src/app/features/settings/general/General.tsx | 85 ++- src/app/pages/client/ClientNonUIFeatures.tsx | 29 +- src/app/state/settings.ts | 21 +- src/index.css | 25 +- src/types/matrix/room.ts | 3 + 27 files changed, 1888 insertions(+), 179 deletions(-) create mode 100644 public/sound/Call_Depart.ogg create mode 100644 public/sound/Call_Dialing.ogg create mode 100644 public/sound/Call_Joined.ogg create mode 100644 public/sound/Mic_Mute.ogg create mode 100644 public/sound/Mic_Unmuted.ogg create mode 100644 public/sound/Speakers_Deafen.ogg create mode 100644 public/sound/Speakers_Undeafen.ogg create mode 100644 src/app/features/call/CallSounds.ts create mode 100644 src/app/features/call/IncomingCallNotification.css.ts create mode 100644 src/app/features/call/IncomingCallNotification.tsx diff --git a/public/sound/Call_Depart.ogg b/public/sound/Call_Depart.ogg new file mode 100644 index 0000000000000000000000000000000000000000..ca926828e1a4395ab7dde87c36930a0faba2761f GIT binary patch literal 8457 zcmaiZ2|Uza_y1=sS+g%AOJkWBA!M6MA;Q=)3?Y;?#yXagY+14sL$YNXy9pt*XW#ds zC{o!=rP7A~9lqb^`Tn2Z@Adrd>&~6~x#x4wx#yhsIdd*1uC8Vf4fIzNx6Hjuot{4- zSPT<^1$q-4-2A8uFzssU7igDZyI;*P6Y9=?HR?_(MQqL0jzeST|7$(8dyMu2y5Yq@ z7iCpBWfeIEd3oxFgW%)r<>>9?Oi%|zM?h3j0z?IbhnvIRJ{bMpH%jZY1q@;Y8x#x? z9pS^NI0)i^pbHWxwsaSL6p2_U6_iEPqs}_y8leO7?Cjf zB4f&}JaSaW%-jeET7a!(pz~y4z+_;ERaUf3bBI-Qw9Qns?Pl~vc+6k@N673ZwePBq zfI$V~=*+jMBC!KS^XMYlsF-RR$cbta1XG$ADy>+*)Wx0bmeX{ps>!pjUZ$^}b@vqr z2411I0=Mka|IuBnGM)eLu5qbb8qxr^?D7@r@|DoRN^}uq7 z&aMYN1_j;%U!r<%cNE+n&h{JBXU!(<&4Rdc7-}j;!J5EgE?cL-Mev^6Xf{K|i*sPF z)o}Jgc?%Di#SwEP8(Ylx`27Wo3cK9FXXrOzN8HOCd<&B;nEiCh{W!oxUHeb?XaK)m zoPu}eT}Ol@?{nZ0Efq}{@lRn(FHq=4m53xIpTSf_p2Z4ClKijlYL0-QD2`ne|2^zN zc@M=UY4Os%Quq25`(&s?b=%;v)!H_W)2s>r#WKnO#Yv3|`F>=y9-h=lu|7;9VlgPv zUPJ+>BAik+_X+F1WAXxpiP*~yUE$s~LJnEwRUzaj?$K@+tblSGF^g|6I4V=bmV2mc*8zC2wwg?esE z7}QG`jmivsQ8L+38sXM4RWdT;wH@IN7~^v`Q?VUWb{@kyPXs!@XmYl>A7a(?SHSFr z&Fo~rKO%=3B4X&|w|Z%G|BjpjspNUJWO4m839Gb2L7A>`*%i0*mn({x|08lP#g}Ep zm&L{}#wJU}WxB;>SGE*+^paOw|Ihxf$k8Q8fDR%@hamBf$a$(DbQ(lcy^PVuo{s7x zz@Yv*{Qqg$#DSBeIN}MZ zT3+hs7!?*I=nK*N3ftjPWgJIY(fh^xQ#B>ko72#3tQ&J!DPC>E$Rwm{GGhPp()0T7 z)fhqAqk~K#C|sdip&d>b)bAogp6QNp7Rb z45a}jWm9upA2)8o95>Bpglj??aT^(ZHOF!5;COLknMSxq&mkq}7v{KEe7K1w=V`uB z%glh;qefQXkpidzlbIp*6i?f%zgQXZ?6Ok9O*Sc;`r}M-&Q|6)t3YR~O`Ihq!~jsu zj9&2Ka80-u-#l@dA#eO~wwBH>HgQ&bp*EAzZ@!1Tfm1n8P6PnzMT+yq`vJ33ZrN_` zMdg**Qk9hzl}(S0-72!jDxi|fE9+${D`l$ctAAI@tTvTb zw$}94*XEF`R-4vY+GJK+eJaW1s`@o5wY6ro(|5I1{#q%jue55VwW77vYqiz4zsXam zxN_yObl>yZ-um^`)*}r8qNccjS47*lmOu|r`^)(@o6+^2$Dh>v?gb1+f1roFg<8P2 zdtQQIt#jk>sf%jKB-d^8WShyPqRN$4 z>AuH6kitx-x6lK5A=_ycK*+1FVF_{U-eelWPj~wMKKAx|K=EBxrmhr#4Hy+0+`Srw zqK9e1)nL#*(M29J4(SerKL>UNQDUyJPt?yC(;-AWi*Xkvj!SnU%#-!IbJMvnmr%q} zEJGpbECydlq+s!NrQ;4C5G8yVH(BDSwR=V9xOF0@BZuN*iPNAy6RCkp0IU`ZO4zh8 zq!JzjjI2dJ0CMrDrd~w}mzjP=BVa`;;WNM}H1XJYDNuyKjuI{#eN-opnWt=LwsR*}gVeNKgg*UJ45bFm5$e;T0WxHpxJ^8D6=R z;!Y*lc&byf{V+9@acmMGIC?&s$PQlrTXP3@*-nZ>4OO^wT}B!ZzVOmI5ltL*0Ibrm ziC~9~M@wm@J0N6Z6B-v@`gtgSAGfZi$}j-G5Pm5w-z-4Z38#fL!kL^4s40XRwwkfn zL47w4LO)uXNvj_%SwS$=LZNW|nvBTCK{N{0K|wPjNqh#RUro9Z>}iK8D)r(tcU;>tqA~7Aa(RxyVo>K zX)xAAQxpx5017TO?OE1`A!rR8Xl*x-AfZ$SNCRFB!vb+Yr3wNouw91*l|Zy!LjY)aCZnMzDlR!sOpGrvF8W3Q%WpT7X~cPPicBUR8dE zYxmL79438iSTX0+gUo06rk1iPo6gepswDl9WkJ9ply*S-_hCNuprgfs|v6 zq=s>-s3}c76p3P;APYhc10Ed`m1D5rm5wQgYK4+fEQn9IV>JjMYdknQUKZ6zaY$?g z_6He);*g+Bv1WnhynLW=y;|RxjFNlLN9J~YEeeWltzBzVF9ulPivlWIM71#hRaA_K zMgceMtwp8&YmMXB;vTdBpcePuBc9dTg%)EWSVIW|kV);%kYL8NSHSIpF^UV6azwy+ zZ;i+WWbBrAL@X7Nz%*3CE>w4COrlhC?Y=C!x0hNA0*KO5v#|J*L~*Z}htiwpv}Z4M zW1w(W2=bkBUqw;65goZvnFdj*1P%u|PHa3{x6WBlpaVWZR3Pz~Vew134vD5Fd5#!` zwrZjUM(XDEQ9^ZNU_ZDeA{Zfg4$-3#8ZaJsHxLv>hk^=fpAIXWr=xqvrO3?6+!f_F z>P8E3M0K;?j6KK7#a$Uu%?Zy<0F05`mz!{RlC!Y;-bP3VSNYP0lo~bZ#PmF9glO!r7s43HsRB zSeNADwzsqr+D=g=?eFN2yJYIX$hdb^q1&DB-jeB{b9-d!Etx82B%MM&AbyuzFL$J8 zpocs&Jvr9VcYk1Vq@}I1rIzCzPtI4#&tKw6FQvpG3&(Y&_bc;(l?11GhTNZvG9NWl z9?`WrKeS&t&InyNmbvz46vF~8s*0Y?KWtQP7$`7EXl*BSuir!8MKhA!$^n4J`7b-UfF*RvJGdADgCB0-mLeKmz=R`FB5lAk`LVb}aEQY;yerP*=UpqWX`RhyRyNS+e%Jh)WedhbqTSgh1 zR~Cm}GEdLnalblaop?G=jBy=4v6y(|ZtJ}jHOvP#ex#&xD`Uj*$45+9%4rzPT_t&S zLS<=o+=A@}_kZr4{UB}6XbR^zBiA|3eg0{o+S5fBozJHmcwb+hdD0q^G84(se(dz! z*Jy*91atfVlG5N7D-wCv<7i+bzNRq7o)H~mbNt&-GV_4dvTvmq{WA@y$@!Y+m<oD6mPzlbBo}|)JaXH8>wer}KW~lD*|%md z+~VS_->O*p2z4|Shn_#?ee70q(HrJYgS9_6wo*o{4a?>OZkA0I6TTFh$}1Z??P6Dq z>#JuiN|hhFKg+Fn;7}6wM~`egqa0jVdC4mb#ZvepsYvu06K{4f>9hEm1$W zR<-6G4l7QEF+(R#w&gM|ev$7Bm0Fw-zl+?|QSB*LOqIL3;)tP?t@_DPrq8P+H>uS{ zAjf!KK?k~6<90aLpQ0O5o_v!1dgPGZjmi6;}ct-Bm=){WyJU*Rx@d1ft z%+$)e#rN-@?2K-=ZU+5IxGT$_PE$ZRi2tBpor6!ZY2o-_q4#p`^ETx)v(JO?LX_cr z7cm*BxG3RLH5QoXK#4i$`s(>v%$?y0i`EyKI*Be?$2U#A^jsr)^Jm%m{g288#x0%o z8Bj{cW&}DCwR&D0)EP^beg~^yMjpc-k$TndC7~Vt^)t<9v5Y+O_}iip`deK=43J%5 zfAUWJ`bCM7u(m^g!p?=cAK?F^;Q)X%d#9*eJ4) z*p{+mfA#ZAFZGtqr`76E2Uh-8ejV#k5hGQWm9IfxI!?2GY!;C%U(Sk+3>En$+_;=E z-L7$cQ)lK(v|YQJwf@Qv?+#YWI<+ahr%2-VgNbu;iz`NLZOp;`ZmaBGSBW|>{AIY_ zZ0l5NMq-~ptolj$htkIzPaNm0NK^Q6N+{2wJfm7|I_qR5tM!pt&eJwAf3$bFLL%s1 zJC9GUZdU5*Zn{29R!QNIUg+|Lxu0UKHHUpMOf+(YjW zq2O4VC2Q!5emmE4T1X!A43fd0I4^h0!0W|O{ss(cx^D!{hmx`K!H0xvavu*}I2W;O zXn$L+Fokn9$KU-#UDy1uv`SGG_pysfOS!`rE?t{A7`M6#yH09jn~=)q^%$#h4Jv)~ z>FXD_<%Z$sY0EdQr*$pN`s;LPE;5{{IKvl6alNr{M2Y;(DdvIb#scjXmO9vW0MGj3 zWx2awUPQ|o@g1dIGTAu@4WIgR7!@mt+YW^-@lM5T)|Jkl4!^WW`dX_-@ZQ6P^N zK^oQ~jiTOt@Hjy}XO=DA<@~2Ymu~xLKDCWCR=59YdOWqsQ#^wE*~f}pOxXErbK60y z<(v0Y$k9prpPTe`Kl0->h-j-$ zdtEhX^_QMfeBY{!=J0yn>qg7*33x>YoKM0y z{g@ccJR{Pi-ZqI%+{RgJXKU)z%dP6E3YT9jWhQ4>b(>>DG8?XlZ7xJCQ)JvV-!#}u z$fjr?-Qc|4+q*T++MPm zrkOlfaqYWjrp{SMB$B%^Fq`H9UuNI0<&`I0FBf;PAMCbP`kvT4e#}?ae)`-UZUdo~ zUe$gZciq~u*H(j=at^1~`;%RnS%*$fKA0tTwr?~fZxAjY@;&q_|7tK&VsQM3c+sJU zYd5;@U5}rwxmf22r%@HVdgAk(c4v~lXfk8_lhMORRlL{r-=*5U?@X(#6&Z;467w(B zd#Jlw)L*$+|2&irqX`p!#9yMLaNPC$nB$_aog(8__w!qgxC4)S{wwNPHZ1FY+rt`{L}8$ z(!|3QFN+!{(-Go#mbTUM!yfba zRr>qE_xz!)rb-7*L%9+!~3J|p~;aHHSQ`PT{QFVAkP4D8P|?Gn(m@jkhMj^{bo=5a|c zW~1ln39F)<>-mSP!ZWqQxyzn6yeN6Z=9KB#CCD=NE1>b#+-VE#%uizb6B6Nc(8)ld z=09c+cTBFl-ElS43ZcWFYMxV(dE>-op_TbQP2CgaqYOJa7#bQVFw3;{^3@fz#I``Iiw-y8PfWi8Wa_w=MhlAw+}jpaUN;IK+?6Z(#S`6hz>q+m{Zz@d*PjXrQyPST%vs#@DTAb0-PsO;iLCid-U|GQPQ ziF5W%f=|j*4y%BkOz>^jxUS01XVJm{;*lbE5_E6|q z8OHLm$(Bqj4$D@YHKPwR=-c<{0S^$req@-+S9^W>$NCr8@oQ`_Xj-zH<}6f8$Z)N; zP^7On;$9qMf3&mcxe-|v2L5AJZ~E$c_|bV3~cpvKv&@;XHBnZe`gWB}z7t zO~qdof^+WF{y~IKt$vt#){dK-a&QazV~J$ula}pUsH8to>e66ry)F4pa6Jh5+4lI) z{VjJ_%LXj$I|y4XK0`gvblKDGRfF#Ss`&Vt%)M`;sg)}xj;F(fuwKk^gsJ@CO-=kF z`TN?V^$xTJQtti3LeVpC2NYqUfgXQU+L*NVoeCYk(3N)bNetsty6ysIoPI|9b4TVA zpS~u1$ho@{`fZCp=+5KN54{;J34N4&oK9@@DLG|jgH3j^S%;iD!bzO9__nI-eQiEUg$RlYy9S#0TML$3Nfy z!T!bUSh=BW;QpGbZ_i_&pL%?a&Oev8goVlIkv)UCQ&Zr{tb?rVu2)Z3qi5(l-ri`6 zYy5qKE{^rmj!zy>wwNs5>Bh&vG5&k4AK)g|w+}$CofF3fPR%j((%18=9eR>94G;Ih z;oLr0=|p76q_^1%pK|~5u8QH{wNI-%M+AI-eR|Wzelj=M7s-M|4%xxnTbg}1=YFcK z^t~r6v%z`huf`8mi*E7j>|Yk>;X*IIF5s^N9trvrrgTK zQ0bQEr2=)EpYl1k&U&*+nAjSt_pim&E>^rET+56%T1a3=jM-XU#oGj5F>Cm`*?z^i zMqu=C1h1FOo#d~1tCKx)Vdn?e7>t@rQ#IKiBvpFiJ2Ys)HOGFMUkFlz!sF}-9a4A~ z=7OF$_ESPvp6cChm1<9fjjXwU`FbtGJ;cJrKK@TVUf01@60-g*x@xtZ`YydUSN_^^ zTlfH5>WB72ThlU3~ zu4qb<1M24T<+Zoc+KERf(V0lDOwY5{m1~M-0*-P{++4p;8p?-G1Xq8NY!rMZ^V)i& z+QS6xmsJ!RcQ0S0R`>Fw&vy*7?u+5bX*rRtGQzjw)n`vsKZi^$KE|LjHp0DfV9-gI zYu;n@Fb-+8OJ%SIoIV$~q>|awha2m!7aon(FudOR`RLuwJpMs(MuW{zS#PJ0uqvzU zY!HVBv-Oh|9xapcXA^`QfdPtdj~v=Gr=8Dun5Z`wWWbZq8?fD;)^}lN_<#j^+Dp2R z$AA8Zjp{k0XA+e7S&T78t${$Y>wOi4vx|7(@gXF;Efv^5@^ z2T!b%`736=%StEtRd^Qj+$c1^_SPiB`@?1c7hCwL<6+&(idU20UB^QC&JXQ=3DZDX zFyfKl8O_}#q^DQE|8!3v)qgK~nb~{2zYXmWdvKlQQ~cJBSKZH_!yZzGt!JxqEw}yZ z(fE}^YjHMXw;4(uzrC2pB;Kmr8hch9_3~%4a(Wl}^YlYR=4)-dun@EM8M>_o48OnG z^s{p~R0}M6@WPcUUPJHx%AmC3JmJPnx%CmZl(qvE;d6avQud3Bll>vA$97z|+O{`t zhK3(rc0wB2=9kPXPml{zoi7i``-6jD%l}|Es!7LqpsG*qNz^DS^m0o#-?d~-CuQ;7 z7yb+w|HO6#&Ci#8V&jZrKO<>niaZ;7Ei)o~ysghFBl}a&X7@gdBE*fSHDqRG{NSRs z;-TCip~nc~a%m&=1BX%>9K$u0(E&}z?X??4tVZn>JPi!qgcO>EF*q4v+sjFP!RML} z^n~mTjgnW$b(78-IR#VEH?ywzTMb>K>ey)dL{@&!&uhHsl40 HZ)*F0#y{q* literal 0 HcmV?d00001 diff --git a/public/sound/Call_Dialing.ogg b/public/sound/Call_Dialing.ogg new file mode 100644 index 0000000000000000000000000000000000000000..895fa3d589290dce72f56f8a08de0ad3764e9b51 GIT binary patch literal 39068 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 literal 0 HcmV?d00001 diff --git a/public/sound/Mic_Mute.ogg b/public/sound/Mic_Mute.ogg new file mode 100644 index 0000000000000000000000000000000000000000..99d012e153d5f0a1b79b991afea5006310ce4f08 GIT binary patch literal 8073 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 literal 0 HcmV?d00001 diff --git a/public/sound/Mic_Unmuted.ogg b/public/sound/Mic_Unmuted.ogg new file mode 100644 index 0000000000000000000000000000000000000000..58489af62c10adf1b44ec5a63980fba4b8f33b8c GIT binary patch literal 7416 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 TuD2afEy=D}DUsfSJNN$pymky3 literal 0 HcmV?d00001 diff --git a/public/sound/Speakers_Undeafen.ogg b/public/sound/Speakers_Undeafen.ogg new file mode 100644 index 0000000000000000000000000000000000000000..57d725bfc23f11042c25f6842b25950dd6b5c637 GIT binary patch literal 8528 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 TG4153$vKfk4T0u^%WVGzxaITV literal 32667 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 diff --git a/src/app/features/call/CallOverlay.css.ts b/src/app/features/call/CallOverlay.css.ts index f2d712b..559a2c5 100644 --- a/src/app/features/call/CallOverlay.css.ts +++ b/src/app/features/call/CallOverlay.css.ts @@ -5,7 +5,7 @@ export const CallOverlayContainer = style([ DefaultReset, { position: 'fixed', - bottom: config.space.S400, + top: '60px', right: config.space.S400, zIndex: 9999, backgroundColor: color.Surface.Container, @@ -16,6 +16,7 @@ export const CallOverlayContainer = style([ maxWidth: '400px', transition: 'box-shadow 0.2s ease', userSelect: 'none', + color: color.Surface.OnContainer, }, ]); @@ -44,6 +45,7 @@ export const CallOverlayHeader = style({ alignItems: 'center', justifyContent: 'space-between', marginBottom: config.space.S200, + color: color.Surface.OnContainer, }); export const CallOverlayControls = style({ @@ -55,6 +57,7 @@ export const CallOverlayControls = style({ export const CallDuration = style({ fontVariantNumeric: 'tabular-nums', + color: color.Surface.OnContainer, }); export const VideoContainer = style({ @@ -111,21 +114,6 @@ export const ScreenShareContainer = style({ }, }); -export const ScreenShareLabel = style({ - position: 'absolute', - top: config.space.S100, - left: config.space.S100, - backgroundColor: 'rgba(0, 0, 0, 0.7)', - color: 'white', - padding: `${config.space.S100} ${config.space.S200}`, - borderRadius: config.radii.R300, - fontSize: '0.75rem', - display: 'flex', - alignItems: 'center', - gap: config.space.S100, - zIndex: 1, -}); - export const ScreenShareVideoContainer = style({ width: '100%', height: '100%', @@ -140,3 +128,89 @@ export const ScreenShareVideo = style({ height: '100%', objectFit: 'contain', }); + +export const ParticipantsSection = style({ + marginBottom: config.space.S200, + borderRadius: config.radii.R300, + backgroundColor: color.Surface.ContainerActive, + overflow: 'hidden', +}); + +export const ParticipantsHeader = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + width: '100%', + padding: `${config.space.S200} ${config.space.S300}`, + backgroundColor: 'transparent', + border: 'none', + cursor: 'pointer', + color: color.Surface.OnContainer, + transition: 'background-color 0.15s ease', + ':hover': { + backgroundColor: color.Surface.ContainerHover, + }, +}); + +export const ParticipantsHeaderText = style({ + color: color.Surface.OnContainer, +}); + +export const ParticipantsList = style({ + padding: `0 ${config.space.S200} ${config.space.S200}`, + display: 'flex', + flexDirection: 'column', + gap: config.space.S100, +}); + +export const ParticipantItem = style({ + display: 'flex', + alignItems: 'center', + gap: config.space.S200, + padding: config.space.S200, + borderRadius: config.radii.R300, + backgroundColor: color.Surface.Container, + transition: 'background-color 0.15s ease', + ':hover': { + backgroundColor: color.Surface.ContainerHover, + }, +}); + +export const ParticipantName = style({ + color: color.Surface.OnContainer, + fontWeight: 500, +}); + +export const ParticipantId = style({ + color: color.Secondary.Main, + opacity: 0.7, +}); + +export const YouBadge = style({ + color: color.Primary.Main, + fontWeight: 600, +}); + +export const ParticipantMutedIcon = style({ + color: color.Critical.Main, + flexShrink: 0, +}); + +export const ParticipantAvatarWrapper = style({ + position: 'relative', + width: '32px', + height: '32px', + flexShrink: 0, + borderRadius: '50%', + transition: 'box-shadow 0.15s ease', +}); + +export const ParticipantAvatar = style({ + width: '32px', + height: '32px', + borderRadius: '50%', +}); + +export const ParticipantSpeaking = style({ + boxShadow: `0 0 0 2px ${color.Success.Main}`, +}); diff --git a/src/app/features/call/CallOverlay.tsx b/src/app/features/call/CallOverlay.tsx index 70b68c4..e05b23a 100644 --- a/src/app/features/call/CallOverlay.tsx +++ b/src/app/features/call/CallOverlay.tsx @@ -1,7 +1,15 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { Box, Icon, IconButton, Icons, Text, Tooltip, TooltipProvider } from 'folds'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Box, Icon, IconButton, Icons, Text, Tooltip, TooltipProvider, color } from 'folds'; import { useCall } from './useCall'; -import { CallState, CallType, ScreenShareTrackInfo } from './types'; +import { useRoomCallMembers } from './useRoomCallMembers'; +import { CallState, CallType } from './types'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { getMemberDisplayName, getMemberAvatarMxc } from '../../utils/room'; +import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix'; +import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; +import { UserAvatar } from '../../components/user-avatar'; +import { useSelectedRoom } from '../../hooks/router/useSelectedRoom'; +import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import * as css from './CallOverlay.css'; /** Drag position state */ @@ -10,9 +18,9 @@ interface DragPosition { y: number; } -/** Custom hook for draggable functionality */ -function useDraggable(initialPosition: DragPosition = { x: 0, y: 0 }) { - const [position, setPosition] = useState(initialPosition); +/** Custom hook for draggable functionality with viewport constraints */ +function useDraggable() { + const [position, setPosition] = useState({ x: 0, y: 0 }); const [isDragging, setIsDragging] = useState(false); const dragRef = useRef(null); const dragStartRef = useRef<{ x: number; y: number; startX: number; startY: number } | null>(null); @@ -29,17 +37,92 @@ function useDraggable(initialPosition: DragPosition = { x: 0, y: 0 }) { }; }, [position]); + // Clamp position to viewport bounds + const clampToViewport = useCallback(() => { + if (!dragRef.current) return; + const rect = dragRef.current.getBoundingClientRect(); + const padding = 16; + + // The element is positioned with right: S400 (16px) and top: 60px in CSS + // Transform moves it from that base position + // We need to ensure the element stays fully visible + + let newX = position.x; + let newY = position.y; + let needsUpdate = false; + + // Check if left edge is off screen (rect.left < padding) + if (rect.left < padding) { + newX = position.x + (padding - rect.left); + needsUpdate = true; + } + // Check if right edge is off screen + if (rect.right > window.innerWidth - padding) { + newX = position.x - (rect.right - window.innerWidth + padding); + needsUpdate = true; + } + // Check if top edge is off screen + if (rect.top < padding) { + newY = position.y + (padding - rect.top); + needsUpdate = true; + } + // Check if bottom edge is off screen + if (rect.bottom > window.innerHeight - padding) { + newY = position.y - (rect.bottom - window.innerHeight + padding); + needsUpdate = true; + } + + if (needsUpdate) { + setPosition({ x: newX, y: newY }); + } + }, [position]); + + // Listen for window resize to keep element in bounds + useEffect(() => { + const handleResize = () => { + clampToViewport(); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [clampToViewport]); + useEffect(() => { if (!isDragging) return undefined; const handleMouseMove = (e: MouseEvent) => { - if (!dragStartRef.current) return; + if (!dragStartRef.current || !dragRef.current) return; const deltaX = e.clientX - dragStartRef.current.startX; const deltaY = e.clientY - dragStartRef.current.startY; - setPosition({ - x: dragStartRef.current.x + deltaX, - y: dragStartRef.current.y + deltaY, - }); + + // Get element dimensions for bounds checking + const rect = dragRef.current.getBoundingClientRect(); + const padding = 16; + + // Calculate new position + let newX = dragStartRef.current.x + deltaX; + let newY = dragStartRef.current.y + deltaY; + + // Calculate where the element would be with the new transform + const newLeft = rect.left - position.x + newX; + const newRight = rect.right - position.x + newX; + const newTop = rect.top - position.y + newY; + const newBottom = rect.bottom - position.y + newY; + + // Clamp to keep fully on screen + if (newLeft < padding) { + newX += padding - newLeft; + } + if (newRight > window.innerWidth - padding) { + newX -= newRight - window.innerWidth + padding; + } + if (newTop < padding) { + newY += padding - newTop; + } + if (newBottom > window.innerHeight - padding) { + newY -= newBottom - window.innerHeight + padding; + } + + setPosition({ x: newX, y: newY }); }; const handleMouseUp = () => { @@ -54,7 +137,7 @@ function useDraggable(initialPosition: DragPosition = { x: 0, y: 0 }) { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; - }, [isDragging]); + }, [isDragging, position]); return { position, isDragging, handleMouseDown, dragRef }; } @@ -119,30 +202,178 @@ function formatDuration(seconds: number): string { return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } +/** Screen share entry type */ +interface ScreenShareEntry { + id: string; + isLocal: boolean; + participantId: string; + livekitId?: string; + track?: unknown; + stream?: MediaStream; +} + +/** Props for ScreenShareItem component */ +interface ScreenShareItemProps { + share: ScreenShareEntry; +} + +/** + * Individual screen share display component + * Handles attaching the video stream/track properly + */ +function ScreenShareItem({ share }: ScreenShareItemProps) { + const containerRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + if (!container) return undefined; + + // Handle local screenshare (MediaStream) + if (share.isLocal && share.stream) { + const videoEl = document.createElement('video'); + videoEl.className = css.ScreenShareVideo; + videoEl.autoplay = true; + videoEl.playsInline = true; + videoEl.muted = true; + videoEl.srcObject = share.stream; + + container.innerHTML = ''; + container.appendChild(videoEl); + + return () => { + videoEl.srcObject = null; + if (videoEl.parentNode) { + videoEl.parentNode.removeChild(videoEl); + } + }; + } + + // Handle remote screenshare (LiveKit track) + if (!share.isLocal && share.track) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const track = share.track as any; + + if (typeof track.attach === 'function') { + const videoEl = track.attach() as HTMLVideoElement; + videoEl.className = css.ScreenShareVideo; + + container.innerHTML = ''; + container.appendChild(videoEl); + + return () => { + if (typeof track.detach === 'function') { + track.detach(videoEl); + } + if (videoEl.parentNode) { + videoEl.parentNode.removeChild(videoEl); + } + }; + } + } + + return undefined; + }, [share.isLocal, share.stream, share.track]); + + /** + * Handles double-click to toggle fullscreen on the screenshare video element + */ + const handleDoubleClick = useCallback(async () => { + const container = containerRef.current; + if (!container) return; + + // Find the video element inside the container + const videoEl = container.querySelector('video'); + if (!videoEl) return; + + try { + if (document.fullscreenElement) { + await document.exitFullscreen(); + } else { + await videoEl.requestFullscreen(); + } + } catch (err) { + // Fullscreen may not be available + } + }, []); + + return ( +

+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} +
+
+ ); +} + /** * Component that displays active call controls and video streams */ export function CallOverlay() { - const { activeCall, endCall, toggleMute, toggleVideo, toggleScreenShare } = useCall(); + const { activeCall, endCall, toggleMute, toggleDeafen, toggleVideo, toggleScreenShare } = useCall(); + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const selectedRoomId = useSelectedRoom(); const [duration, setDuration] = useState(0); const [isTogglingScreenShare, setIsTogglingScreenShare] = useState(false); - const [screenShareTrack, setScreenShareTrack] = useState(null); const [isFullscreen, setIsFullscreen] = useState(false); const [isPip, setIsPip] = useState(false); + const [showParticipants, setShowParticipants] = useState(true); + const [collapsedScreenShares, setCollapsedScreenShares] = useState>(new Set()); const localVideoRef = useRef(null); const remoteVideoRef = useRef(null); const containerRef = useRef(null); - const { position, isDragging, handleMouseDown } = useDraggable(); + const { position, isDragging, handleMouseDown, dragRef } = useDraggable(); + const { navigateRoom } = useRoomNavigate(); + + // Get call members from Matrix state events (provides real Matrix user IDs) + const { callMembers } = useRoomCallMembers(activeCall?.roomId ?? ''); + const myUserId = mx.getUserId() ?? ''; - // Update screen share track state when activeCall changes - useEffect(() => { - if (activeCall?.remoteScreenTracks && activeCall.remoteScreenTracks.size > 0) { - const [firstTrackInfo] = activeCall.remoteScreenTracks.values(); - setScreenShareTrack(firstTrackInfo || null); - } else { - setScreenShareTrack(null); + // Memoize the screenshare list to prevent re-renders causing flickering + const allScreenShares = useMemo((): ScreenShareEntry[] => { + const shares: ScreenShareEntry[] = []; + + // Add local screenshare first if active + if (activeCall?.isScreenSharing && activeCall?.localScreenStream) { + shares.push({ + id: 'local', + isLocal: true, + participantId: myUserId, + stream: activeCall.localScreenStream, + }); } - }, [activeCall]); + + // Add remote screenshares + const remoteScreenTracks = activeCall?.remoteScreenTracks + ? Array.from(activeCall.remoteScreenTracks.values()) + : []; + + remoteScreenTracks.forEach((trackInfo) => { + // Try to extract deviceId from LiveKit identity (format: "deviceId_timestamp") + const livekitId = trackInfo.participantId; + const underscoreIndex = livekitId.lastIndexOf('_'); + const possibleDeviceId = underscoreIndex > 0 ? livekitId.substring(0, underscoreIndex) : livekitId; + + // Look up Matrix user ID by matching device ID from call members + const matchingMember = callMembers.find((m) => m.deviceId === possibleDeviceId); + const matrixUserId = matchingMember?.userId ?? livekitId; + + shares.push({ + id: livekitId, + isLocal: false, + participantId: matrixUserId, + livekitId, + track: trackInfo.track, + stream: trackInfo.stream, + }); + }); + + return shares; + }, [activeCall?.isScreenSharing, activeCall?.localScreenStream, activeCall?.remoteScreenTracks, callMembers, myUserId]); useEffect(() => { if (!activeCall || activeCall.state !== CallState.Connected) { @@ -177,46 +408,6 @@ export function CallOverlay() { } }, [activeCall?.remoteStreams]); - // Attach screen share track to container using LiveKit's recommended approach - useEffect(() => { - const containerEl = document.getElementById('screen-share-container'); - - if (screenShareTrack?.track && containerEl) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const track = screenShareTrack.track as any; - - if (typeof track.attach === 'function') { - // Let LiveKit create and manage the video element - const videoEl = track.attach() as HTMLVideoElement; - videoEl.className = css.ScreenShareVideo; - - // Clear any existing content - containerEl.innerHTML = ''; - containerEl.appendChild(videoEl); - - // eslint-disable-next-line no-console - console.log('📺 Attached screen share - LiveKit created video element'); - - // Cleanup: detach when effect re-runs or unmounts - return () => { - if (typeof track.detach === 'function') { - track.detach(videoEl); - } - if (videoEl.parentNode) { - videoEl.parentNode.removeChild(videoEl); - } - }; - } - } - - // No track, clear container - if (containerEl && !screenShareTrack?.track) { - containerEl.innerHTML = ''; - } - - return undefined; - }, [screenShareTrack]); - // Fullscreen toggle handler const handleToggleFullscreen = useCallback(async () => { if (!containerRef.current) return; @@ -254,10 +445,11 @@ export function CallOverlay() { } }, [isPip]); - // Listen for fullscreen change events + // Listen for fullscreen change events (only for the call overlay itself, not screenshares) useEffect(() => { const handleFullscreenChange = () => { - setIsFullscreen(!!document.fullscreenElement); + // Only set fullscreen state if the call overlay container is fullscreened + setIsFullscreen(document.fullscreenElement === containerRef.current); }; document.addEventListener('fullscreenchange', handleFullscreenChange); return () => document.removeEventListener('fullscreenchange', handleFullscreenChange); @@ -280,15 +472,90 @@ export function CallOverlay() { }; }, [activeCall]); + // Assign both refs to the container - must be before early return + const setContainerRef = useCallback((el: HTMLDivElement | null) => { + (containerRef as React.MutableRefObject).current = el; + (dragRef as React.MutableRefObject).current = el; + }, [dragRef]); + if (!activeCall || activeCall.state === CallState.Idle || activeCall.state === CallState.Ended) { return null; } const isConnecting = activeCall.state === CallState.Connecting; const isVideoCall = activeCall.callType === CallType.Video; - - // Check if anyone is sharing their screen - const hasRemoteScreenShare = screenShareTrack !== null; + + // Get room for participant info + const room = mx.getRoom(activeCall.roomId); + + // Build participant list from Matrix state events (provides real user IDs) + // callMembers has the actual Matrix user IDs from org.matrix.msc3401.call.member events + const allParticipants = callMembers.map((m) => m.userId); + const participantCount = allParticipants.length; + + /** + * Gets avatar URL for a participant + */ + const getParticipantAvatar = (participantId: string): string | undefined => { + if (!room) return undefined; + const mxcUrl = getMemberAvatarMxc(room, participantId); + if (!mxcUrl) return undefined; + return mxcUrlToHttp(mx, mxcUrl, useAuthentication, 48, 48, 'crop') ?? undefined; + }; + + /** + * Gets display name for a participant + */ + const getParticipantName = (participantId: string): string => { + if (!room) return getMxIdLocalPart(participantId) ?? participantId; + return getMemberDisplayName(room, participantId) ?? getMxIdLocalPart(participantId) ?? participantId; + }; + + /** + * Checks if a participant is currently speaking + * Handles mapping between Matrix user IDs and LiveKit identities + */ + const isParticipantSpeaking = (userId: string): boolean => { + if (!activeCall) return false; + + // Direct check for Matrix user ID (works for local user) + if (activeCall.activeSpeakers.has(userId)) return true; + + // For remote users, activeSpeakers contains LiveKit identities (deviceId_timestamp) + // We need to find the deviceId for this user and check if any matching identity is speaking + const member = callMembers.find((m) => m.userId === userId); + if (!member) return false; + + // Check if any active speaker has this user's deviceId + return Array.from(activeCall.activeSpeakers).some((speakerId) => { + const underscoreIndex = speakerId.lastIndexOf('_'); + const deviceId = underscoreIndex > 0 ? speakerId.substring(0, underscoreIndex) : speakerId; + return deviceId === member.deviceId; + }); + }; + + /** + * Checks if a participant is currently muted + * Handles mapping between Matrix user IDs and LiveKit identities + */ + const isParticipantMuted = (userId: string): boolean => { + if (!activeCall) return false; + + // For local user, check activeCall.isMuted + if (userId === myUserId) return activeCall.isMuted; + + // For remote users, mutedParticipants contains LiveKit identities (deviceId_timestamp) + // We need to find the deviceId for this user and check if any matching identity is muted + const member = callMembers.find((m) => m.userId === userId); + if (!member) return false; + + // Check if any muted participant has this user's deviceId + return Array.from(activeCall.mutedParticipants).some((mutedId) => { + const underscoreIndex = mutedId.lastIndexOf('_'); + const deviceId = underscoreIndex > 0 ? mutedId.substring(0, underscoreIndex) : mutedId; + return deviceId === member.deviceId; + }); + }; const handleEndCall = () => { endCall(); @@ -298,6 +565,10 @@ export function CallOverlay() { toggleMute(); }; + const handleToggleDeafen = () => { + toggleDeafen(); + }; + const handleToggleVideo = () => { toggleVideo(); }; @@ -314,7 +585,7 @@ export function CallOverlay() { return (
- + - - {isConnecting ? 'Connecting...' : 'In Call'} + + {isConnecting ? 'Connecting...' : `In Call - ${room?.name ?? 'Unknown Room'}`} + {selectedRoomId === activeCall.roomId && ( + (Current Room) + )} + {selectedRoomId !== activeCall.roomId && ( + Go to Room} + > + {(triggerRef) => ( + navigateRoom(activeCall.roomId)} + variant="Secondary" + size="300" + style={{ padding: '2px', marginLeft: '4px' }} + > + + + )} + + )} @@ -372,17 +665,109 @@ export function CallOverlay() {
- {/* Remote Screen Share Display */} - {hasRemoteScreenShare && screenShareTrack && ( -
-
- - {screenShareTrack.participantId} + {/* Participants Section */} +
+ + {showParticipants && ( +
+ {allParticipants.map((participantId) => { + const isMe = participantId === myUserId; + const displayName = getParticipantName(participantId); + const avatarUrl = getParticipantAvatar(participantId); + const isSpeaking = isParticipantSpeaking(participantId); + const isMuted = isParticipantMuted(participantId); + // Get screenshares for this participant + const participantScreenShares = allScreenShares.filter( + (share) => share.participantId === participantId + ); + + return ( +
+
+ ( + + {displayName.charAt(0).toUpperCase()} + + )} + /> +
+ + + {displayName} + {isMe && (You)} + + + {participantId} + + + {isMuted && ( + + )} + {participantScreenShares.length > 0 && ( + {collapsedScreenShares.has(participantScreenShares[0].id) ? 'Show Screen' : 'Hide Screen'}} + > + {(triggerRef) => ( + { + setCollapsedScreenShares((prev) => { + const next = new Set(prev); + // Toggle all screenshares for this participant + participantScreenShares.forEach((share) => { + if (next.has(share.id)) { + next.delete(share.id); + } else { + next.add(share.id); + } + }); + return next; + }); + }} + variant="Secondary" + size="300" + style={{ padding: '2px' }} + > + + + )} + + )} +
+ ); + })}
- {/* LiveKit will create and manage the video element inside this container */} -
-
- )} + )} +
+ + {/* Screen shares rendered outside participant list */} + {allScreenShares + .filter((share) => !collapsedScreenShares.has(share.id)) + .map((share) => ( + + ))} {isVideoCall && (
@@ -432,6 +817,29 @@ export function CallOverlay() { )} + + {activeCall.isDeafened ? 'Undeafen' : 'Deafen'} + + } + > + {(triggerRef) => ( + + + + )} + + {isVideoCall && ( {children} + ); } diff --git a/src/app/features/call/CallService.ts b/src/app/features/call/CallService.ts index bbd5320..752e528 100644 --- a/src/app/features/call/CallService.ts +++ b/src/app/features/call/CallService.ts @@ -24,6 +24,7 @@ import { } from './types'; import { fetchLiveKitJWT, fetchWellKnownWithRTC, getLiveKitFocus, requestOpenIdToken } from './useRtcConfig'; import { getAudioSettings, SCREEN_SHARE_RESOLUTIONS, SCREEN_SHARE_BITRATES } from '../settings/audio/Audio'; +import { getCallSounds, CallSoundType } from './CallSounds'; type CallEventListener = (event: CallEvent) => void; @@ -190,11 +191,17 @@ export class CallService { remoteScreenStreams: new Map(), remoteScreenTracks: new Map(), participants: [], + activeSpeakers: new Set(), + mutedParticipants: new Set(), isMuted: false, + isDeafened: false, isVideoEnabled: callType === CallType.Video, isScreenSharing: false, }; + // Play connecting/dialing sound + getCallSounds().playSound(CallSoundType.CallDialing, true); + this.emit({ type: CallEventType.StateChanged, roomId, @@ -262,10 +269,15 @@ export class CallService { } // Step 5: Send Matrix state event to announce participation + // This state event will be rendered in the timeline as a "joined the call" notification await this.sendCallMemberEvent(roomId, callId, true); console.log('Announced call participation to Matrix room'); + // Stop dialing sound and play join sound + getCallSounds().stopDialingLoop(); + getCallSounds().playSound(CallSoundType.CallJoined); + this.setState(CallState.Connected); } catch (error) { console.error('Failed to start call:', error); @@ -339,6 +351,12 @@ export class CallService { document.body.appendChild(audioElement); console.log('Audio element attached for', participant.identity); + + // If we're currently deafened, mute this new track so we don't hear them + if (this.activeCall?.isDeafened) { + track.setMuted(true); + console.log('Auto-muted new audio track from', participant.identity, 'due to deafen state'); + } } // Handle screen share video tracks separately @@ -454,6 +472,10 @@ export class CallService { if (this.activeCall && !this.activeCall.participants.includes(participant.identity)) { this.activeCall.participants.push(participant.identity); + + // Play join sound for remote participant + getCallSounds().playSound(CallSoundType.CallJoined); + this.emit({ type: CallEventType.ParticipantJoined, roomId: this.activeCall.roomId, @@ -471,6 +493,9 @@ export class CallService { ); this.activeCall.remoteStreams.delete(participant.identity); + // Play leave sound for remote participant + getCallSounds().playSound(CallSoundType.CallDepart); + this.emit({ type: CallEventType.ParticipantLeft, roomId: this.activeCall.roomId, @@ -488,21 +513,125 @@ export class CallService { } }); + // Track mute/unmute events for remote participants + this.livekitRoom.on(RoomEvent.TrackMuted, (publication, participant) => { + // Only emit for remote participants and audio tracks + if (!this.activeCall) return; + if (participant.identity === this.livekitRoom?.localParticipant.identity) return; + if (publication.kind !== 'audio') return; + + console.log(`🔇 ${participant.identity} muted their microphone`); + + // Track muted state + this.activeCall.mutedParticipants.add(participant.identity); + + // Play mute sound for remote participant + getCallSounds().playSound(CallSoundType.MicMute); + + this.emit({ + type: CallEventType.RemoteMuteChanged, + roomId: this.activeCall.roomId, + data: { participantId: participant.identity, isMuted: true }, + }); + }); + + this.livekitRoom.on(RoomEvent.TrackUnmuted, (publication, participant) => { + // Only emit for remote participants and audio tracks + if (!this.activeCall) return; + if (participant.identity === this.livekitRoom?.localParticipant.identity) return; + if (publication.kind !== 'audio') return; + + console.log(`🔊 ${participant.identity} unmuted their microphone`); + + // Track unmuted state + this.activeCall.mutedParticipants.delete(participant.identity); + + // Play unmute sound for remote participant + getCallSounds().playSound(CallSoundType.MicUnmute); + + this.emit({ + type: CallEventType.RemoteMuteChanged, + roomId: this.activeCall.roomId, + data: { participantId: participant.identity, isMuted: false }, + }); + }); + // Voice activity detection - logs when speakers change + // Note: This event tracks remote speakers. Local speaker is tracked separately. + // We preserve the existing activeSpeakers that may include local user this.livekitRoom.on(RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => { + if (!this.activeCall) return; + + // Get remote speaker identities + const remoteSpeakerIds = speakers + .filter((s) => s.identity !== this.livekitRoom?.localParticipant.identity) + .map((s) => s.identity); + + // Check if local participant is speaking (from LiveKit's list) + const localSpeaking = speakers.some( + (s) => s.identity === this.livekitRoom?.localParticipant.identity + ); + + // Build new activeSpeakers set - use Matrix user ID for local + const newSpeakers = new Set(remoteSpeakerIds); + if (localSpeaking) { + newSpeakers.add(this.config.userId); + } + + this.activeCall.activeSpeakers = newSpeakers; + if (speakers.length > 0) { const speakerNames = speakers.map((s) => s.identity).join(', '); console.log('🎤 Active speakers:', speakerNames); } + + this.emit({ + type: CallEventType.SpeakersChanged, + roomId: this.activeCall.roomId, + data: { speakers: newSpeakers }, + }); }); - // Track local participant speaking state + // Listen for participant metadata changes (for deafen state) + this.livekitRoom.on(RoomEvent.ParticipantMetadataChanged, (prevMetadata: string | undefined, participant: Participant) => { + if (!this.activeCall) return; + if (participant.identity === this.livekitRoom?.localParticipant.identity) return; + + try { + const prevData = prevMetadata ? JSON.parse(prevMetadata) : {}; + const newData = participant.metadata ? JSON.parse(participant.metadata) : {}; + + const wasDeafened = prevData.isDeafened === true; + const isDeafened = newData.isDeafened === true; + + if (wasDeafened !== isDeafened) { + console.log(`🎧 ${participant.identity} ${isDeafened ? 'deafened' : 'undeafened'}`); + getCallSounds().playSound(isDeafened ? CallSoundType.Deafen : CallSoundType.Undeafen); + } + } catch { + // Ignore metadata parse errors + } + }); + + // Track local participant speaking state - use Matrix user ID for consistency this.livekitRoom.localParticipant.on(ParticipantEvent.IsSpeakingChanged, (speaking: boolean) => { + if (!this.activeCall) return; + + const myUserId = this.config.userId; + if (speaking) { console.log('🎙️ You are speaking'); + this.activeCall.activeSpeakers.add(myUserId); } else { console.log('🔇 You stopped speaking'); + this.activeCall.activeSpeakers.delete(myUserId); } + + this.emit({ + type: CallEventType.SpeakersChanged, + roomId: this.activeCall.roomId, + data: { speakers: this.activeCall.activeSpeakers }, + }); }); // Track audio level changes for local participant (more detailed) @@ -522,8 +651,13 @@ export class CallService { const { roomId } = this.activeCall; this.setState(CallState.Disconnecting); + // Stop any dialing loop and play departure sound + getCallSounds().stopDialingLoop(); + getCallSounds().playSound(CallSoundType.CallDepart); + try { - // Clear Matrix call membership + // Clear Matrix call membership - this state event will be rendered + // in the timeline as a "left the call" notification await this.sendCallMemberEvent(roomId, '', false); } catch (error) { console.error('Failed to clear call membership:', error); @@ -550,22 +684,97 @@ export class CallService { /** * Toggles audio mute state + * Unmuting will also undeafen if currently deafened */ toggleMute(): boolean { - if (!this.livekitRoom?.localParticipant) { + if (!this.livekitRoom?.localParticipant || !this.activeCall) { return false; } - const newMuteState = !this.activeCall?.isMuted; - this.livekitRoom.localParticipant.setMicrophoneEnabled(!newMuteState); - - if (this.activeCall) { - this.activeCall.isMuted = newMuteState; + const newMuteState = !this.activeCall.isMuted; + + // If unmuting while deafened, undeafen first + if (!newMuteState && this.activeCall.isDeafened) { + this.activeCall.isDeafened = false; + getCallSounds().setDeafened(false); + + // Unmute all remote audio tracks + this.livekitRoom.remoteParticipants.forEach((participant) => { + participant.audioTrackPublications.forEach((publication) => { + if (publication.track) { + publication.track.setMuted(false); + } + }); + }); + + // Broadcast undeafen state + const metadata = JSON.stringify({ isDeafened: false }); + this.livekitRoom.localParticipant.setMetadata(metadata).catch((err) => { + console.warn('Failed to set participant metadata:', err); + }); } + + this.livekitRoom.localParticipant.setMicrophoneEnabled(!newMuteState); + this.activeCall.isMuted = newMuteState; + + // Play mute/unmute sound + getCallSounds().playSound( + newMuteState ? CallSoundType.MicMute : CallSoundType.MicUnmute + ); return newMuteState; } + /** + * Toggles deafen state (mutes all incoming audio and your mic) + * Also broadcasts deafen state to other participants via metadata + */ + toggleDeafen(): boolean { + if (!this.livekitRoom || !this.activeCall) { + return false; + } + + const newDeafenState = !this.activeCall.isDeafened; + this.activeCall.isDeafened = newDeafenState; + + // Update CallSounds deafen state + getCallSounds().setDeafened(newDeafenState); + + // Mute/unmute all remote audio tracks (so we don't hear them) + this.livekitRoom.remoteParticipants.forEach((participant) => { + participant.audioTrackPublications.forEach((publication) => { + if (publication.track) { + publication.track.setMuted(newDeafenState); + } + }); + }); + + // Also mute/unmute our mic when deafening (silently - no mute sound) + // Only change mic state if it doesn't match deafen state + if (newDeafenState && !this.activeCall.isMuted) { + // Deafening - also mute mic + this.livekitRoom.localParticipant.setMicrophoneEnabled(false); + this.activeCall.isMuted = true; + } else if (!newDeafenState && this.activeCall.isMuted) { + // Undeafening - also unmute mic + this.livekitRoom.localParticipant.setMicrophoneEnabled(true); + this.activeCall.isMuted = false; + } + + // Broadcast deafen state to other participants via metadata + const metadata = JSON.stringify({ isDeafened: newDeafenState }); + this.livekitRoom.localParticipant.setMetadata(metadata).catch((err) => { + console.warn('Failed to set participant metadata:', err); + }); + + // Play deafen/undeafen sound (these always play for user feedback) + getCallSounds().playSound( + newDeafenState ? CallSoundType.Deafen : CallSoundType.Undeafen + ); + + return newDeafenState; + } + /** * Toggles video enabled state */ @@ -658,7 +867,9 @@ export class CallService { if (screenSharePub?.track) { console.log(`Screen share started with bitrate: ${SCREEN_SHARE_BITRATES[audioSettings.screenShareBitrate].bitrate}kbps`); - // LiveKit handles bitrate via simulcast and adaptive streaming + // Capture local screen stream for preview + const mediaStream = new MediaStream([screenSharePub.track.mediaStreamTrack]); + this.activeCall.localScreenStream = mediaStream; } this.activeCall.isScreenSharing = true; @@ -693,6 +904,7 @@ export class CallService { try { await this.livekitRoom.localParticipant.setScreenShareEnabled(false); this.activeCall.isScreenSharing = false; + this.activeCall.localScreenStream = undefined; this.emit({ type: CallEventType.StateChanged, diff --git a/src/app/features/call/CallSounds.ts b/src/app/features/call/CallSounds.ts new file mode 100644 index 0000000..e233445 --- /dev/null +++ b/src/app/features/call/CallSounds.ts @@ -0,0 +1,157 @@ +/* eslint-disable no-console */ +import MicMuteSound from '../../../../public/sound/Mic_Mute.ogg'; +import MicUnmuteSound from '../../../../public/sound/Mic_Unmuted.ogg'; +import SpeakersDeafenSound from '../../../../public/sound/Speakers_Deafen.ogg'; +import SpeakersUndeafenSound from '../../../../public/sound/Speakers_Undeafen.ogg'; +import CallJoinedSound from '../../../../public/sound/Call_Joined.ogg'; +import CallDepartSound from '../../../../public/sound/Call_Depart.ogg'; +import CallDialingSound from '../../../../public/sound/Call_Dialing.ogg'; + +/** + * Sound types that can be played during a call + */ +export enum CallSoundType { + MicMute = 'mic_mute', + MicUnmute = 'mic_unmute', + Deafen = 'deafen', + Undeafen = 'undeafen', + CallJoined = 'call_joined', + CallDepart = 'call_depart', + CallDialing = 'call_dialing', +} + +/** + * Maps sound types to their audio sources + */ +const SOUND_SOURCES: Record = { + [CallSoundType.MicMute]: MicMuteSound, + [CallSoundType.MicUnmute]: MicUnmuteSound, + [CallSoundType.Deafen]: SpeakersDeafenSound, + [CallSoundType.Undeafen]: SpeakersUndeafenSound, + [CallSoundType.CallJoined]: CallJoinedSound, + [CallSoundType.CallDepart]: CallDepartSound, + [CallSoundType.CallDialing]: CallDialingSound, +}; + +/** + * Manages call-related sound effects. + * Handles playing local sounds and coordinating with remote participants. + */ +export class CallSounds { + private audioElements: Map = new Map(); + + private dialingLoopElement: HTMLAudioElement | null = null; + + private isDeafened = false; + + constructor() { + this.preloadSounds(); + } + + /** + * Preloads all call sounds for instant playback + */ + private preloadSounds(): void { + Object.entries(SOUND_SOURCES).forEach(([type, src]) => { + const audio = new Audio(src); + audio.preload = 'auto'; + this.audioElements.set(type as CallSoundType, audio); + }); + } + + /** + * Plays a call sound locally + * @param soundType - The type of sound to play + * @param loop - Whether to loop the sound (e.g., for dialing) + */ + playSound(soundType: CallSoundType, loop = false): void { + // Don't play sounds if deafened (except deafen/undeafen sounds for feedback) + if (this.isDeafened && + soundType !== CallSoundType.Deafen && + soundType !== CallSoundType.Undeafen) { + return; + } + + const audio = this.audioElements.get(soundType); + if (!audio) { + console.warn(`Sound not found: ${soundType}`); + return; + } + + // Stop any currently playing instance and reset + audio.currentTime = 0; + audio.loop = loop; + + // Track looping dialing sound separately + if (soundType === CallSoundType.CallDialing && loop) { + this.dialingLoopElement = audio; + } + + audio.play().catch((err) => { + console.warn(`Failed to play sound ${soundType}:`, err); + }); + } + + /** + * Stops the dialing loop sound if playing + */ + stopDialingLoop(): void { + if (this.dialingLoopElement) { + this.dialingLoopElement.pause(); + this.dialingLoopElement.currentTime = 0; + this.dialingLoopElement.loop = false; + this.dialingLoopElement = null; + } + } + + /** + * Sets the deafened state - affects whether sounds are played + * @param deafened - Whether the user is deafened + */ + setDeafened(deafened: boolean): void { + this.isDeafened = deafened; + } + + /** + * Gets whether sounds are currently deafened + */ + getIsDeafened(): boolean { + return this.isDeafened; + } + + /** + * Cleans up audio resources + */ + dispose(): void { + this.stopDialingLoop(); + this.audioElements.forEach((audio) => { + audio.pause(); + }); + this.audioElements.clear(); + } +} + +/** + * Singleton instance of CallSounds for app-wide use + */ +let callSoundsInstance: CallSounds | null = null; + +/** + * Gets or creates the CallSounds singleton instance + */ +export function getCallSounds(): CallSounds { + if (!callSoundsInstance) { + callSoundsInstance = new CallSounds(); + } + return callSoundsInstance; +} + +/** + * Disposes the CallSounds singleton + */ +export function disposeCallSounds(): void { + if (callSoundsInstance) { + callSoundsInstance.dispose(); + callSoundsInstance = null; + } +} diff --git a/src/app/features/call/IncomingCallNotification.css.ts b/src/app/features/call/IncomingCallNotification.css.ts new file mode 100644 index 0000000..b42c994 --- /dev/null +++ b/src/app/features/call/IncomingCallNotification.css.ts @@ -0,0 +1,119 @@ +import { style, keyframes } from '@vanilla-extract/css'; +import { config, DefaultReset, color } from 'folds'; + +const pulseRing = keyframes({ + '0%': { + transform: 'scale(0.95)', + opacity: 1, + }, + '50%': { + transform: 'scale(1.1)', + opacity: 0.5, + }, + '100%': { + transform: 'scale(0.95)', + opacity: 1, + }, +}); + +const slideIn = keyframes({ + '0%': { + transform: 'translateX(100%)', + opacity: 0, + }, + '100%': { + transform: 'translateX(0)', + opacity: 1, + }, +}); + +export const IncomingCallContainer = style([ + DefaultReset, + { + position: 'fixed', + top: config.space.S400, + right: config.space.S400, + zIndex: 10000, + animation: `${slideIn} 0.3s ease-out`, + }, +]); + +export const IncomingCallContent = style({ + backgroundColor: color.Surface.Container, + borderRadius: config.radii.R400, + padding: config.space.S400, + boxShadow: `0 4px 20px ${color.Other.Shadow}`, + minWidth: '300px', + maxWidth: '400px', + display: 'flex', + flexDirection: 'column', + gap: config.space.S400, + border: `1px solid ${color.Surface.ContainerLine}`, +}); + +export const CallerInfo = style({ + display: 'flex', + alignItems: 'center', + gap: config.space.S300, +}); + +export const CallerAvatarWrapper = style({ + position: 'relative', + width: '56px', + height: '56px', + flexShrink: 0, +}); + +export const CallerAvatar = style({ + width: '56px', + height: '56px', + borderRadius: '50%', + position: 'relative', + zIndex: 1, +}); + +export const CallerAvatarRing = style({ + position: 'absolute', + top: '-4px', + left: '-4px', + right: '-4px', + bottom: '-4px', + borderRadius: '50%', + border: `2px solid ${color.Success.Main}`, + animation: `${pulseRing} 1.5s ease-in-out infinite`, + zIndex: 0, +}); + +export const CallerName = style({ + color: color.Surface.OnContainer, + fontWeight: 600, +}); + +export const OtherCallers = style({ + color: color.Secondary.Main, + fontWeight: 400, +}); + +export const RoomName = style({ + color: color.Secondary.Main, + display: 'flex', + alignItems: 'center', + gap: config.space.S100, +}); + +export const CallActions = style({ + justifyContent: 'flex-end', + marginTop: config.space.S100, +}); + +export const DeclineButton = style({ + width: '48px', + height: '48px', + borderRadius: '50%', +}); + +export const AcceptButton = style({ + width: '48px', + height: '48px', + borderRadius: '50%', +}); diff --git a/src/app/features/call/IncomingCallNotification.tsx b/src/app/features/call/IncomingCallNotification.tsx new file mode 100644 index 0000000..2bc683b --- /dev/null +++ b/src/app/features/call/IncomingCallNotification.tsx @@ -0,0 +1,316 @@ +/* eslint-disable no-console */ +import React, { useCallback, useEffect, useState } from 'react'; +import { MatrixEvent, Room, RoomStateEvent } from 'matrix-js-sdk'; +import { Box, Icon, IconButton, Icons, Text, color } from 'folds'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { useCall } from './useCall'; +import { CallType } from './types'; +import { getCallSounds, CallSoundType } from './CallSounds'; +import { getMemberDisplayName, getMemberAvatarMxc } from '../../utils/room'; +import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix'; +import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; +import { UserAvatar } from '../../components/user-avatar'; +import * as css from './IncomingCallNotification.css'; + +/** The Matrix state event type for call membership (MSC3401) */ +const CALL_MEMBER_EVENT_TYPE = 'org.matrix.msc3401.call.member'; + +/** Represents an incoming call */ +interface IncomingCall { + roomId: string; + roomName: string; + callerIds: string[]; + startTime: number; +} + +/** + * Checks if a room is a DM or small group (not a server/space room) + * @param room - The Matrix room to check + * @returns true if the room is a DM or small group + */ +function isDirectOrSmallGroup(room: Room): boolean { + if (room.isSpaceRoom()) { + return false; + } + const memberCount = room.getJoinedMemberCount(); + return memberCount <= 10; +} + +/** + * Gets active call members from a room who are NOT the current user + * Also returns the earliest call start time + * @param room - The Matrix room + * @param myUserId - The current user's ID + * @returns Object with member IDs and earliest call start time + */ +function getOtherCallMembersWithTime(room: Room, myUserId: string): { members: string[]; earliestTime: number } { + const members: string[] = []; + const now = Date.now(); + let earliestTime = now; + + try { + const stateEvents = room.currentState.getStateEvents(CALL_MEMBER_EVENT_TYPE); + + stateEvents.forEach((event) => { + const content = event.getContent(); + const userId = event.getStateKey(); + + if (!userId || userId === myUserId || !content['m.calls']) return; + + const calls = content['m.calls']; + if (!Array.isArray(calls)) return; + + calls.forEach((call) => { + const devices = call['m.devices']; + if (!Array.isArray(devices)) return; + + devices.forEach((device) => { + const expiresTs = device.expires_ts || 0; + + if (expiresTs > now && !members.includes(userId)) { + members.push(userId); + // Track event origin time to determine call age + const eventTime = event.getTs() || now; + if (eventTime < earliestTime) { + earliestTime = eventTime; + } + } + }); + }); + }); + } catch (e) { + console.error('Error reading call member events:', e); + } + + return { members, earliestTime }; +} + +/** + * Wrapper for backward compatibility + */ +function getOtherCallMembers(room: Room, myUserId: string): string[] { + return getOtherCallMembersWithTime(room, myUserId).members; +} + +/** + * Component that monitors for incoming calls and displays a notification UI + * with room info, caller details, and accept/decline buttons. + */ +export function IncomingCallNotification() { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const { activeCall, startCall } = useCall(); + const [incomingCall, setIncomingCall] = useState(null); + const [declinedRooms, setDeclinedRooms] = useState>(new Set()); + + /** + * Checks all rooms for incoming calls + */ + const checkForIncomingCalls = useCallback(() => { + const myUserId = mx.getUserId(); + if (!myUserId) return; + + // If we're already in a call, don't show incoming call notification + if (activeCall) { + setIncomingCall(null); + return; + } + + // Check all joined rooms for calls + const rooms = mx.getRooms(); + + // Find the first room with an incoming call + const incomingRoom = rooms.find((room) => { + // Only monitor DMs and small groups + if (!isDirectOrSmallGroup(room)) return false; + // Skip rooms we've declined + if (declinedRooms.has(room.roomId)) return false; + const { members } = getOtherCallMembersWithTime(room, myUserId); + return members.length > 0; + }); + + if (incomingRoom) { + const { members, earliestTime } = getOtherCallMembersWithTime(incomingRoom, myUserId); + setIncomingCall({ + roomId: incomingRoom.roomId, + roomName: incomingRoom.name || 'Unknown Room', + callerIds: members, + startTime: earliestTime, + }); + } else { + setIncomingCall(null); + } + }, [mx, activeCall, declinedRooms]); + + /** + * Handles state events for call member changes + */ + const handleStateEvent = useCallback((event: MatrixEvent) => { + if (event.getType() === CALL_MEMBER_EVENT_TYPE) { + // Clear declined status if call ended in that room + const roomId = event.getRoomId(); + if (roomId && declinedRooms.has(roomId)) { + const room = mx.getRoom(roomId); + if (room) { + const myUserId = mx.getUserId(); + if (myUserId) { + const members = getOtherCallMembers(room, myUserId); + if (members.length === 0) { + // Call ended, remove from declined + setDeclinedRooms((prev) => { + const newSet = new Set(prev); + newSet.delete(roomId); + return newSet; + }); + } + } + } + } + checkForIncomingCalls(); + } + }, [checkForIncomingCalls, declinedRooms, mx]); + + // Set up event listeners + useEffect(() => { + mx.on(RoomStateEvent.Events, handleStateEvent); + checkForIncomingCalls(); + const interval = setInterval(checkForIncomingCalls, 10000); + + return () => { + mx.off(RoomStateEvent.Events, handleStateEvent); + clearInterval(interval); + }; + }, [mx, handleStateEvent, checkForIncomingCalls]); + + // Play/stop dialing sound based on incoming call state + // Only play sound if call started less than 15 seconds ago + useEffect(() => { + if (incomingCall) { + const callAge = Date.now() - incomingCall.startTime; + const MAX_CALL_AGE_FOR_SOUND = 15000; // 15 seconds + + if (callAge < MAX_CALL_AGE_FOR_SOUND) { + getCallSounds().playSound(CallSoundType.CallDialing, true); + console.log('🔔 Incoming call from room:', incomingCall.roomName, '(age:', Math.round(callAge / 1000), 's)'); + } else { + console.log('📞 Incoming call too old for sound:', incomingCall.roomName, '(age:', Math.round(callAge / 1000), 's)'); + getCallSounds().stopDialingLoop(); + } + } else { + getCallSounds().stopDialingLoop(); + } + + return () => { + getCallSounds().stopDialingLoop(); + }; + }, [incomingCall]); + + /** + * Handles accepting the incoming call + */ + const handleAccept = useCallback(async () => { + if (!incomingCall) return; + + getCallSounds().stopDialingLoop(); + try { + await startCall(incomingCall.roomId, CallType.Voice); + } catch (error) { + console.error('Failed to join call:', error); + } + }, [incomingCall, startCall]); + + /** + * Handles declining the incoming call + */ + const handleDecline = useCallback(() => { + if (!incomingCall) return; + + getCallSounds().stopDialingLoop(); + setDeclinedRooms((prev) => new Set(prev).add(incomingCall.roomId)); + setIncomingCall(null); + }, [incomingCall]); + + // Don't render if no incoming call + if (!incomingCall) { + return null; + } + + const room = mx.getRoom(incomingCall.roomId); + const firstCaller = incomingCall.callerIds[0]; + + /** + * Gets the display name for a user + */ + const getDisplayName = (userId: string): string => { + if (!room) return getMxIdLocalPart(userId) ?? userId; + return getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId; + }; + + /** + * Gets the avatar URL for a user + */ + const getAvatarUrl = (userId: string): string | undefined => { + if (!room) return undefined; + const mxcUrl = getMemberAvatarMxc(room, userId); + if (!mxcUrl) return undefined; + return mxcUrlToHttp(mx, mxcUrl, useAuthentication, 48, 48, 'crop') ?? undefined; + }; + + const callerName = getDisplayName(firstCaller); + const avatarUrl = getAvatarUrl(firstCaller); + const otherCallersCount = incomingCall.callerIds.length - 1; + + return ( +
+
+
+
+ ( + + {callerName.charAt(0).toUpperCase()} + + )} + /> +
+
+ + + {callerName} + {otherCallersCount > 0 && ( + + {` +${otherCallersCount} other${otherCallersCount > 1 ? 's' : ''}`} + + )} + + + + Calling from {incomingCall.roomName} + + +
+ + + + + + + + + +
+
+ ); +} diff --git a/src/app/features/call/index.ts b/src/app/features/call/index.ts index f3a6fce..6b769e8 100644 --- a/src/app/features/call/index.ts +++ b/src/app/features/call/index.ts @@ -1,7 +1,9 @@ export * from './types'; export * from './useRtcConfig'; export * from './CallService'; +export * from './CallSounds'; export * from './useCall'; export * from './CallProviderWrapper'; export * from './CallOverlay'; +export * from './IncomingCallNotification'; export * from './useRoomCallMembers'; diff --git a/src/app/features/call/types.ts b/src/app/features/call/types.ts index b8a4d69..f3b2e82 100644 --- a/src/app/features/call/types.ts +++ b/src/app/features/call/types.ts @@ -113,7 +113,10 @@ export interface ActiveCall { remoteScreenStreams: Map; remoteScreenTracks: Map; participants: string[]; + activeSpeakers: Set; + mutedParticipants: Set; isMuted: boolean; + isDeafened: boolean; isVideoEnabled: boolean; isScreenSharing: boolean; } @@ -129,6 +132,8 @@ export enum CallEventType { RemoteStreamRemoved = 'remote_stream_removed', ScreenShareStarted = 'screen_share_started', ScreenShareStopped = 'screen_share_stopped', + SpeakersChanged = 'speakers_changed', + RemoteMuteChanged = 'remote_mute_changed', Error = 'error', } diff --git a/src/app/features/call/useCall.ts b/src/app/features/call/useCall.ts index 7270ce5..47ddbee 100644 --- a/src/app/features/call/useCall.ts +++ b/src/app/features/call/useCall.ts @@ -12,6 +12,7 @@ interface CallContextValue { startCall: (roomId: string, callType: CallType) => Promise; endCall: () => Promise; toggleMute: () => boolean; + toggleDeafen: () => boolean; toggleVideo: () => boolean; toggleScreenShare: () => Promise; } @@ -113,15 +114,19 @@ export function useCallService(options?: UseCallServiceOptions): CallContextValu event.type === CallEventType.ScreenShareStarted || event.type === CallEventType.ScreenShareStopped || event.type === CallEventType.RemoteStreamAdded || - event.type === CallEventType.RemoteStreamRemoved) { + event.type === CallEventType.RemoteStreamRemoved || + event.type === CallEventType.ParticipantJoined || + event.type === CallEventType.ParticipantLeft || + event.type === CallEventType.SpeakersChanged) { const newCall = service?.getActiveCall(); if (newCall) { - // Deep copy Maps to ensure React detects changes + // Deep copy Maps and Sets to ensure React detects changes setActiveCall({ ...newCall, remoteStreams: new Map(newCall.remoteStreams), remoteScreenTracks: new Map(newCall.remoteScreenTracks), remoteScreenStreams: new Map(newCall.remoteScreenStreams), + activeSpeakers: new Set(newCall.activeSpeakers), }); } else { setActiveCall(null); @@ -173,6 +178,18 @@ export function useCallService(options?: UseCallServiceOptions): CallContextValu return newState; }, [callService]); + const toggleDeafen = useCallback(() => { + if (!callService) { + return false; + } + const newState = callService.toggleDeafen(); + const call = callService.getActiveCall(); + if (call) { + setActiveCall({ ...call }); + } + return newState; + }, [callService]); + const toggleVideo = useCallback(() => { if (!callService) { return false; @@ -206,10 +223,11 @@ export function useCallService(options?: UseCallServiceOptions): CallContextValu startCall, endCall, toggleMute, + toggleDeafen, toggleVideo, toggleScreenShare, }), - [callService, activeCall, callSupported, callSupportLoading, startCall, endCall, toggleMute, toggleVideo, toggleScreenShare] + [callService, activeCall, callSupported, callSupportLoading, startCall, endCall, toggleMute, toggleDeafen, toggleVideo, toggleScreenShare] ); } diff --git a/src/app/features/call/useRoomCallMembers.ts b/src/app/features/call/useRoomCallMembers.ts index e2063fe..cf79774 100644 --- a/src/app/features/call/useRoomCallMembers.ts +++ b/src/app/features/call/useRoomCallMembers.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { useCallback, useEffect, useState } from 'react'; import { MatrixEvent, Room, RoomStateEvent } from 'matrix-js-sdk'; import { useMatrixClient } from '../../hooks/useMatrixClient'; @@ -25,20 +26,20 @@ function getActiveCallMembers(room: Room): CallMember[] { try { const stateEvents = room.currentState.getStateEvents(CALL_MEMBER_EVENT_TYPE); - for (const event of stateEvents) { + stateEvents.forEach((event) => { const content = event.getContent(); const userId = event.getStateKey(); - if (!userId || !content['m.calls']) continue; + if (!userId || !content['m.calls']) return; const calls = content['m.calls']; - if (!Array.isArray(calls)) continue; + if (!Array.isArray(calls)) return; - for (const call of calls) { + calls.forEach((call) => { const devices = call['m.devices']; - if (!Array.isArray(devices)) continue; + if (!Array.isArray(devices)) return; - for (const device of devices) { + devices.forEach((device) => { const expiresTs = device.expires_ts || 0; // Check if the call membership is still valid (not expired) @@ -50,9 +51,9 @@ function getActiveCallMembers(room: Room): CallMember[] { expiresTs, }); } - } - } - } + }); + }); + }); } catch (e) { console.error('Error reading call member events:', e); } diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 2281b59..9e729be 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -127,6 +127,17 @@ import { useTheme } from '../../hooks/useTheme'; import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag'; import { usePowerLevelTags } from '../../hooks/usePowerLevelTags'; +/** Information about a call member event for grouping */ +interface CallEventInfo { + senderId: string; + senderName: string; + isJoining: boolean; + ts: number; + item: number; + mEventId: string; + mEvent: MatrixEvent; +} + const TimelineFloat = as<'div', css.TimelineFloatVariants>( ({ position, className, ...props }, ref) => ( ([]); + const ignoredUsersList = useIgnoredUsers(); const ignoredUsersSet = useMemo(() => new Set(ignoredUsersList), [ignoredUsersList]); @@ -1468,6 +1482,46 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli ); }, + [StateEvent.CallMember]: (mEventId, mEvent, item) => { + const senderId = mEvent.getSender() ?? ''; + const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId); + const content = mEvent.getContent(); + const prevContent = mEvent.getPrevContent(); + + // Check current content for active devices + const calls = content['m.calls']; + const hasDevices = Array.isArray(calls) && calls.some((call: { 'm.devices'?: unknown[] }) => + Array.isArray(call['m.devices']) && call['m.devices'].length > 0 + ); + + // Check previous content for active devices + const prevCalls = prevContent['m.calls']; + const hadDevices = Array.isArray(prevCalls) && prevCalls.some((call: { 'm.devices'?: unknown[] }) => + Array.isArray(call['m.devices']) && call['m.devices'].length > 0 + ); + + // Determine if this is a join or leave + const isJoining = hasDevices && !hadDevices; + const isLeaving = !hasDevices && hadDevices; + + // Skip if no change (refresh events or device updates within same call) + if (!isJoining && !isLeaving) return null; + + // Add to pending call events for grouping + // Return a special marker that eventRenderer will recognize + pendingCallEventsRef.current.push({ + senderId, + senderName, + isJoining, + ts: mEvent.getTs(), + item, + mEventId, + mEvent, + }); + + // Return special marker for call event (will be handled by eventRenderer) + return 'CALL_EVENT_PENDING' as unknown as React.ReactNode; + }, }, (mEventId, mEvent, item) => { if (!showHiddenEvents) return null; @@ -1571,7 +1625,128 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli let isPrevRendered = false; let newDivider = false; let dayDivider = false; - const eventRenderer = (item: number) => { + + // Clear pending call events at the start of each render + pendingCallEventsRef.current = []; + + /** + * Renders a summary of accumulated call events + */ + const renderCallEventSummary = (callEvents: CallEventInfo[]): React.ReactNode => { + if (callEvents.length === 0) return null; + + // Group by user, tracking the sequence of actions to preserve order + const userActions = new Map(); + + callEvents.forEach((evt) => { + const existing = userActions.get(evt.senderId); + const action = evt.isJoining ? 'join' : 'leave'; + + if (existing) { + existing.lastAction = action; + if (evt.isJoining) { + existing.joins += 1; + } else { + existing.leaves += 1; + } + } else { + userActions.set(evt.senderId, { + senderName: evt.senderName, + firstAction: action, + lastAction: action, + joins: evt.isJoining ? 1 : 0, + leaves: evt.isJoining ? 0 : 1, + }); + } + }); + + // Build summary text + const summaryParts: string[] = []; + userActions.forEach((actions) => { + const { joins, leaves, senderName, firstAction, lastAction } = actions; + const total = joins + leaves; + + if (total === 1) { + // Single action + summaryParts.push(`${senderName} ${firstAction === 'join' ? 'joined' : 'left'}`); + } else if (joins > 0 && leaves > 0) { + // Mixed joins and leaves - describe based on first action and cycles + const startedWith = firstAction === 'join' ? 'joined' : 'left'; + const opposite = firstAction === 'join' ? 'left' : 'joined'; + const cycles = Math.min(joins, leaves); + + if (firstAction === lastAction) { + // Ended on same action as started (odd number of same action) + // e.g., join-leave-join or leave-join-leave + if (cycles === 1) { + summaryParts.push(`${senderName} ${startedWith}, ${opposite}, then ${startedWith} again`); + } else { + summaryParts.push(`${senderName} ${startedWith} and ${opposite} ${cycles} times, then ${startedWith}`); + } + } else { + // Ended on opposite action (equal joins/leaves or started-opposite pattern) + if (cycles === 1) { + summaryParts.push(`${senderName} ${startedWith}, then ${opposite}`); + } else { + summaryParts.push(`${senderName} ${startedWith} and ${opposite} ${cycles} times`); + } + } + } else if (joins > 1) { + summaryParts.push(`${senderName} joined ${joins} times`); + } else if (leaves > 1) { + summaryParts.push(`${senderName} left ${leaves} times`); + } + }); + + // Use the first event for rendering metadata + const firstEvent = callEvents[0]; + const lastEvent = callEvents[callEvents.length - 1]; + + const timeJSX = ( + + } + /> + + ); + }; + + const eventRenderer = (item: number, index: number, allItems: number[]) => { const [eventTimeline, baseIndex] = getTimelineAndBaseIndex(timeline.linkedTimelines, item); if (!eventTimeline) return null; const timelineSet = eventTimeline?.getTimelineSet(); @@ -1604,7 +1779,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli prevEvent.getType() === mEvent.getType() && minuteDifference(prevEvent.getTs(), mEvent.getTs()) < 2; - const eventJSX = reactionOrEditEvent(mEvent) + const rawEventJSX = reactionOrEditEvent(mEvent) ? null : renderMatrixEvent( mEvent.getType(), @@ -1615,11 +1790,30 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli timelineSet, collapsed ); + + // Check if this is a call event that should be grouped + const isCallEventPending = rawEventJSX === ('CALL_EVENT_PENDING' as unknown as React.ReactNode); + + // Determine if we need to flush pending call events + // Flush when: we hit a non-call event, or this is the last item + const isLastItem = index === allItems.length - 1; + const pendingCallEvents = pendingCallEventsRef.current; + const shouldFlushCallEvents = pendingCallEvents.length > 0 && (!isCallEventPending || isLastItem); + + let callSummaryJSX: React.ReactNode = null; + if (shouldFlushCallEvents) { + callSummaryJSX = renderCallEventSummary([...pendingCallEvents]); + pendingCallEventsRef.current = []; // Clear array + } + + // If this was a call event marker, don't render it directly + const eventJSX = isCallEventPending ? null : rawEventJSX; + prevEvent = mEvent; - isPrevRendered = !!eventJSX; + isPrevRendered = !!eventJSX || !!callSummaryJSX; const newDividerJSX = - newDivider && eventJSX && eventSender !== mx.getUserId() ? ( + newDivider && (eventJSX || callSummaryJSX) && eventSender !== mx.getUserId() ? ( @@ -1630,7 +1824,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli ) : null; const dayDividerJSX = - dayDivider && eventJSX ? ( + dayDivider && (eventJSX || callSummaryJSX) ? ( @@ -1646,7 +1840,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli ) : null; - if (eventJSX && (newDividerJSX || dayDividerJSX)) { + // Handle various combinations of dividers, call summary, and regular events + const hasContent = eventJSX || callSummaryJSX; + const hasDividers = newDividerJSX || dayDividerJSX; + + if (hasContent && hasDividers) { if (newDividerJSX) newDivider = false; if (dayDividerJSX) dayDivider = false; @@ -1654,12 +1852,22 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli {newDividerJSX} {dayDividerJSX} + {callSummaryJSX} {eventJSX} ); } - return eventJSX; + if (callSummaryJSX && eventJSX) { + return ( + + {callSummaryJSX} + {eventJSX} + + ); + } + + return callSummaryJSX || eventJSX; }; return ( diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index 2fd460b..2112e92 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -21,6 +21,7 @@ import { RectCords, Badge, Spinner, + color, } from 'folds'; import { useNavigate } from 'react-router-dom'; import { JoinRule, Room } from 'matrix-js-sdk'; @@ -71,6 +72,8 @@ import { useRoomPermissions } from '../../hooks/useRoomPermissions'; import { InviteUserPrompt } from '../../components/invite-user-prompt'; import { useRoomCall, useRoomCallMembers } from '../call'; import { CallState, CallType } from '../call/types'; +import { UserAvatar } from '../../components/user-avatar'; +import { getMemberDisplayName, getMemberAvatarMxc } from '../../utils/room'; type RoomMenuProps = { room: Room; @@ -261,53 +264,106 @@ type CallIndicatorProps = { }; /** - * Shows when others are in a call in this room + * Shows all users currently in a call in this room (including self) */ function CallIndicator({ roomId }: CallIndicatorProps) { - const { othersInCall, isCallActive } = useRoomCallMembers(roomId); + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const { callMembers, isCallActive } = useRoomCallMembers(roomId); + const room = mx.getRoom(roomId); - if (!isCallActive || othersInCall.length === 0) { + if (!isCallActive || callMembers.length === 0) { return null; } - const userCount = othersInCall.length; - const tooltipText = userCount === 1 - ? `${othersInCall[0].userId.split(':')[0].slice(1)} is in a call` - : `${userCount} people are in a call`; + const getAvatarUrl = (userId: string): string | undefined => { + if (!room) return undefined; + const mxcUrl = getMemberAvatarMxc(room, userId); + if (!mxcUrl) return undefined; + return mxcUrlToHttp(mx, mxcUrl, useAuthentication, 24, 24, 'crop') ?? undefined; + }; + + const getDisplayName = (userId: string): string => { + if (!room) return userId.split(':')[0].slice(1); + return getMemberDisplayName(room, userId) ?? userId.split(':')[0].slice(1); + }; return ( - - {tooltipText} - - } + - {(triggerRef) => ( - - + + {callMembers.slice(0, 5).map((member) => { + const displayName = getDisplayName(member.userId); + const avatarUrl = getAvatarUrl(member.userId); + return ( + + {/* Speech bubble arrow */} +
+ {displayName} + {member.userId} + + } + > + {(triggerRef) => ( + + ( + + {displayName.charAt(0).toUpperCase()} + + )} + /> + + )} + + ); + })} + {callMembers.length > 5 && ( - {userCount} in call + +{callMembers.length - 5} - - )} - + )} + + ); } diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index b861a06..a5e09ef 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -32,7 +32,7 @@ import FocusTrap from 'focus-trap-react'; import { Page, PageContent, PageHeader } from '../../../components/page'; import { SequenceCard } from '../../../components/sequence-card'; import { useSetting } from '../../../state/hooks/settings'; -import { DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings'; +import { DateFormat, EmojiStyle, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings'; import { SettingTile } from '../../../components/setting-tile'; import { KeySymbol } from '../../../utils/key-symbol'; import { isMacOS } from '../../../utils/user-agent'; @@ -303,10 +303,50 @@ function PageZoomInput() { ); } +const emojiStyleNames: Record = { + [EmojiStyle.System]: 'System', + [EmojiStyle.Apple]: 'Apple', + [EmojiStyle.Twemoji]: 'Twemoji', +}; + +type EmojiStyleSelectorProps = { + selected: EmojiStyle; + onSelect: (style: EmojiStyle) => void; +}; +const EmojiStyleSelector = as<'div', EmojiStyleSelectorProps>( + ({ selected, onSelect, ...props }, ref) => ( + + + {Object.values(EmojiStyle).map((style) => ( + onSelect(style)} + > + {emojiStyleNames[style]} + + ))} + + + ) +); + function Appearance() { const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme'); const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode'); - const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); + const [emojiStyle, setEmojiStyle] = useSetting(settingsAtom, 'emojiStyle'); + const [emojiMenuCords, setEmojiMenuCords] = useState(); + + const handleEmojiMenu: MouseEventHandler = (evt) => { + setEmojiMenuCords(evt.currentTarget.getBoundingClientRect()); + }; + + const handleEmojiStyleSelect = (style: EmojiStyle) => { + setEmojiStyle(style); + setEmojiMenuCords(undefined); + }; return ( @@ -342,8 +382,45 @@ function Appearance() { } + title="Emoji Style" + description="Choose which emoji set to display." + after={ + <> + + setEmojiMenuCords(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => + evt.key === 'ArrowDown' || evt.key === 'ArrowRight', + isKeyBackward: (evt: KeyboardEvent) => + evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', + escapeDeactivates: stopPropagation, + }} + > + + + } + /> + + } /> diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx index ee63c77..30e1aa9 100644 --- a/src/app/pages/client/ClientNonUIFeatures.tsx +++ b/src/app/pages/client/ClientNonUIFeatures.tsx @@ -10,7 +10,7 @@ import NotificationSound from '../../../../public/sound/notification.ogg'; import InviteSound from '../../../../public/sound/invite.ogg'; import { notificationPermission, setFavicon } from '../../utils/dom'; import { useSetting } from '../../state/hooks/settings'; -import { settingsAtom } from '../../state/settings'; +import { EmojiStyle, settingsAtom } from '../../state/settings'; import { allInvitesAtom } from '../../state/room-list/inviteList'; import { usePreviousValue } from '../../hooks/usePreviousValue'; import { useMatrixClient } from '../../hooks/useMatrixClient'; @@ -28,13 +28,26 @@ import { useInboxNotificationsSelected } from '../../hooks/router/useInbox'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { isTauri, sendNotification, setupNotificationTapListener } from '../../utils/tauri'; -function SystemEmojiFeature() { - const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); +/** + * Applies the selected emoji style font to the document. + * - System: Uses the native OS emoji font + * - Apple: Uses Apple Color Emoji (bundled font) + * - Twemoji: Uses Twitter's Twemoji font + */ +function EmojiStyleFeature() { + const [emojiStyle] = useSetting(settingsAtom, 'emojiStyle'); - if (twitterEmoji) { - document.documentElement.style.setProperty('--font-emoji', 'Twemoji'); - } else { - document.documentElement.style.setProperty('--font-emoji', 'Twemoji_DISABLED'); + switch (emojiStyle) { + case EmojiStyle.Apple: + document.documentElement.style.setProperty('--font-emoji', 'AppleColorEmoji'); + break; + case EmojiStyle.Twemoji: + document.documentElement.style.setProperty('--font-emoji', 'Twemoji'); + break; + case EmojiStyle.System: + default: + document.documentElement.style.setProperty('--font-emoji', 'SystemEmoji'); + break; } return null; @@ -316,7 +329,7 @@ type ClientNonUIFeaturesProps = { export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) { return ( <> - + diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 31ee6cc..864b0d5 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -9,6 +9,12 @@ export enum MessageLayout { Bubble = 2, } +export enum EmojiStyle { + System = 'system', + Apple = 'apple', + Twemoji = 'twemoji', +} + export interface Settings { themeId?: string; useSystemTheme: boolean; @@ -17,7 +23,7 @@ export interface Settings { monochromeMode?: boolean; isMarkdown: boolean; editorToolbar: boolean; - twitterEmoji: boolean; + emojiStyle: EmojiStyle; pageZoom: number; hideActivity: boolean; @@ -51,7 +57,7 @@ const defaultSettings: Settings = { monochromeMode: false, isMarkdown: true, editorToolbar: false, - twitterEmoji: false, + emojiStyle: EmojiStyle.Apple, pageZoom: 100, hideActivity: false, @@ -80,9 +86,18 @@ const defaultSettings: Settings = { export const getSettings = () => { const settings = localStorage.getItem(STORAGE_KEY); if (settings === null) return defaultSettings; + + const parsed = JSON.parse(settings) as Settings & { twitterEmoji?: boolean }; + + // Migrate old twitterEmoji boolean to new emojiStyle enum + if ('twitterEmoji' in parsed && parsed.emojiStyle === undefined) { + parsed.emojiStyle = parsed.twitterEmoji ? EmojiStyle.Twemoji : EmojiStyle.Apple; + delete parsed.twitterEmoji; + } + return { ...defaultSettings, - ...(JSON.parse(settings) as Settings), + ...parsed, }; }; diff --git a/src/index.css b/src/index.css index 3612138..7704600 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,4 @@ +/* Twemoji font (Twitter emoji) */ @font-face { font-family: Twemoji; src: url('../public/font/Twemoji.Mozilla.v15.1.0.woff2'), @@ -5,6 +6,28 @@ font-display: swap; } +/* Apple Color Emoji - uses local Apple Color Emoji if available (macOS/iOS), + falls back to Segoe UI Emoji (Windows) or Noto (Linux/Android) */ +@font-face { + font-family: AppleColorEmoji; + src: local('Apple Color Emoji'), + local('Segoe UI Emoji'), + local('Noto Color Emoji'); + font-display: swap; +} + +/* System emoji - uses native OS emoji */ +@font-face { + font-family: SystemEmoji; + src: local('Apple Color Emoji'), + local('Segoe UI Emoji'), + local('Segoe UI Symbol'), + local('Noto Color Emoji'), + local('Android Emoji'), + local('EmojiOne Color'); + font-display: swap; +} + :root { --tc-link: hsl(213deg 100% 45%); @@ -18,7 +41,7 @@ --mx-uc-7: hsl(242, 100%, 45%); --mx-uc-8: hsl(94, 100%, 35%); - --font-emoji: 'Twemoji_DISABLED'; + --font-emoji: 'AppleColorEmoji'; --font-secondary: 'InterVariable', var(--font-emoji), sans-serif; } diff --git a/src/types/matrix/room.ts b/src/types/matrix/room.ts index b866fd7..5ab3672 100644 --- a/src/types/matrix/room.ts +++ b/src/types/matrix/room.ts @@ -38,6 +38,9 @@ export enum StateEvent { PoniesRoomEmotes = 'im.ponies.room_emotes', PowerLevelTags = 'in.cinny.room.power_level_tags', + + /** Matrix RTC call membership (MSC3401) */ + CallMember = 'org.matrix.msc3401.call.member', } export enum MessageEvent {