You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			274 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			274 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			TypeScript
		
	
| // Audio Player
 | |
| import React, { useEffect, useRef, useState } from 'react';
 | |
| import H5AudioPlayer, { RHAP_UI } from 'react-h5-audio-player';
 | |
| import { useDispatch, useSelector } from 'react-redux';
 | |
| import styled from 'styled-components';
 | |
| import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
 | |
| import { setNextMessageToPlayId } from '../../state/ducks/conversations';
 | |
| import { useMessageSelected } from '../../state/selectors';
 | |
| import {
 | |
|   getNextMessageToPlayId,
 | |
|   getSortedMessagesOfSelectedConversation,
 | |
|   isMessageSelectionMode,
 | |
| } from '../../state/selectors/conversations';
 | |
| import { getAudioAutoplay } from '../../state/selectors/userConfig';
 | |
| import { SessionButton, SessionButtonType } from '../basic/SessionButton';
 | |
| import { SessionIcon } from '../icon';
 | |
| 
 | |
| const StyledSpeedButton = styled.div`
 | |
|   padding: var(--margins-xs);
 | |
|   transition: none;
 | |
| 
 | |
|   .session-button {
 | |
|     transition: none;
 | |
|     width: 34px;
 | |
|     padding: 0px;
 | |
|   }
 | |
| `;
 | |
| 
 | |
| export const StyledH5AudioPlayer = styled(H5AudioPlayer)<{ dropShadow?: boolean }>`
 | |
|   &.rhap_container {
 | |
|     min-width: 220px;
 | |
|     padding: 0px;
 | |
|     outline: none;
 | |
|     padding: var(--padding-message-content);
 | |
|     border-radius: var(--border-radius-message-box);
 | |
| 
 | |
|     svg {
 | |
|       transition: fill var(--default-duration);
 | |
|     }
 | |
| 
 | |
|     button {
 | |
|       outline: none;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   .rhap_progress-container {
 | |
|     margin: 0 0 0 calc(10px + 1%);
 | |
|     outline: none;
 | |
|   }
 | |
| 
 | |
|   .rhap_total-time {
 | |
|     display: none;
 | |
|   }
 | |
| 
 | |
|   .rhap_current-time {
 | |
|     margin: 0 5px 0 4px;
 | |
|     flex-shrink: 0;
 | |
|   }
 | |
| 
 | |
|   .rhap_play-pause-button {
 | |
|     display: flex;
 | |
|     justify-content: center;
 | |
|     align-items: center;
 | |
|   }
 | |
| 
 | |
|   .rhap_volume-bar {
 | |
|     display: none;
 | |
|   }
 | |
| 
 | |
|   .rhap_volume-button {
 | |
|     .module-message__container--incoming & {
 | |
|       color: var(--message-bubbles-received-text-color);
 | |
|     }
 | |
|     .module-message__container--outgoing & {
 | |
|       color: var(--message-bubbles-sent-text-color);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   .rhap_volume-container div[role='progressbar'] {
 | |
|     display: none;
 | |
|   }
 | |
| 
 | |
|   .rhap_time {
 | |
|     .module-message__container--incoming & {
 | |
|       color: var(--message-bubbles-received-text-color);
 | |
|     }
 | |
|     .module-message__container--outgoing & {
 | |
|       color: var(--message-bubbles-sent-text-color);
 | |
|     }
 | |
| 
 | |
|     font-size: 12px;
 | |
|   }
 | |
| 
 | |
|   .rhap_progress-bar {
 | |
|     box-sizing: border-box;
 | |
|     position: relative;
 | |
|     z-index: 0;
 | |
|     width: 100%;
 | |
|     height: 5px;
 | |
|     border-radius: 2px;
 | |
|   }
 | |
| 
 | |
|   .rhap_progress-filled {
 | |
|     padding-left: 5px;
 | |
|   }
 | |
| 
 | |
|   .rhap_download-progress {
 | |
|     height: 100%;
 | |
|     position: absolute;
 | |
|     z-index: 1;
 | |
|     border-radius: 2px;
 | |
|   }
 | |
| 
 | |
|   .rhap_progress-indicator {
 | |
|     z-index: 3;
 | |
|     width: 15px;
 | |
|     height: 15px;
 | |
|     top: -5px;
 | |
|     margin-left: -10px;
 | |
|     box-shadow: rgba(0, 0, 0, 0.5) 0 0 5px !important;
 | |
|   }
 | |
| 
 | |
|   .rhap_controls-section {
 | |
|     display: flex;
 | |
|     justify-content: space-between;
 | |
|     align-items: center;
 | |
|   }
 | |
| 
 | |
|   .rhap_additional-controls {
 | |
|     display: none;
 | |
|   }
 | |
| 
 | |
|   .rhap_play-pause-button {
 | |
|     width: unset;
 | |
|     height: unset;
 | |
|   }
 | |
| 
 | |
|   .rhap_controls-section {
 | |
|     flex: unset;
 | |
|     justify-content: flex-start;
 | |
|   }
 | |
| 
 | |
|   .rhap_volume-button {
 | |
|     font-size: 20px;
 | |
|     width: 20px;
 | |
|     height: 20px;
 | |
|     margin-right: 0px;
 | |
|   }
 | |
| 
 | |
|   ${props => props.dropShadow && 'box-shadow: var(--drop-shadow);'}
 | |
| `;
 | |
| 
 | |
