feature(ui) - design changes
This commit is contained in:
parent
2e77aa3ca7
commit
2c2fa5b0b5
11 changed files with 155 additions and 50 deletions
29
frontend/app/components/Assist/ChatControls/ChatControls.css
Normal file
29
frontend/app/components/Assist/ChatControls/ChatControls.css
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
.controls {
|
||||
height: 38px;
|
||||
/* margin-top: 5px; */
|
||||
/* background-color: white; */
|
||||
/* border-top: solid thin #CCC; */
|
||||
}
|
||||
|
||||
.btnWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
color: $gray-medium;
|
||||
|
||||
&.disabled {
|
||||
/* background-color: red; */
|
||||
& svg {
|
||||
fill: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.endButton {
|
||||
background-color: $red;
|
||||
border-radius: 3px;
|
||||
padding: 2px 8px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
}
|
||||
54
frontend/app/components/Assist/ChatControls/ChatControls.tsx
Normal file
54
frontend/app/components/Assist/ChatControls/ChatControls.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import React, { useState } from 'react'
|
||||
import stl from './ChatControls.css'
|
||||
import cn from 'classnames'
|
||||
import { Button, Icon } from 'UI'
|
||||
|
||||
interface Props {
|
||||
stream: MediaStream | null,
|
||||
endCall: () => void
|
||||
}
|
||||
function ChatControls({ stream, endCall } : Props) {
|
||||
const [audioEnabled, setAudioEnabled] = useState(true)
|
||||
const [videoEnabled, setVideoEnabled] = useState(true)
|
||||
|
||||
const toggleAudio = () => {
|
||||
if (!stream) { return; }
|
||||
const aEn = !audioEnabled
|
||||
stream.getAudioTracks().forEach(track => track.enabled = aEn);
|
||||
setAudioEnabled(aEn);
|
||||
}
|
||||
|
||||
const toggleVideo = () => {
|
||||
if (!stream) { return; }
|
||||
const vEn = !videoEnabled;
|
||||
stream.getVideoTracks().forEach(track => track.enabled = vEn);
|
||||
setVideoEnabled(vEn)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(stl.controls, "flex items-center w-full justify-start bottom-0 px-2")}>
|
||||
<div className="flex items-center">
|
||||
<div className={cn(stl.btnWrapper, { [stl.disabled]: !audioEnabled})}>
|
||||
<Button plain size="small" onClick={toggleAudio} noPadding className="flex items-center">
|
||||
<Icon name={audioEnabled ? 'mic' : 'mic-mute'} size="16" />
|
||||
<span className="ml-2 color-gray-medium text-sm">{audioEnabled ? 'Mute' : 'Unmute'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={cn(stl.btnWrapper, { [stl.disabled]: !videoEnabled})}>
|
||||
<Button plain size="small" onClick={toggleVideo} noPadding className="flex items-center">
|
||||
<Icon name={ videoEnabled ? 'camera-video' : 'camera-video-off' } size="16" />
|
||||
<span className="ml-2 color-gray-medium text-sm">{videoEnabled ? 'Stop Video' : 'Start Video'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<button className={stl.endButton} onClick={endCall}>
|
||||
END
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatControls
|
||||
1
frontend/app/components/Assist/ChatControls/index.js
Normal file
1
frontend/app/components/Assist/ChatControls/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ChatControls'
|
||||
|
|
@ -1,29 +1,35 @@
|
|||
import React, { useState, FC } from 'react'
|
||||
import VideoContainer from '../components/VideoContainer'
|
||||
import { Icon, Popup } from 'UI'
|
||||
import { Icon, Popup, Button } from 'UI'
|
||||
import cn from 'classnames'
|
||||
import Counter from 'App/components/shared/SessionItem/Counter'
|
||||
import stl from './chatWindow.css'
|
||||
import ChatControls from '../ChatControls/ChatControls'
|
||||
|
||||
export interface Props {
|
||||
incomeStream: MediaStream | null,
|
||||
localStream: MediaStream | null
|
||||
// call: (oStream: MediaStream, cb: (iStream: MediaStream)=>void)=>void
|
||||
localStream: MediaStream | null,
|
||||
userId: String,
|
||||
endCall: () => void
|
||||
}
|
||||
|
||||
const ChatWindow: FC<Props> = function ChatWindow({ incomeStream, localStream }) {
|
||||
const ChatWindow: FC<Props> = function ChatWindow({ userId, incomeStream, localStream, endCall }) {
|
||||
const [minimize, setMinimize] = useState(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed border radius bg-white z-50 shadow-xl mt-16 p-2"
|
||||
style={{ width: '220px' }}
|
||||
className={cn(stl.wrapper, "fixed radius bg-white z-50 shadow-xl mt-16")}
|
||||
style={{ width: '280px' }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center p-2">
|
||||
<div>
|
||||
<button onClick={() => setMinimize(!minimize)}>
|
||||
<div className={stl.headerTitle}><b>Meeting</b> {userId}</div>
|
||||
{/* <button onClick={() => setMinimize(!minimize)}>
|
||||
<Icon name={ minimize ? "plus" : "minus" } size="14" />
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
<Popup
|
||||
<Counter startTime={new Date().getTime() } className="text-sm ml-auto" />
|
||||
{/* <Popup
|
||||
trigger={
|
||||
<button className="flex items-center ml-auto">
|
||||
<Icon name="high-engagement" size="16" color="teal" />
|
||||
|
|
@ -33,14 +39,16 @@ const ChatWindow: FC<Props> = function ChatWindow({ incomeStream, localStream })
|
|||
size="tiny"
|
||||
inverted
|
||||
position="top center"
|
||||
/>
|
||||
/> */}
|
||||
</div>
|
||||
<div className={cn({'hidden' : minimize}, 'mt-2')}>
|
||||
<div className={cn({'hidden' : minimize}, 'relative')}>
|
||||
<VideoContainer stream={ incomeStream } />
|
||||
<div className="py-1" />
|
||||
<VideoContainer stream={ localStream } muted/>
|
||||
<div className="absolute bottom-0 right-0 z-50">
|
||||
<VideoContainer stream={ localStream } muted height={50} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChatControls stream={localStream} endCall={endCall} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
.wrapepr {
|
||||
.wrapper {
|
||||
background-color: white;
|
||||
border: solid thin #CCC;
|
||||
border: solid thin #000;
|
||||
border-radius: 3px;
|
||||
position: fixed;
|
||||
height: 400px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
font-size: 12px;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -3,6 +3,6 @@ import ChatWindow from './ChatWindow';
|
|||
|
||||
storiesOf('Assist', module)
|
||||
.add('ChatWindow', () => (
|
||||
<ChatWindow />
|
||||
<ChatWindow userId="test@test.com" />
|
||||
))
|
||||
|
||||
|
|
|
|||
|
|
@ -56,15 +56,16 @@ function AssistActions({ toggleChatWindow, userId, calling }: Props) {
|
|||
<Popup
|
||||
trigger={
|
||||
<div
|
||||
className={cn('cursor-pointer p-2 mr-2')}
|
||||
className={cn('cursor-pointer p-2 mr-2 flex items-center')}
|
||||
onClick={inCall ? endCall : call}
|
||||
role="button"
|
||||
>
|
||||
<Icon
|
||||
name="telephone-fill"
|
||||
name="headset"
|
||||
size="20"
|
||||
color={ calling !== 2 ? "red" : "teal" }
|
||||
color={ inCall ? "red" : "gray-darkest" }
|
||||
/>
|
||||
<span className={cn("ml-2", { 'red' : inCall })}>{ inCall ? 'End Meeting' : 'Start Meeting' }</span>
|
||||
</div>
|
||||
}
|
||||
content={ `Call ${userId}` }
|
||||
|
|
@ -73,7 +74,7 @@ function AssistActions({ toggleChatWindow, userId, calling }: Props) {
|
|||
position="top right"
|
||||
/>
|
||||
<div className="fixed ml-3 left-0 top-0 z-50">
|
||||
{ inCall && <ChatWindow incomeStream={incomeStream} localStream={localStream} /> }
|
||||
{ inCall && <ChatWindow endCall={endCall} userId={userId} incomeStream={incomeStream} localStream={localStream} /> }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.controls {
|
||||
margin-bottom: 8px;
|
||||
margin-right: 15px;
|
||||
.controls {
|
||||
height: 28px;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.btnWrapper {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import stl from './VideoContainer.css'
|
|||
|
||||
interface Props {
|
||||
stream: MediaStream | null
|
||||
muted?: boolean
|
||||
muted?: boolean,
|
||||
height?: number
|
||||
}
|
||||
|
||||
function VideoContainer({ stream, muted = false }: Props) {
|
||||
const [audioEnabled, setAudioEnabled] = useState(true)
|
||||
const [videoEnabled, setVideoEnabled] = useState(true)
|
||||
function VideoContainer({ stream, muted = false, height = 280 }: Props) {
|
||||
// const [audioEnabled, setAudioEnabled] = useState(true)
|
||||
// const [videoEnabled, setVideoEnabled] = useState(true)
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -19,25 +20,25 @@ function VideoContainer({ stream, muted = false }: Props) {
|
|||
}
|
||||
}, [ ref.current, stream ])
|
||||
|
||||
const toggleAudio = () => {
|
||||
if (!stream) { return; }
|
||||
const aEn = !audioEnabled
|
||||
stream.getAudioTracks().forEach(track => track.enabled = aEn);
|
||||
setAudioEnabled(aEn);
|
||||
}
|
||||
// const toggleAudio = () => {
|
||||
// if (!stream) { return; }
|
||||
// const aEn = !audioEnabled
|
||||
// stream.getAudioTracks().forEach(track => track.enabled = aEn);
|
||||
// setAudioEnabled(aEn);
|
||||
// }
|
||||
|
||||
const toggleVideo = () => {
|
||||
if (!stream) { return; }
|
||||
const vEn = !videoEnabled;
|
||||
stream.getVideoTracks().forEach(track => track.enabled = vEn);
|
||||
setVideoEnabled(vEn)
|
||||
}
|
||||
// const toggleVideo = () => {
|
||||
// if (!stream) { return; }
|
||||
// const vEn = !videoEnabled;
|
||||
// stream.getVideoTracks().forEach(track => track.enabled = vEn);
|
||||
// setVideoEnabled(vEn)
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="relative bg-gray-light-shade" style={{ height: '152px' }}>
|
||||
<div className="absolute inset-0 flex justify-center border border-gray-300 bg-white radius bg-opacity-25">
|
||||
<video autoPlay ref={ ref } muted={ muted } />
|
||||
<div className={cn(stl.controls, "flex items-center absolute w-full justify-end bottom-0")}>
|
||||
<div className="relative bg-gray-light-shade">
|
||||
<div className="justify-center border border-gray-800 radius bg-opacity-25">
|
||||
<video autoPlay ref={ ref } muted={ muted } style={{ width: height }} />
|
||||
{/* <div className={cn(stl.controls, "flex items-center border-t w-full justify-start bottom-0")}>
|
||||
<div className={cn(stl.btnWrapper, { [stl.disabled]: !audioEnabled})}>
|
||||
<Button plain size="small" onClick={toggleAudio} noPadding>
|
||||
<Icon name={audioEnabled ? 'mic' : 'mic-mute'} size="14" />
|
||||
|
|
@ -49,7 +50,7 @@ function VideoContainer({ stream, muted = false }: Props) {
|
|||
<Icon name={ videoEnabled ? 'camera-video' : 'camera-video-off' } size="14" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ import React, { useState, useEffect } from 'react'
|
|||
import { Duration } from 'luxon';
|
||||
|
||||
interface Props {
|
||||
startTime: any
|
||||
startTime: any,
|
||||
className: string
|
||||
}
|
||||
|
||||
function Counter({ startTime }: Props) {
|
||||
function Counter({ startTime, className }: Props) {
|
||||
let intervalId;
|
||||
const [duration, setDuration] = useState(new Date().getTime() - startTime)
|
||||
|
||||
|
|
@ -19,7 +20,7 @@ function Counter({ startTime }: Props) {
|
|||
}, [duration])
|
||||
|
||||
return (
|
||||
<div className="mx-2">
|
||||
<div className={className}>
|
||||
{startTime && Duration.fromMillis(duration).toFormat('m:ss')}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
3
frontend/app/svg/icons/headset.svg
Normal file
3
frontend/app/svg/icons/headset.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-headset" viewBox="0 0 16 16">
|
||||
<path d="M8 1a5 5 0 0 0-5 5v1h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6a6 6 0 1 1 12 0v6a2.5 2.5 0 0 1-2.5 2.5H9.366a1 1 0 0 1-.866.5h-1a1 1 0 1 1 0-2h1a1 1 0 0 1 .866.5H11.5A1.5 1.5 0 0 0 13 12h-1a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h1V6a5 5 0 0 0-5-5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 349 B |
Loading…
Add table
Reference in a new issue