Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add voicemail feature #232

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6310ee1
idk what im doing
C0ffeeCode Jan 2, 2022
0f07692
idk what im doing vol 2
C0ffeeCode Jan 2, 2022
22801c1
needs ui
C0ffeeCode Jan 2, 2022
c2cda97
cleanup + VoiceMailRecorder
C0ffeeCode Jan 3, 2022
240d164
Added type checks on attachment / attachmentOrUi
C0ffeeCode Jan 3, 2022
9bb8d0a
- clear old voicerecorder
C0ffeeCode Jan 3, 2022
949468d
extract AttachmentFrame out of RoomViewInput
C0ffeeCode Jan 3, 2022
5e97ce6
works or something like that i guess
C0ffeeCode Jan 4, 2022
0bebb60
works or something like that disk 2
C0ffeeCode Jan 4, 2022
da09ab1
I call them needles 🪡
C0ffeeCode Jan 5, 2022
62b7074
better !?
C0ffeeCode Jan 5, 2022
2f94a5c
one bug left 🪲
C0ffeeCode Jan 5, 2022
750ef7f
No more bugs i guess, time to implement the timer (yay)
C0ffeeCode Jan 5, 2022
ed5a777
timer not fully working
C0ffeeCode Jan 5, 2022
596e7d1
fix timer
C0ffeeCode Jan 6, 2022
8a441f6
fixed shit
C0ffeeCode Jan 6, 2022
e3c9f43
Merge branch 'ajbura:dev' into voicemail
C0ffeeCode Jan 6, 2022
75a82aa
- removed old code
C0ffeeCode Jan 6, 2022
838364b
now checking for browser support and permission
C0ffeeCode Jan 6, 2022
8cd2372
fixed bug: If recording is paused before restarting it will cause the…
C0ffeeCode Jan 6, 2022
c9d2c33
Improve code quality
C0ffeeCode Jan 6, 2022
45fe3fc
Merge branch 'voicemail' of https://github.com/C0ffeeCode/cinny into …
C0ffeeCode Jan 6, 2022
a326b00
cleanup
C0ffeeCode Jan 6, 2022
1e53be7
- made code more extendable
C0ffeeCode Jan 6, 2022
985d468
Add comments
C0ffeeCode Jan 6, 2022
c22e9b0
Merge branch 'dev' into voicemail
C0ffeeCode Jan 11, 2022
6dc7cf9
Merge branch 'dev' into voicemail
C0ffeeCode Jan 14, 2022
9739eb6
fix stupid merge issue, sorry
C0ffeeCode Jan 14, 2022
eb97a85
Undo dependency changes and fixes to webpack
C0ffeeCode Jan 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/app/organisms/room/AttachmentUis/AttachmentFrame.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-disable react/prop-types */
import React from 'react';
import PropTypes from 'prop-types';
import FileAttachedIndicator from './FileAttachedIndicator';
import attachmentUis from './attachmentUis';

function AttachmentFrame({
attachmentOrUi,
fileSetter,
uploadProgressRef,
}) {
// To enable child components to learn how to attach their result
let submission;
const fnHowToSubmit = (func) => {
submission = func;
fileSetter(submission);
};

// If there already is an attachment, show it
if (typeof attachmentOrUi === 'object') {
return (
<FileAttachedIndicator
attachmentOrUi={attachmentOrUi}
uploadProgressRef={uploadProgressRef}
/>
);
}

// Show the desired UI
const UiComponent = attachmentUis.get(attachmentOrUi).component;
return (<UiComponent fnHowToSubmit={fnHowToSubmit} />);
}

AttachmentFrame.propTypes = {
attachmentOrUi: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
fileSetter: PropTypes.func.isRequired,
uploadProgressRef: PropTypes.shape().isRequired,
};

export default AttachmentFrame;
71 changes: 71 additions & 0 deletions src/app/organisms/room/AttachmentUis/AttachmentTypeSelector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import PropTypes from 'prop-types';
import IconButton from '../../../atoms/button/IconButton';
import CirclePlusIC from '../../../../../public/res/ic/outlined/circle-plus.svg';
import PlusIC from '../../../../../public/res/ic/outlined/plus.svg';
import ContextMenu, { MenuHeader, MenuItem } from '../../../atoms/context-menu/ContextMenu';
import attachmentUiFrameTypes from './attachmentUis';

