| 
									
										
										
										
											2022-02-08 12:30:33 -06:00
										 |  |  | // Copyright 2022 Signal Messenger, LLC
 | 
					
						
							|  |  |  | // SPDX-License-Identifier: AGPL-3.0-only
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-09 16:51:57 -07:00
										 |  |  | import classNames from 'classnames'; | 
					
						
							| 
									
										
										
										
											2022-02-25 09:24:05 -06:00
										 |  |  | import { noop } from 'lodash'; | 
					
						
							| 
									
										
										
										
											2022-02-08 12:30:33 -06:00
										 |  |  | import type { ReactElement } from 'react'; | 
					
						
							| 
									
										
										
										
											2022-05-23 15:00:01 -07:00
										 |  |  | import React, { useEffect, useState } from 'react'; | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  | import { useSpring, animated } from '@react-spring/web'; | 
					
						
							| 
									
										
										
										
											2022-02-08 12:30:33 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  | import { AUDIO_LEVEL_INTERVAL_MS } from '../calling/constants'; | 
					
						
							|  |  |  | import { missingCaseError } from '../util/missingCaseError'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const SPEAKING_LINGER_MS = 500; | 
					
						
							| 
									
										
										
										
											2022-02-25 09:24:05 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-09 16:51:57 -07:00
										 |  |  | const BASE_CLASS_NAME = 'CallingAudioIndicator'; | 
					
						
							|  |  |  | const CONTENT_CLASS_NAME = `${BASE_CLASS_NAME}__content`; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  | const MIN_BAR_HEIGHT = 2; | 
					
						
							|  |  |  | const SIDE_SCALE_FACTOR = 0.75; | 
					
						
							|  |  |  | const MAX_CENTRAL_BAR_DELTA = 9; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* Should match css */ | 
					
						
							| 
									
										
										
										
											2022-05-23 15:00:01 -07:00
										 |  |  | const CONTENT_WIDTH = 14; | 
					
						
							|  |  |  | const CONTENT_HEIGHT = 14; | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  | const BAR_WIDTH = 2; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-23 15:00:01 -07:00
										 |  |  | const CONTENT_PADDING = 1; | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | enum BarPosition { | 
					
						
							|  |  |  |   Left, | 
					
						
							|  |  |  |   Center, | 
					
						
							|  |  |  |   Right, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function generateBarPath(position: BarPosition, audioLevel: number): string { | 
					
						
							|  |  |  |   let x: number; | 
					
						
							|  |  |  |   if (position === BarPosition.Left) { | 
					
						
							|  |  |  |     x = CONTENT_PADDING; | 
					
						
							|  |  |  |   } else if (position === BarPosition.Center) { | 
					
						
							| 
									
										
										
										
											2022-05-23 15:00:01 -07:00
										 |  |  |     x = CONTENT_WIDTH / 2 - BAR_WIDTH / 2; | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  |   } else if (position === BarPosition.Right) { | 
					
						
							|  |  |  |     x = CONTENT_WIDTH - CONTENT_PADDING - BAR_WIDTH; | 
					
						
							|  |  |  |   } else { | 
					
						
							|  |  |  |     throw missingCaseError(position); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-23 15:00:01 -07:00
										 |  |  |   x = Math.round(x); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  |   let height: number; | 
					
						
							|  |  |  |   if (position === BarPosition.Left || position === BarPosition.Right) { | 
					
						
							|  |  |  |     height = | 
					
						
							|  |  |  |       MIN_BAR_HEIGHT + audioLevel * MAX_CENTRAL_BAR_DELTA * SIDE_SCALE_FACTOR; | 
					
						
							|  |  |  |   } else if (position === BarPosition.Center) { | 
					
						
							|  |  |  |     height = MIN_BAR_HEIGHT + audioLevel * MAX_CENTRAL_BAR_DELTA; | 
					
						
							|  |  |  |   } else { | 
					
						
							|  |  |  |     throw missingCaseError(position); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Take the round corners off the height
 | 
					
						
							|  |  |  |   height -= 2; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const y = (CONTENT_HEIGHT - height) / 2; | 
					
						
							| 
									
										
										
										
											2022-05-23 15:00:01 -07:00
										 |  |  |   const left = x; | 
					
						
							|  |  |  |   const right = x + BAR_WIDTH; | 
					
						
							|  |  |  |   const top = y.toFixed(2); | 
					
						
							|  |  |  |   const bottom = (y + height).toFixed(2); | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return ( | 
					
						
							| 
									
										
										
										
											2022-05-23 15:00:01 -07:00
										 |  |  |     `M ${left} ${top} ` + | 
					
						
							|  |  |  |     `L ${left} ${bottom} ` + | 
					
						
							|  |  |  |     `A 0.5 0.5 0 0 0 ${right} ${bottom} ` + | 
					
						
							|  |  |  |     `L ${right} ${top} ` + | 
					
						
							|  |  |  |     `A 0.5 0.5 0 0 0 ${left} ${top}` | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  |   ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-23 15:00:01 -07:00
										 |  |  | function generateCombinedPath(audioLevel: number): string { | 
					
						
							|  |  |  |   return ( | 
					
						
							|  |  |  |     `${generateBarPath(BarPosition.Left, audioLevel)} ` + | 
					
						
							|  |  |  |     `${generateBarPath(BarPosition.Center, audioLevel)} ` + | 
					
						
							|  |  |  |     `${generateBarPath(BarPosition.Right, audioLevel)} ` | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function Bars({ audioLevel }: { audioLevel: number }): ReactElement { | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  |   const animatedProps = useSpring({ | 
					
						
							|  |  |  |     from: { audioLevel: 0 }, | 
					
						
							|  |  |  |     config: { duration: AUDIO_LEVEL_INTERVAL_MS }, | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     animatedProps.audioLevel.stop(); | 
					
						
							|  |  |  |     animatedProps.audioLevel.start(audioLevel); | 
					
						
							|  |  |  |   }, [audioLevel, animatedProps]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return ( | 
					
						
							|  |  |  |     <animated.path | 
					
						
							| 
									
										
										
										
											2022-05-23 15:00:01 -07:00
										 |  |  |       d={animatedProps.audioLevel.to(generateCombinedPath)} | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  |       fill="#ffffff" | 
					
						
							|  |  |  |     /> | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-08 12:30:33 -06:00
										 |  |  | export function CallingAudioIndicator({ | 
					
						
							| 
									
										
										
										
											2022-02-25 09:24:05 -06:00
										 |  |  |   hasAudio, | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  |   audioLevel, | 
					
						
							|  |  |  | }: Readonly<{ hasAudio: boolean; audioLevel: number }>): ReactElement { | 
					
						
							|  |  |  |   const [shouldShowSpeaking, setShouldShowSpeaking] = useState(audioLevel > 0); | 
					
						
							| 
									
										
										
										
											2022-02-25 09:24:05 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  |     if (audioLevel > 0) { | 
					
						
							| 
									
										
										
										
											2022-05-12 14:03:43 -07:00
										 |  |  |       setShouldShowSpeaking(true); | 
					
						
							|  |  |  |     } else if (shouldShowSpeaking) { | 
					
						
							|  |  |  |       const timeout = setTimeout(() => { | 
					
						
							|  |  |  |         setShouldShowSpeaking(false); | 
					
						
							| 
									
										
										
										
											2022-02-25 09:24:05 -06:00
										 |  |  |       }, SPEAKING_LINGER_MS); | 
					
						
							| 
									
										
										
										
											2022-05-12 14:03:43 -07:00
										 |  |  |       return () => { | 
					
						
							|  |  |  |         clearTimeout(timeout); | 
					
						
							|  |  |  |       }; | 
					
						
							| 
									
										
										
										
											2022-02-25 09:24:05 -06:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2022-05-12 14:03:43 -07:00
										 |  |  |     return noop; | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  |   }, [audioLevel, shouldShowSpeaking]); | 
					
						
							| 
									
										
										
										
											2022-02-25 09:24:05 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |   if (!hasAudio) { | 
					
						
							| 
									
										
										
										
											2022-02-08 12:30:33 -06:00
										 |  |  |     return ( | 
					
						
							| 
									
										
										
										
											2022-05-09 16:51:57 -07:00
										 |  |  |       <div | 
					
						
							|  |  |  |         className={classNames( | 
					
						
							|  |  |  |           BASE_CLASS_NAME, | 
					
						
							|  |  |  |           `${BASE_CLASS_NAME}--with-content` | 
					
						
							|  |  |  |         )} | 
					
						
							|  |  |  |       > | 
					
						
							|  |  |  |         <div | 
					
						
							|  |  |  |           className={classNames( | 
					
						
							|  |  |  |             CONTENT_CLASS_NAME, | 
					
						
							|  |  |  |             `${CONTENT_CLASS_NAME}--muted` | 
					
						
							|  |  |  |           )} | 
					
						
							|  |  |  |         /> | 
					
						
							|  |  |  |       </div> | 
					
						
							| 
									
										
										
										
											2022-02-08 12:30:33 -06:00
										 |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-12 14:03:43 -07:00
										 |  |  |   if (shouldShowSpeaking) { | 
					
						
							| 
									
										
										
										
											2022-02-08 12:30:33 -06:00
										 |  |  |     return ( | 
					
						
							| 
									
										
										
										
											2022-05-09 16:51:57 -07:00
										 |  |  |       <div | 
					
						
							|  |  |  |         className={classNames( | 
					
						
							|  |  |  |           BASE_CLASS_NAME, | 
					
						
							|  |  |  |           `${BASE_CLASS_NAME}--with-content` | 
					
						
							|  |  |  |         )} | 
					
						
							|  |  |  |       > | 
					
						
							| 
									
										
										
										
											2022-05-23 15:00:01 -07:00
										 |  |  |         <svg | 
					
						
							|  |  |  |           xmlns="http://www.w3.org/2000/svg" | 
					
						
							|  |  |  |           className={CONTENT_CLASS_NAME} | 
					
						
							|  |  |  |           viewBox={`0 0 ${CONTENT_WIDTH} ${CONTENT_HEIGHT}`} | 
					
						
							|  |  |  |           width={CONTENT_WIDTH} | 
					
						
							|  |  |  |           height={CONTENT_HEIGHT} | 
					
						
							|  |  |  |           style={{ transform: 'translate3d(0px, 0px, 0px)' }} | 
					
						
							|  |  |  |         > | 
					
						
							|  |  |  |           <Bars audioLevel={audioLevel} /> | 
					
						
							| 
									
										
										
										
											2022-05-18 20:28:51 -07:00
										 |  |  |         </svg> | 
					
						
							| 
									
										
										
										
											2022-05-09 16:51:57 -07:00
										 |  |  |       </div> | 
					
						
							| 
									
										
										
										
											2022-02-08 12:30:33 -06:00
										 |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Render an empty spacer so that names don't move around.
 | 
					
						
							| 
									
										
										
										
											2022-05-09 16:51:57 -07:00
										 |  |  |   return <div className={BASE_CLASS_NAME} />; | 
					
						
							| 
									
										
										
										
											2022-02-08 12:30:33 -06:00
										 |  |  | } |