diff --git a/src/chat-api/store/useVoiceUsers.ts b/src/chat-api/store/useVoiceUsers.ts index c9e8f57d..14cb05d9 100644 --- a/src/chat-api/store/useVoiceUsers.ts +++ b/src/chat-api/store/useVoiceUsers.ts @@ -25,6 +25,7 @@ export type VoiceUser = RawVoice & { videoStream?: MediaStream; vad?: VADInstance; voiceActivity: boolean; + audio?: HTMLAudioElement }; // voiceUsers[channelId][userId] = VoiceUser @@ -185,7 +186,7 @@ const getVoiceUsers = (channelId: string): VoiceUser[] => { }; const getVoiceUser = (channelId: string, userId: string) => { - return voiceUsers[channelId][userId]; + return voiceUsers[channelId]?.[userId]; }; export async function createPeer(voiceUser: VoiceUser | RawVoice) { @@ -315,9 +316,10 @@ const onStream = (voiceUser: VoiceUser | RawVoice, stream: MediaStream) => { stream.onremovetrack = null; }; + let mic: HTMLAudioElement | undefined = undefined; if (streamType === "audioStream") { setVAD(stream, voiceUser); - const mic = new Audio(); + mic = new Audio(); const deviceId = getStorageString(StorageKeys.outputDeviceId, undefined); if (deviceId) { mic.setSinkId(JSON.parse(deviceId)); @@ -327,6 +329,7 @@ const onStream = (voiceUser: VoiceUser | RawVoice, stream: MediaStream) => { } setVoiceUsers(voiceUser.channelId, voiceUser.userId, { [streamType]: stream, + audio: mic, }); }; diff --git a/src/components/main-pane-header/MainPaneHeader.tsx b/src/components/main-pane-header/MainPaneHeader.tsx index 101983dd..a3a6808d 100644 --- a/src/components/main-pane-header/MainPaneHeader.tsx +++ b/src/components/main-pane-header/MainPaneHeader.tsx @@ -40,6 +40,7 @@ import { hasBit, } from "@/chat-api/Bitwise"; import { WebcamModal } from "./WebcamModal"; +import MemberContextMenu from "../member-context-menu/MemberContextMenu"; export default function MainPaneHeader() { const { @@ -444,6 +445,11 @@ function VoiceParticipantItem(props: { onClick: () => void; }) { const { voiceUsers } = useStore(); + const params = useParams<{ serverId?: string; channelId?: string }>(); + const [contextPosition, setContextPosition] = createSignal(null); const isMuted = () => { return !voiceUsers.micEnabled( @@ -467,9 +473,14 @@ function VoiceParticipantItem(props: { event.preventDefault(); } }; + const onContextMenu = (event: MouseEvent) => { + event.preventDefault(); + setContextPosition({ x: event.clientX, y: event.clientY }); + }; return ( + { + setContextPosition(null); + }} + /> & { serverId?: string; userId: string; @@ -153,6 +154,7 @@ export default function MemberContextMenu(props: Props) { return ( <> } {...props} items={[ { @@ -178,6 +180,62 @@ export default function MemberContextMenu(props: Props) { ); } +function Header(props: { userId: string }) { + const [voiceVolume, setVoiceVolume] = createSignal(1); + const store = useStore(); + const user = () => store.users.get(props.userId); + + const voiceUser = () => + store.voiceUsers.getVoiceUser( + store.voiceUsers.currentVoiceChannelId()!, + props.userId + ); + const audio = () => voiceUser()?.audio; + + createEffect( + on(audio, () => { + const audio = voiceUser()?.audio; + if (!audio) return; + console.log(audio.volume); + setVoiceVolume(audio.volume); + }) + ); + + const isMe = () => user()?.id === store.account.user()?.id; + + const onVolumeChange = (e: any) => { + setVoiceVolume(Number(e.currentTarget?.value!)); + const audio = voiceUser()?.audio; + if (!audio) return; + audio.volume = Number(e.currentTarget?.value!); + }; + + return ( + +
+
+ +
{user()!.username}
+
+
+ + +
+
Call Volume
+ +
+
+
+ ); +} + function KickModal(props: { member: ServerMember; close: () => void }) { const [requestSent, setRequestSent] = createSignal(false); const onKickClick = async () => { diff --git a/src/components/member-context-menu/styles.module.scss b/src/components/member-context-menu/styles.module.scss index b890e43e..97a8e7a3 100644 --- a/src/components/member-context-menu/styles.module.scss +++ b/src/components/member-context-menu/styles.module.scss @@ -68,3 +68,38 @@ margin-top: 10px; } } + +.header { + display: flex; + flex-direction: column; +} +.headerDetails { + position: relative; + display: flex; + align-items: center; + gap: 6px; + height: 34px; + margin-left: 6px; + font-size: 14px; + + .username { + position: absolute; + overflow: hidden; + text-overflow: ellipsis; + font-weight: bold; + white-space: nowrap; + left: 30px; + right: 0; + } +} + +.voiceVolume { + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 4px; + .label { + font-size: 14px; + margin-left: 4px; + } +} diff --git a/src/components/message-pane/message-item/MessageItem.tsx b/src/components/message-pane/message-item/MessageItem.tsx index d75c368e..1ace10f6 100644 --- a/src/components/message-pane/message-item/MessageItem.tsx +++ b/src/components/message-pane/message-item/MessageItem.tsx @@ -991,7 +991,7 @@ const FileEmbed = (props: { }; }) => { const { createPortal } = useCustomPortal(); - const isImage = () => props.file?.mime.startsWith("image/"); + const isImage = () => props.file?.mime?.startsWith("image/"); const previewClick = () => { createPortal((close) => ( diff --git a/src/components/ui/context-menu/ContextMenu.tsx b/src/components/ui/context-menu/ContextMenu.tsx index 73e26e7a..81657817 100644 --- a/src/components/ui/context-menu/ContextMenu.tsx +++ b/src/components/ui/context-menu/ContextMenu.tsx @@ -42,10 +42,11 @@ export interface ContextMenuProps { x: number; y: number; } | null; + header?: JSXElement; } export default function ContextMenu(props: ContextMenuProps) { - let [contextMenuEl, setContextMenuEl] = createSignal( + const [contextMenuEl, setContextMenuEl] = createSignal( null ); const [pos, setPos] = createSignal({ top: "0", left: "0" }); @@ -174,6 +175,7 @@ export default function ContextMenu(props: ContextMenuProps) { style={isMobileWidth() ? {} : pos()} >
+ {props.header} {(item, i) => (