diff --git a/src/button/RaisedHandToggleButton.module.css b/src/button/ReactionToggleButton.module.css similarity index 73% rename from src/button/RaisedHandToggleButton.module.css rename to src/button/ReactionToggleButton.module.css index d6920cc0..317d85b5 100644 --- a/src/button/RaisedHandToggleButton.module.css +++ b/src/button/ReactionToggleButton.module.css @@ -21,7 +21,11 @@ .reactionPopupMenuItem { list-style: none; - gap: 1em; + margin-left: var(--cpd-separator-spacing); +} + +.reactionPopupMenuItem:first-child { + margin-left: 0; } .reactionButton { @@ -37,3 +41,13 @@ margin-left: var(--cpd-separator-spacing); margin-right: var(--cpd-separator-spacing); } + +.searchForm { + display: flex; + flex-direction: row; + gap: var(--cpd-separator-spacing); +} + +.searchForm > label { + flex: auto; +} diff --git a/src/button/RaisedHandToggleButton.tsx b/src/button/ReactionToggleButton.tsx similarity index 79% rename from src/button/RaisedHandToggleButton.tsx rename to src/button/ReactionToggleButton.tsx index 00b5d223..a808c9c7 100644 --- a/src/button/RaisedHandToggleButton.tsx +++ b/src/button/ReactionToggleButton.tsx @@ -12,7 +12,11 @@ import { Search, Form, } from "@vector-im/compound-web"; -import { ReactionIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { + SearchIcon, + ReactionIcon, + CloseIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; import { ChangeEventHandler, ComponentPropsWithoutRef, @@ -35,6 +39,7 @@ import { ECallReactionEventContent, ReactionOption, ReactionSet, + ElementCallReactionEventType, } from "../reactions"; interface InnerButtonProps extends ComponentPropsWithoutRef<"button"> { @@ -69,6 +74,7 @@ export function ReactionPopupMenu({ }): ReactNode { const { t } = useTranslation(); const [searchText, setSearchText] = useState(""); + const [isSearching, setIsSearching] = useState(false); const onSearch = useCallback>((ev) => { ev.preventDefault(); setSearchText(ev.target.value.trim().toLocaleLowerCase()); @@ -78,11 +84,14 @@ export function ReactionPopupMenu({ () => ReactionSet.filter( (reaction) => - reaction.name.startsWith(searchText) || - reaction.alias?.some((a) => a.startsWith(searchText)), + !isSearching || + (!!searchText && + (reaction.name.startsWith(searchText) || + reaction.alias?.some((a) => a.startsWith(searchText)))), ).slice(0, 6), - [searchText], + [searchText, isSearching], ); + return (
@@ -99,14 +108,28 @@ export function ReactionPopupMenu({
- e.preventDefault()}> - - - + {isSearching ? ( + <> + e.preventDefault()} + > + + setIsSearching(false)} + /> + + + + ) : null} {filteredReactionSet.map((reaction) => (
  • @@ -123,21 +146,33 @@ export function ReactionPopupMenu({
  • ))} + {!isSearching ? ( +
  • + + setIsSearching(true)} + /> + +
  • + ) : null}
    ); } -interface RaisedHandToggleButtonProps { +interface ReactionToggleButtonProps { rtcSession: MatrixRTCSession; client: MatrixClient; } -export function RaiseHandToggleButton({ +export function ReactionToggleButton({ client, rtcSession, -}: RaisedHandToggleButtonProps): ReactNode { +}: ReactionToggleButtonProps): ReactNode { const { raisedHands, myReactionId, reactions } = useReactions(); const [busy, setBusy] = useState(false); const userId = client.getUserId()!; @@ -161,7 +196,7 @@ export function RaiseHandToggleButton({ await client.sendEvent( rtcSession.room.roomId, null, - "io.element.call.reaction", + ElementCallReactionEventType, { "m.relates_to": { rel_type: RelationType.Reference, @@ -171,7 +206,7 @@ export function RaiseHandToggleButton({ name: reaction.name, } as ECallReactionEventContent, ); - setShowReactionsMenu(false); + // Do NOT close the menu after this. } catch (ex) { logger.error("Failed to send reaction", ex); } finally { @@ -222,6 +257,7 @@ export function RaiseHandToggleButton({ logger.error("Failed to send reaction event", ex); } finally { setBusy(false); + setShowReactionsMenu(false); } } }; diff --git a/src/button/index.ts b/src/button/index.ts index e4e7cfad..07b19866 100644 --- a/src/button/index.ts +++ b/src/button/index.ts @@ -7,4 +7,4 @@ Please see LICENSE in the repository root for full details. export * from "./Button"; export * from "./LinkButton"; -export * from "./RaisedHandToggleButton"; +export * from "./ReactionToggleButton"; diff --git a/src/reactions/index.ts b/src/reactions/index.ts index a9f2fde3..0be6024e 100644 --- a/src/reactions/index.ts +++ b/src/reactions/index.ts @@ -7,17 +7,22 @@ Please see LICENSE in the repository root for full details. import { RelationType } from "matrix-js-sdk/src/types"; -import catSoundOgg from "../sound/reactions/cat.mp3?url"; +import catSoundOgg from "../sound/reactions/cat.ogg?url"; import catSoundMp3 from "../sound/reactions/cat.mp3?url"; -import cricketsSoundOgg from "../sound/reactions/crickets.mp3?url"; +import clapSoundOgg from "../sound/reactions/clap.ogg?url"; +import clapSoundMp3 from "../sound/reactions/clap.mp3?url"; +import cricketsSoundOgg from "../sound/reactions/crickets.ogg?url"; import cricketsSoundMp3 from "../sound/reactions/crickets.mp3?url"; import dogSoundOgg from "../sound/reactions/dog.ogg?url"; import dogSoundMp3 from "../sound/reactions/dog.mp3?url"; -import genericSoundOgg from "../sound/reactions/generic.mp3?url"; +import genericSoundOgg from "../sound/reactions/generic.ogg?url"; import genericSoundMp3 from "../sound/reactions/generic.mp3?url"; -import lightbulbSoundOgg from "../sound/reactions/lightbulb.mp3?url"; +import lightbulbSoundOgg from "../sound/reactions/lightbulb.ogg?url"; import lightbulbSoundMp3 from "../sound/reactions/lightbulb.mp3?url"; +import partySoundOgg from "../sound/reactions/party.ogg?url"; +import partySoundMp3 from "../sound/reactions/party.mp3?url"; +export const ElementCallReactionEventType = "io.element.call.reaction"; export interface ReactionOption { emoji: string; @@ -47,21 +52,30 @@ export const GenericReaction: ReactionOption = { }, }; +// The first 6 reactions are always visible. export const ReactionSet: ReactionOption[] = [ { emoji: "πŸ‘", name: "thumbsup", alias: ["+1", "yes", "thumbs up"], }, - { - emoji: "πŸ‘Ž", - name: "thumbsdown", - alias: ["-1", "no", "thumbs no"], - }, { emoji: "πŸŽ‰", name: "party", alias: ["hurray", "success"], + sound: { + ogg: partySoundOgg, + mp3: partySoundMp3, + }, + }, + { + emoji: "πŸ‘", + name: "clapping", + alias: ["celebrate", "success"], + sound: { + ogg: clapSoundOgg, + mp3: clapSoundMp3, + }, }, { emoji: "🐢", @@ -72,15 +86,6 @@ export const ReactionSet: ReactionOption[] = [ mp3: dogSoundMp3, }, }, - { - emoji: "πŸ¦—", - name: "crickets", - alias: ["awkward", "silence"], - sound: { - ogg: cricketsSoundOgg, - mp3: cricketsSoundMp3, - }, - }, { emoji: "🐱", name: "cat", @@ -99,6 +104,20 @@ export const ReactionSet: ReactionOption[] = [ mp3: lightbulbSoundMp3, }, }, + { + emoji: "πŸ¦—", + name: "crickets", + alias: ["awkward", "silence"], + sound: { + ogg: cricketsSoundOgg, + mp3: cricketsSoundMp3, + }, + }, + { + emoji: "πŸ‘Ž", + name: "thumbsdown", + alias: ["-1", "no", "thumbs no"], + }, { emoji: "πŸ˜΅β€πŸ’«", name: "dizzy", @@ -109,4 +128,14 @@ export const ReactionSet: ReactionOption[] = [ name: "ok", alias: ["okay", "cool"], }, + { + emoji: "πŸ₯°", + name: "heart", + alias: ["heart", "love", "smiling"], + }, + { + emoji: "πŸ˜„", + name: "laugh", + alias: ["giggle", "joy", "smiling"], + }, ]; diff --git a/src/room/InCallView.module.css b/src/room/InCallView.module.css index e6a67547..600a57de 100644 --- a/src/room/InCallView.module.css +++ b/src/room/InCallView.module.css @@ -147,11 +147,11 @@ Please see LICENSE in the repository root for full details. .floatingReaction { position: relative; + display: inline; z-index: 2; font-size: 32pt; - animation-duration: 5s; + animation-duration: 4s; animation-name: reaction-up; - bottom: 0; width: fit-content; } diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index f93bbfca..e486e721 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -41,7 +41,7 @@ import { VideoButton, ShareScreenButton, SettingsButton, - RaiseHandToggleButton, + ReactionToggleButton, } from "../button"; import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header"; import { useUrlParams } from "../UrlParams"; @@ -83,6 +83,7 @@ import { makeSpotlightPortraitLayout } from "../grid/SpotlightPortraitLayout"; import { ReactionsProvider, useReactions } from "../useReactions"; import handSoundOgg from "../sound/raise_hand.ogg?url"; import handSoundMp3 from "../sound/raise_hand.mp3?url"; +import { ReactionsAudioRenderer } from "./ReactionAudioRenderer"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); @@ -182,12 +183,6 @@ export const InCallView: FC = ({ ); 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], - ); - const reactionsIcons = useMemo( () => Object.entries(reactions).map(([sender, { emoji }]) => ({ @@ -558,7 +553,7 @@ export const InCallView: FC = ({ } if (supportsReactions) { buttons.push( - = ({ - {reactionsSet.map( - (r) => - r.sound && ( - - ), - )} + {reactionsIcons.map(({ sender, emoji, startX }) => ( + {expectedReactions.map( + (r) => + r.sound && ( + + ), + )} + + ); +} diff --git a/src/sound/reactions/clap.mp3 b/src/sound/reactions/clap.mp3 new file mode 100644 index 00000000..680a60f7 Binary files /dev/null and b/src/sound/reactions/clap.mp3 differ diff --git a/src/sound/reactions/clap.ogg b/src/sound/reactions/clap.ogg new file mode 100644 index 00000000..17363923 Binary files /dev/null and b/src/sound/reactions/clap.ogg differ diff --git a/src/sound/reactions/party.mp3 b/src/sound/reactions/party.mp3 new file mode 100644 index 00000000..5004353f Binary files /dev/null and b/src/sound/reactions/party.mp3 differ diff --git a/src/sound/reactions/party.ogg b/src/sound/reactions/party.ogg new file mode 100644 index 00000000..92a4bc1d Binary files /dev/null and b/src/sound/reactions/party.ogg differ