function AttachmentTypeSelector({ alreadyHasAttachment, actOnAttaching }) {
function getList(toggleMenu) {
const list = [];

attachmentUiFrameTypes.forEach((obj, key) => {
// Entries have to have an icon
const icon = obj.icon ?? PlusIC;

list.push(
<MenuItem
// This does not matter
// eslint-disable-next-line react/no-array-index-key
key={`attachmentUiListItem-${key}`}
onClick={() => {
toggleMenu();
actOnAttaching(key);
}}
iconSrc={icon}
>
{obj.fullName}
</MenuItem>,
);
});

return list;
}

return (
<ContextMenu
maxWidth={200}
content={(toggleMenu) => (
<div>
<MenuHeader>Attachment</MenuHeader>
{getList(toggleMenu)}
</div>
)}
render={(toggleMenu) => (
<IconButton
onClick={() => {
if (!alreadyHasAttachment) {
toggleMenu();
} else {
actOnAttaching(attachmentUiFrameTypes.none);
}
}}
tooltip={alreadyHasAttachment ? 'Cancel' : 'Select attachment'}
src={CirclePlusIC}
/>
)}
/>
);
}

AttachmentTypeSelector.propTypes = {
alreadyHasAttachment: PropTypes.bool,
actOnAttaching: PropTypes.func.isRequired,
};

AttachmentTypeSelector.defaultProps = {
alreadyHasAttachment: false,
};

export default AttachmentTypeSelector;
38 changes: 38 additions & 0 deletions src/app/organisms/room/AttachmentUis/FileAttachedIndicator.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
import RawIcon from '../../../atoms/system-icons/RawIcon';
import VLCIC from '../../../../../public/res/ic/outlined/vlc.svg';
import VolumeFullIC from '../../../../../public/res/ic/outlined/volume-full.svg';
import FileIC from '../../../../../public/res/ic/outlined/file.svg';
import Text from '../../../atoms/text/Text';
import { bytesToSize } from '../../../../util/common';

function FileAttachedIndicator({
attachmentOrUi,
uploadProgressRef,
}) {
if (typeof attachmentOrUi !== 'object') return null;

const fileType = attachmentOrUi.type.slice(0, attachmentOrUi.type.indexOf('/'));
return (
<div className="room-attachment">
<div className={`room-attachment__preview${fileType !== 'image' ? ' room-attachment__icon' : ''}`}>
{fileType === 'image' && <img alt={attachmentOrUi.name} src={URL.createObjectURL(attachmentOrUi)} />}
{fileType === 'video' && <RawIcon src={VLCIC} />}
{fileType === 'audio' && <RawIcon src={VolumeFullIC} />}
{fileType !== 'image' && fileType !== 'video' && fileType !== 'audio' && <RawIcon src={FileIC} />}
</div>
<div className="room-attachment__info">
<Text variant="b1">{attachmentOrUi.name}</Text>
<Text variant="b3"><span ref={uploadProgressRef}>{`size: ${bytesToSize(attachmentOrUi.size)}`}</span></Text>
</div>
</div>
);
}

FileAttachedIndicator.propTypes = {
attachmentOrUi: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
uploadProgressRef: PropTypes.shape().isRequired,
};

export default FileAttachedIndicator;
169 changes: 169 additions & 0 deletions src/app/organisms/room/AttachmentUis/VoiceMailRecorder.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import Text from '../../../atoms/text/Text';
import RawIcon from '../../../atoms/system-icons/RawIcon';
import VolumeFullIC from '../../../../../public/res/ic/outlined/volume-full.svg';
import ChevronBottomIC from '../../../../../public/res/ic/outlined/chevron-bottom.svg';
import ArrowIC from '../../../../../public/res/ic/outlined/leave-arrow.svg';
import PauseIC from '../../../../../public/res/ic/outlined/pause.svg';
import PlayIC from '../../../../../public/res/ic/outlined/play.svg';
import IconButton from '../../../atoms/button/IconButton';
import './VoiceMailRecorder.scss';
import Timer from '../../../../util/Timer';

/**
* @type {Timer}
*/
let timer;

/**
* @type {MediaStream}
*/
let _stream;
/**
* @type {MediaRecorder}
*/
let _mediaRecorder;

async function init() {
if (_mediaRecorder) return;

timer = new Timer();
_stream = null;
_mediaRecorder = null;

_stream = await navigator.mediaDevices.getUserMedia({ audio: true });

_mediaRecorder = new MediaRecorder(_stream);

_mediaRecorder.onerror = (error) => {
console.log(error);
_mediaRecorder.stop();
};
}

function pauseRec() {
if (_mediaRecorder.state === 'recording') {
_mediaRecorder.pause();
timer.pause();
}
}

function startOrResumeRec() {
if (!_mediaRecorder) return;

if (_mediaRecorder.state === 'paused') {
_mediaRecorder.resume();
} else if (_mediaRecorder.state === 'inactive') {
_mediaRecorder.start();
}
timer.resume();
}

