diff --git a/preload.js b/preload.js index bc3d4188c..8a3a040fa 100644 --- a/preload.js +++ b/preload.js @@ -60,13 +60,15 @@ window.isBeforeVersion = (toCheck, baseVersion) => { } }; +const DEFAULT_PUBLIC_CHAT_URL = appConfig.get('defaultPublicChatServer'); + window.CONSTANTS = { SECS_IN_DAY: 60 * 60 * 24, MAX_LOGIN_TRIES: 3, MAX_PASSWORD_LENGTH: 32, MAX_USERNAME_LENGTH: 20, MAX_GROUP_NAME_LENGTH: 64, - DEFAULT_PUBLIC_CHAT_URL: appConfig.get('defaultPublicChatServer'), + DEFAULT_PUBLIC_CHAT_URL, MAX_CONNECTION_DURATION: 5000, MAX_MESSAGE_BODY_LENGTH: 64 * 1024, // Limited due to the proof-of-work requirement @@ -79,7 +81,8 @@ window.CONSTANTS = { // at which more messages should be loaded MESSAGE_CONTAINER_BUFFER_OFFSET_PX: 30, MESSAGE_FETCH_INTERVAL: 1, - MAX_VOICE_MESSAGE_DURATION: 600, + // Maximum voice message duraiton of 5 minutes + MAX_VOICE_MESSAGE_DURATION: 300, }; window.versionInfo = { diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index f264053b8..756a3c592 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -52,6 +52,7 @@ $session-color-green-alt-2: #00fd73; $session-color-green-alt-3: #00f782; $session-shade-1: #0c0c0c; +$session-shade-1-alt: #0F1011; $session-shade-2: #161616; $session-shade-3: #191818; $session-shade-4: #1b1b1b; diff --git a/stylesheets/_session_conversation.scss b/stylesheets/_session_conversation.scss index d872d0af4..0d1815112 100644 --- a/stylesheets/_session_conversation.scss +++ b/stylesheets/_session_conversation.scss @@ -29,6 +29,18 @@ $composition-container-height: 60px; } } +@keyframes pulseLight { + 0% { + box-shadow: 0px 0px 0px 0px $session-color-danger-alt; + } + 50% { + box-shadow: 0px 0px 12px 0px rgba($session-color-danger-alt, 1); + } + 100% { + box-shadow: 0px 0px 0px 0px rgba($session-color-danger-alt, 1); + } +} + .conversation-item { display: flex; @@ -267,6 +279,7 @@ $composition-container-height: 60px; width: 100%; padding: 0px $session-margin-lg; } + } &--status { @@ -290,7 +303,10 @@ $composition-container-height: 60px; user-select: none; &:hover{ filter: brightness(100%); + border: 2px solid #161819; } + background-color: $session-shade-1-alt; + border: 2px solid #161819; } } } @@ -308,6 +324,8 @@ $composition-container-height: 60px; border-radius: 50%; background-color: $session-color-danger-alt; margin-left: $session-margin-sm; + + animation: pulseLight 4s infinite; } } } \ No newline at end of file diff --git a/ts/components/session/conversation/SessionRecording.tsx b/ts/components/session/conversation/SessionRecording.tsx index 70926bb96..10adbc8ef 100644 --- a/ts/components/session/conversation/SessionRecording.tsx +++ b/ts/components/session/conversation/SessionRecording.tsx @@ -13,12 +13,27 @@ interface Props { interface State { recordDuration: number; isRecording: boolean; + isPlaying: boolean; isPaused: boolean; + actionHover: boolean; mediaSetting?: boolean; + + // Steam information and data + mediaBlob?: any; + audioElement?: HTMLAudioElement; + streamParams?: { + stream: any; + media: any; + input: any; + processor: any; + } + volumeArray?: Array; + startTimestamp: number; nowTimestamp: number; + updateTimerInterval: NodeJS.Timeout; } @@ -40,6 +55,7 @@ export class SessionRecording extends React.Component { this.timerUpdate = this.timerUpdate.bind(this); this.onStream = this.onStream.bind(this); + this.stopStream = this.stopStream.bind(this); this.visualisationRef = React.createRef(); this.visualisationCanvas = React.createRef(); @@ -50,10 +66,15 @@ export class SessionRecording extends React.Component { this.state = { recordDuration: 0, isRecording: true, + isPlaying: false, isPaused: false, actionHover: false, mediaSetting: undefined, + mediaBlob: undefined, + audioElement: undefined, + streamParams: undefined, volumeArray: undefined, + startTimestamp: now, nowTimestamp: now, updateTimerInterval: updateTimerInterval, @@ -78,12 +99,8 @@ export class SessionRecording extends React.Component { const actionDefault = !actionPause && !actionPlay; const { isRecording, startTimestamp, nowTimestamp } = this.state; - const elapsedTime = nowTimestamp - startTimestamp; - const displayTimeString = moment(elapsedTime).format('mm:ss'); - - console.log(`[vince][time] Elapsed time: `, this.state.nowTimestamp - this.state.startTimestamp); - console.log(`[vince][time] Elapsed time calculated: `, elapsedTime); - + const elapsedTimeMs = 1000 * (nowTimestamp - startTimestamp); + const displayTimeString = moment.utc(elapsedTimeMs).format('m:ss'); return (
@@ -98,7 +115,7 @@ export class SessionRecording extends React.Component { iconSize={SessionIconSize.Medium} // FIXME VINCE: Globalise constants for JS Session Colors iconColor={'#FF4538'} - onClick={this.stopRecording} + onClick={this.stopStream} /> )} {actionPlay && ( @@ -166,12 +183,6 @@ export class SessionRecording extends React.Component {
); } - - public blobToFile (data: any, fileName:string) { - const file = new File([data.blob], fileName); - console.log(`[vince][mic] File: `, file); - return file; - } private handleHoverActions() { if ((this.state.isRecording) && !this.state.actionHover) { @@ -182,6 +193,15 @@ export class SessionRecording extends React.Component { } private timerUpdate(){ + const { nowTimestamp, startTimestamp } = this.state; + const elapsedTime = nowTimestamp - startTimestamp; + + if (elapsedTime >= window.CONSTANTS.MAX_VOICE_MESSAGE_DURATION){ + clearInterval(this.state.updateTimerInterval); + this.stopStream(); + return; + } + this.setState({ nowTimestamp: moment().unix() }); @@ -195,9 +215,7 @@ export class SessionRecording extends React.Component { } } - private stopRecording() { - console.log(`[vince][mic] Stopped recording`); - + private async stopRecording() { this.setState({ isRecording: false, isPaused: true, @@ -205,23 +223,47 @@ export class SessionRecording extends React.Component { } private playRecording() { - console.log(`[vince][mic] Playing recording`); + const { mediaBlob, streamParams } = this.state; - this.setState({ - isRecording: false, - isPaused: false, + if (!mediaBlob){ + window.pushToast({ + title: 'There was an error playing your voice recording.', + }); + return; + } + + this.setState({ + isRecording: false, + isPaused: false, + isPlaying: true, }); + + // Start playing recording + const audioURL = window.URL.createObjectURL(mediaBlob.data); + const audioElement = new Audio(audioURL); + this.setState({audioElement}); + audioElement.play(); + + + console.log(`[vince][stream] Audio: `, audioURL); + console.log(`[vince][stream] AudioURL: `, audioURL); + } private initSendVoiceRecording(){ + // Is the audio file < 10mb? That's the attachment filesize limit + + return; } private onDeleteVoiceMessage() { - //this.stopRecording(); + this.stopStream(); + this.setState({ isRecording: false, isPaused: true, + isPlaying: false, }, () => this.props.onStoppedRecording()); } @@ -231,15 +273,48 @@ export class SessionRecording extends React.Component { private async initiateStream() { navigator.getUserMedia({audio:true}, this.onStream, this.onStreamError); + } + + private stopStream() { + const {streamParams} = this.state; - //const mediaStreamSource = audioContext.createMediaStreamSource(stream); - //const meter = getMeter(audioContext); - //mediaStreamSource.connect(meter); + // Exit if parameters aren't yet set + if (!streamParams){ + return; + } + + // Stop the stream + streamParams.media.stop(); + streamParams.input.disconnect(); + streamParams.processor.disconnect(); + streamParams.stream.getTracks().forEach((track: any) => track.stop); + + console.log(`[vince][stream] Stream: `, streamParams.stream); + console.log(`[vince][stream] Media: `, streamParams.media); + console.log(`[vince][stream] Input: `, streamParams.input); + console.log(`[vince][stream] Processor: `, streamParams.processor); + + + // Stop recording + this.stopRecording(); } private onStream(stream: any) { + // If not recording, stop stream + if (!this.state.isRecording) { + this.stopStream(); + return; + } + + // Start recording the stream + const media = new window.MediaRecorder(stream); + media.ondataavailable = (mediaBlob: any) => { + this.setState({mediaBlob}); + }; + media.start(); + - // AUDIO CONTEXT + // Audio Context const audioContext = new window.AudioContext(); const input = audioContext.createMediaStreamSource(stream); @@ -251,6 +326,9 @@ export class SessionRecording extends React.Component { const processor = audioContext.createScriptProcessor(bufferSize, 1, 1); processor.onaudioprocess = () => { + const streamParams = {stream, media, input, processor}; + this.setState({streamParams}); + // Array of volumes by frequency (not in Hz, arbitrary unit) const freqTypedArray = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(freqTypedArray); @@ -263,7 +341,7 @@ export class SessionRecording extends React.Component { // CANVAS CONTEXT - const drawCanvas = () => { + const drawRecordingCanvas = () => { const canvas = this.visualisationCanvas.current; const CANVAS_HEIGHT = 35; const CANVAS_WIDTH = VISUALISATION_WIDTH || 600; @@ -289,9 +367,8 @@ export class SessionRecording extends React.Component { const frontLoad = volumeArray.slice(0, frontLoadLen - 1).reverse().map(n => n * 0.80); volumeArray = [...frontLoad, ...volumeArray]; - // Chop off values which exceed the bouinds of the container + // Chop off values which exceed the bounds of the container volumeArray = volumeArray.slice(0, numBars); - console.log(`[vince][mic] Width: `, VISUALISATION_WIDTH); canvas && (canvas.height = CANVAS_HEIGHT); canvas && (canvas.width = CANVAS_WIDTH); @@ -316,24 +393,20 @@ export class SessionRecording extends React.Component { } } - requestAnimationFrame(drawCanvas); + const drawPlayingCanvas = () => { + return; + } + this.state.isRecording && requestAnimationFrame(drawRecordingCanvas); + this.state.isPlaying && requestAnimationFrame(drawPlayingCanvas); } - - // Get volume for visualisation + // Init listeners for visualisation visualisation input.connect(analyser); processor.connect(audioContext.destination); - - console.log(`[vince][mic] Freq:`, analyser.frequencyBinCount); - - //Start recording the stream - const media = new window.MediaRecorder(stream); - } - private onStreamError(error: any) { return error; }