| export const AudioPlayerWithEncryptedFile = (props: {
 | |
|   src: string;
 | |
|   contentType: string;
 | |
|   messageId: string;
 | |
| }) => {
 | |
|   const { messageId, contentType, src } = props;
 | |
|   const dispatch = useDispatch();
 | |
|   const [playbackSpeed, setPlaybackSpeed] = useState(1.0);
 | |
|   const { urlToLoad } = useEncryptedFileFetch(src, contentType, false);
 | |
|   const player = useRef<H5AudioPlayer | null>(null);
 | |
| 
 | |
|   const autoPlaySetting = useSelector(getAudioAutoplay);
 | |
|   const messageProps = useSelector(getSortedMessagesOfSelectedConversation);
 | |
|   const nextMessageToPlayId = useSelector(getNextMessageToPlayId);
 | |
|   const multiSelectMode = useSelector(isMessageSelectionMode);
 | |
|   const selected = useMessageSelected(messageId);
 | |
| 
 | |
|   const dataTestId = `audio-${messageId}`;
 | |
| 
 | |
|   useEffect(() => {
 | |
|     // Updates datatestId once rendered
 | |
|     if (
 | |
|       player.current?.audio.current &&
 | |
|       player.current?.container.current &&
 | |
|       player.current.container.current.dataset.testId !== dataTestId
 | |
|     ) {
 | |
|       // NOTE we can't assign the value using dataset.testId because the result is data-test-id not data-testid which is our convention
 | |
|       player.current.container.current.setAttribute('data-testid', dataTestId);
 | |
|     }
 | |
|   }, [dataTestId, player]);
 | |
| 
 | |
|   useEffect(() => {
 | |
|     // updates playback speed to value selected in context menu
 | |
|     if (
 | |
|       player.current?.audio.current &&
 | |
|       player.current?.audio.current?.playbackRate !== playbackSpeed
 | |
|     ) {
 | |
|       player.current.audio.current.playbackRate = playbackSpeed;
 | |
|     }
 | |
|   }, [playbackSpeed, player]);
 | |
| 
 | |
|   useEffect(() => {
 | |
|     if (messageId !== undefined && messageId === nextMessageToPlayId) {
 | |
|       void player.current?.audio.current?.play();
 | |
|     }
 | |
|   }, [messageId, nextMessageToPlayId, player]);
 | |
| 
 | |
|   const triggerPlayNextMessageIfNeeded = (endedMessageId: string) => {
 | |
|     const justEndedMessageIndex = messageProps.findIndex(
 | |
|       m => m.propsForMessage.id === endedMessageId
 | |
|     );
 | |
|     if (justEndedMessageIndex === -1) {
 | |
|       // make sure that even with switching convo or stuff, the next message to play is unset
 | |
|       dispatch(setNextMessageToPlayId(undefined));
 | |
| 
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const isLastMessage = justEndedMessageIndex === 0;
 | |
| 
 | |
|     // to prevent autoplaying as soon as a message is received.
 | |
|     if (isLastMessage) {
 | |
|       dispatch(setNextMessageToPlayId(undefined));
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // justEndedMessageIndex cannot be -1 nor 0, so it is >= 1
 | |
|     const nextMessageIndex = justEndedMessageIndex - 1;
 | |
|     // stop auto-playing when the audio messages change author.
 | |
|     const prevAuthorNumber = messageProps[justEndedMessageIndex].propsForMessage.sender;
 | |
|     const nextAuthorNumber = messageProps[nextMessageIndex].propsForMessage.sender;
 | |
|     const differentAuthor = prevAuthorNumber !== nextAuthorNumber;
 | |
|     if (differentAuthor) {
 | |
|       dispatch(setNextMessageToPlayId(undefined));
 | |
|     } else {
 | |
|       dispatch(setNextMessageToPlayId(messageProps[nextMessageIndex].propsForMessage.id));
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const onEnded = () => {
 | |
|     // if audio autoplay is enabled, call method to start playing
 | |
|     // the next playable message
 | |
|     if (autoPlaySetting === true && messageId) {
 | |
|       triggerPlayNextMessageIfNeeded(messageId);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   return (
 | |
|     <StyledH5AudioPlayer
 | |
|       src={urlToLoad}
 | |
|       preload="metadata"
 | |
|       style={{ pointerEvents: multiSelectMode ? 'none' : 'inherit' }}
 | |
|       layout="horizontal-reverse"
 | |
|       showSkipControls={false}
 | |
|       autoPlay={false}
 | |
|       autoPlayAfterSrcChange={false}
 | |
|       showJumpControls={false}
 | |
|       showDownloadProgress={false}
 | |
|       listenInterval={100}
 | |
|       onEnded={onEnded}
 | |
|       ref={player}
 | |
|       customControlsSection={[
 | |
|         RHAP_UI.MAIN_CONTROLS,
 | |
|         <StyledSpeedButton key="togglePlaybackSpeed">
 | |
|           <SessionButton
 | |
|             text={`${playbackSpeed}x`}
 | |
|             onClick={() => {
 | |
|               setPlaybackSpeed(playbackSpeed === 1 ? 1.5 : 1);
 | |
|             }}
 | |
|             buttonType={SessionButtonType.Simple}
 | |
|           />
 | |
|         </StyledSpeedButton>,
 | |
|       ]}
 | |
|       customIcons={{
 | |
|         play: <SessionIcon iconType="play" iconSize="small" />,
 | |
|         pause: <SessionIcon iconType="pause" iconSize="small" />,
 | |
|       }}
 | |
|       dropShadow={selected}
 | |
|     />
 | |
|   );
 | |
| };
 |