async function restartRec() {
// Needed, otherwise the browser indicator would remain after closing UI
if (_mediaRecorder.state !== 'inactive') _mediaRecorder.stop();
_stream.getTracks().forEach((track) => track.stop());
_mediaRecorder = null;
timer = new Timer();
await init();
startOrResumeRec();
}

function VoiceMailRecorder({ fnHowToSubmit }) {
const [state, setState] = React.useState('Recording');
const [timeRecording, setTimeRecording] = React.useState('00:00');
const [browserHasNoSupport, setBrowserHasNoSupport] = React.useState(!navigator.mediaDevices
? 'It seems like your browser is unsupported' : null);

async function initiateInitiation() {
if (!_mediaRecorder) {
await init().catch((err) => {
console.warn('Recording is disallowed', err);
setBrowserHasNoSupport('It seems like you have disallowed Cinny to record your voice ༼ つ ◕_◕ ༽つ');
});

if (browserHasNoSupport) return;
startOrResumeRec();
}

_mediaRecorder.onstart = () => setState('Recording...');
_mediaRecorder.onpause = () => setState('Recording paused');
_mediaRecorder.onresume = () => setState('Recording...');
}

function stopAndSubmit(skipSubmission = false) {
if (!skipSubmission) {
_mediaRecorder.ondataavailable = (event) => {
const audioChunks = [];
audioChunks.push(event.data);
_mediaRecorder = null;
_stream = null;

const opts = { type: 'audio/webm' };
const audioBlob = new Blob(audioChunks, opts);

const audioFile = new File([audioBlob], 'voicemail.webm', opts);
fnHowToSubmit(audioFile);
};
}

// Stop recording, remove browser indicator
if (_mediaRecorder && _mediaRecorder.state !== 'inactive') _mediaRecorder.stop();
_stream.getTracks().forEach((track) => track.stop());
}

useEffect(() => {
const timerUpdater = setInterval(() => {
setTimeRecording(timer.getTimeStr);
}, 500); // .5 seconds

// Cleanup after components unmount
return () => {
clearInterval(timerUpdater);
if (_mediaRecorder) {
_mediaRecorder = null;
}
if (_stream) {
// To remove the browser's recording indicator
_stream.getTracks().forEach((track) => track.stop());
_stream = null;
}
};
}, []);

initiateInitiation();

const ui = (
<div className="room-attachment">
<div className="room-attachment__preview">
<RawIcon src={VolumeFullIC} />
</div>
<div className="room-attachment__info room-attachment-ui-recorder">
<div>
<Text variant="b1">
{state}
</Text>
<Text variant="b3">{`for ${timeRecording}`}</Text>
</div>
{(_mediaRecorder && _mediaRecorder.state === 'recording')
? (<IconButton onClick={pauseRec} src={PauseIC}>Pause</IconButton>)
: (<IconButton onClick={startOrResumeRec} src={PlayIC}>Start</IconButton>)}
<IconButton onClick={() => restartRec().then(() => setState('Recording...'))} src={ArrowIC} tooltip="Start over">
Reset
</IconButton>
<IconButton onClick={() => stopAndSubmit()} src={ChevronBottomIC} tooltip="Add as attachment" type="submit">Submit</IconButton>
</div>
</div>
);

return browserHasNoSupport
? <Text variant="b1" className="room-attachment unsupported-info">{browserHasNoSupport}</Text>
: ui;
}

VoiceMailRecorder.propTypes = {
fnHowToSubmit: PropTypes.func.isRequired,
};

export default VoiceMailRecorder;
13 changes: 13 additions & 0 deletions src/app/organisms/room/AttachmentUis/VoiceMailRecorder.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

.room-attachment-ui-recorder {
display: flex;

div {
width: 150px;
max-width: 100%;
}
}

.unsupported-info {
height: 2em;
}
27 changes: 27 additions & 0 deletions src/app/organisms/room/AttachmentUis/attachmentUis.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import FileIC from '../../../../../public/res/ic/outlined/file.svg';
import VoiceMailRecorder from './VoiceMailRecorder';

/**
* @typedef {Object} AttachmentUi
* @property {string} fullName How should it be listed as to the user?
* @property {any} icon The icon to use for the attachment type.
* @property {React.ComponentType<{fnHowToSubmit: Function}>} component
* The component for the attachment type
*/

/**
* @type {Map<string, AttachmentUi>} attachmentUis
*/
const attachmentUis = new Map();

// Populate attachmentUis
attachmentUis.set('file', {
fullName: 'File',
icon: FileIC,
});
attachmentUis.set('voiceMailRecorder', {
fullName: 'Voice mail',
component: VoiceMailRecorder,
});

export default attachmentUis;
Loading
Loading