diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 52266265cf..1bf651a4c0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -777,6 +777,18 @@ "message": "Original message found, but not loaded. Scroll up to load it.", "description": "Shown in toast if user clicks on quote references messages not loaded in view, but in database" }, + "voiceRecordingInterruptedMax": { + "message": "Voice message recording stopped because the maximum time limit was reached.", + "description": "Confirmation dialog message for when the voice recording is interrupted due to max time limit" + }, + "voiceRecordingInterruptedBlur": { + "message": "Voice message recording stopped because you switched to another app.", + "description": "Confirmation dialog message for when the voice recording is interrupted due to app losing focus" + }, + "voiceNoteLimit": { + "message": "Voice messages are limited to five minutes. Recording will stop if you switch to another app.", + "description": "Shown in toast to warn user about limited time and that window must be in focus" + }, "voiceNoteMustBeOnlyAttachment": { "message": "A voice message must have only one attachment.", "description": "Shown in toast if tries to record a voice note with any staged attachments" @@ -863,6 +875,9 @@ "cancel": { "message": "Cancel" }, + "discard": { + "message": "Discard" + }, "failedToSend": { "message": "Failed to send to some recipients. Check your network connection." }, diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index c3b546d8a6..1a59a3b663 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -75,6 +75,11 @@ return { toastMessage: i18n('messageFoundButNotLoaded') }; }, }); + Whisper.VoiceNoteLimit = Whisper.ToastView.extend({ + render_attributes() { + return { toastMessage: i18n('voiceNoteLimit') }; + }, + }); Whisper.VoiceNoteMustBeOnlyAttachmentToast = Whisper.ToastView.extend({ render_attributes() { return { toastMessage: i18n('voiceNoteMustBeOnlyAttachment') }; @@ -1650,6 +1655,8 @@ return; } + this.showToast(Whisper.VoiceNoteLimit); + // Note - clicking anywhere will close the audio capture panel, due to // the onClick handler in InboxView, which calls its closeRecording method. @@ -1663,6 +1670,7 @@ const view = this.captureAudioView; view.render(); view.on('send', this.handleAudioCapture.bind(this)); + view.on('confirm', this.handleAudioConfirm.bind(this)); view.on('closed', this.endCaptureAudio.bind(this)); view.$el.appendTo(this.$('.capture-audio')); view.$('.finish').focus(); @@ -1671,6 +1679,19 @@ this.disableMessageField(); this.$('.microphone').hide(); }, + handleAudioConfirm(blob, lostFocus) { + const dialog = new Whisper.ConfirmationDialogView({ + cancelText: i18n('discard'), + message: lostFocus ? i18n('voiceRecordingInterruptedBlur') : i18n('voiceRecordingInterruptedMax'), + okText: i18n('sendAnyway'), + resolve: async () => { + await this.handleAudioCapture(blob); + }, + }); + + this.$el.prepend(dialog.el); + dialog.focusCancel(); + }, async handleAudioCapture(blob) { if (this.hasFiles()) { throw new Error('A voice note cannot be sent with other attachments'); diff --git a/js/views/recorder_view.js b/js/views/recorder_view.js index eb780281b3..9e756a76fc 100644 --- a/js/views/recorder_view.js +++ b/js/views/recorder_view.js @@ -29,6 +29,8 @@ close: 'close', }, onSwitchAway() { + this.lostFocus = true; + this.recorder.finishRecording(); this.close(); }, handleKeyDown(event) { @@ -89,11 +91,14 @@ handleBlob(recorder, blob) { if (blob && this.clickedFinish) { this.trigger('send', blob); + } else if (blob) { + this.trigger('confirm', blob, this.lostFocus); } else { this.close(); } }, start() { + this.lostFocus = false; this.clickedFinish = false; this.context = new AudioContext(); this.input = this.context.createGain(); @@ -103,6 +108,7 @@ }); this.recorder.onComplete = this.handleBlob.bind(this); this.recorder.onError = this.onError.bind(this); + this.recorder.onTimeout = this.onTimeout.bind(this); navigator.webkitGetUserMedia( { audio: true }, stream => { @@ -113,6 +119,10 @@ ); this.recorder.startRecording(); }, + onTimeout() { + this.recorder.finishRecording(); + this.close(); + }, onError(error) { // Protect against out-of-band errors, which can happen if the user revokes media // permissions after successfully accessing the microphone. diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index a8e04a1f14..7254bbb69c 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -793,7 +793,7 @@ "rule": "jQuery-$(", "path": "js/views/recorder_view.js", "line": " this.$('.time').text(`${minutes}:${seconds}`);", - "lineNumber": 49, + "lineNumber": 50, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -802,7 +802,7 @@ "rule": "jQuery-$(", "path": "js/views/recorder_view.js", "line": " $(window).off('blur', this.onSwitchAwayBound);", - "lineNumber": 80, + "lineNumber": 81, "reasonCategory": "usageTrusted", "updated": "2018-10-11T19:22:47.331Z", "reasonDetail": "Operating on already-existing DOM elements"