Learn how to easily add voice note sharing to your web app with this step-by-step guide. Enhance user interaction today!

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Why Voice Notes Matter in Modern Web Apps
Voice notes have transformed from a nice-to-have feature into a core expectation for many users. They bridge the expressiveness gap that text alone can't fill, making your application feel more personal and engaging. In our increasingly mobile-first world, the ability to quickly record and share thoughts is becoming essential across sectors from team collaboration to social platforms.
The Technical Architecture You'll Need
Let's break down how to implement this feature from scratch.
Step 1: Implementing the Audio Recording Interface
First, we'll need to set up the browser's MediaRecorder API to capture audio from the user's microphone:
class VoiceRecorder {
constructor() {
this.mediaRecorder = null;
this.audioChunks = [];
this.isRecording = false;
this.stream = null;
}
async startRecording() {
try {
// Request microphone access from the user
this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.mediaRecorder = new MediaRecorder(this.stream);
this.audioChunks = [];
this.isRecording = true;
// Event handler for when audio data becomes available
this.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
this.audioChunks.push(event.data);
}
};
this.mediaRecorder.start();
return true;
} catch (error) {
console.error("Error accessing microphone:", error);
return false;
}
}
stopRecording() {
return new Promise((resolve) => {
if (!this.mediaRecorder) {
resolve(null);
return;
}
this.mediaRecorder.onstop = () => {
// Create a Blob from the recorded audio chunks
const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });
this.isRecording = false;
// Stop all tracks in the stream to release the microphone
this.stream.getTracks().forEach(track => track.stop());
resolve(audioBlob);
};
this.mediaRecorder.stop();
});
}
}
Step 2: Creating a User-Friendly Recording Component
Now let's build a React component to provide a clean interface for recording:
import React, { useState, useRef } from 'react';
import { VoiceRecorder } from './VoiceRecorder';
const VoiceNoteRecorder = ({ onSave }) => {
const [isRecording, setIsRecording] = useState(false);
const [audioURL, setAudioURL] = useState(null);
const [duration, setDuration] = useState(0);
const [error, setError] = useState(null);
const recorderRef = useRef(new VoiceRecorder());
const timerRef = useRef(null);
const startRecording = async () => {
setError(null);
const success = await recorderRef.current.startRecording();
if (success) {
setIsRecording(true);
setAudioURL(null);
// Start a timer to track recording duration
let seconds = 0;
timerRef.current = setInterval(() => {
seconds += 1;
setDuration(seconds);
}, 1000);
} else {
setError("Couldn't access microphone. Please check permissions.");
}
};
const stopRecording = async () => {
clearInterval(timerRef.current);
setIsRecording(false);
const audioBlob = await recorderRef.current.stopRecording();
if (audioBlob) {
const url = URL.createObjectURL(audioBlob);
setAudioURL(url);
}
};
const saveRecording = () => {
if (audioURL) {
onSave({
blob: recorderRef.current.audioChunks,
url: audioURL,
duration: duration
});
}
};
const discardRecording = () => {
setAudioURL(null);
setDuration(0);
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
};
return (
<div className="voice-recorder">
{error && <div className="error-message">{error}</div>}
<div className="recording-controls">
{!isRecording && !audioURL && (
<button onClick={startRecording} className="record-button">
Start Recording
</button>
)}
{isRecording && (
<>
<div className="recording-indicator">
Recording... {formatTime(duration)}
</div>
<button onClick={stopRecording} className="stop-button">
Stop
</button>
</>
)}
{audioURL && (
<div className="playback-controls">
<audio src={audioURL} controls />
<div className="action-buttons">
<button onClick={saveRecording} className="save-button">
Save
</button>
<button onClick={discardRecording} className="discard-button">
Discard
</button>
</div>
</div>
)}
</div>
</div>
);
};
export default VoiceNoteRecorder;
Step 3: Setting Up API Endpoints for Voice Note Upload
Now we need an endpoint to handle the voice note uploads. Here's how it might look with Express.js:
const express = require('express');
const multer = require('multer');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const router = express.Router();
// Configure storage for voice notes
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/voice-notes/');
},
filename: (req, file, cb) => {
// Generate unique filename with original extension
const uniqueId = uuidv4();
const extension = path.extname(file.originalname) || '.webm';
cb(null, `${uniqueId}${extension}`);
}
});
// Set up file filter to only accept audio files
const fileFilter = (req, file, cb) => {
const allowedMimeTypes = ['audio/webm', 'audio/mp4', 'audio/mpeg', 'audio/ogg'];
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only audio files are allowed.'), false);
}
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 1024 * 1024 * 10, // 10MB max file size
}
});
// Endpoint to upload a voice note
router.post('/upload', upload.single('voiceNote'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No audio file provided' });
}
// Create database entry for the voice note
const voiceNote = await VoiceNote.create({
userId: req.user.id, // Assuming you have authentication middleware
fileName: req.file.filename,
originalName: req.file.originalname,
mimeType: req.file.mimetype,
size: req.file.size,
duration: req.body.duration || 0,
path: req.file.path,
createdAt: new Date()
});
return res.status(201).json({
success: true,
voiceNoteId: voiceNote.id,
url: `/api/voice-notes/${voiceNote.id}/stream`
});
} catch (error) {
console.error('Error uploading voice note:', error);
return res.status(500).json({ error: 'Failed to upload voice note' });
}
});
// Endpoint to stream a voice note
router.get('/:id/stream', async (req, res) => {
try {
const voiceNote = await VoiceNote.findById(req.params.id);
if (!voiceNote) {
return res.status(404).json({ error: 'Voice note not found' });
}
// Check access permissions
if (!canAccessVoiceNote(req.user, voiceNote)) {
return res.status(403).json({ error: 'Access denied' });
}
// Set appropriate headers
res.set({
'Content-Type': voiceNote.mimeType,
'Content-Length': voiceNote.size,
'Accept-Ranges': 'bytes'
});
// Stream the file
const fileStream = fs.createReadStream(voiceNote.path);
fileStream.pipe(res);
} catch (error) {
console.error('Error streaming voice note:', error);
return res.status(500).json({ error: 'Failed to stream voice note' });
}
});
module.exports = router;
Step 4: Setting Up the Database Schema
Using MongoDB with Mongoose as an example:
const mongoose = require('mongoose');
const voiceNoteSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
fileName: {
type: String,
required: true
},
originalName: String,
mimeType: {
type: String,
required: true
},
size: {
type: Number,
required: true
},
duration: {
type: Number,
default: 0
},
path: {
type: String,
required: true
},
transcription: {
text: String,
status: {
type: String,
enum: ['pending', 'completed', 'failed'],
default: 'pending'
}
},
isShared: {
type: Boolean,
default: false
},
shareableLink: String,
viewCount: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
}
});
// Create index for faster querying
voiceNoteSchema.index({ userId: 1, createdAt: -1 });
// Method to generate a shareable link
voiceNoteSchema.methods.generateShareableLink = function() {
this.shareableLink = `${process.env.APP_URL}/share/voice/${this._id}/${generateRandomString(12)}`;
this.isShared = true;
return this.save();
};
module.exports = mongoose.model('VoiceNote', voiceNoteSchema);
Step 5: Adding Sharing Functionality
Let's build the sharing API endpoints:
// Generate a shareable link for a voice note
router.post('/:id/share', async (req, res) => {
try {
const voiceNote = await VoiceNote.findById(req.params.id);
if (!voiceNote) {
return res.status(404).json({ error: 'Voice note not found' });
}
// Verify ownership or sharing permissions
if (voiceNote.userId.toString() !== req.user.id) {
return res.status(403).json({ error: 'You do not have permission to share this voice note' });
}
// Generate shareable link if it doesn't exist
if (!voiceNote.shareableLink) {
await voiceNote.generateShareableLink();
}
return res.json({
success: true,
shareableLink: voiceNote.shareableLink
});
} catch (error) {
console.error('Error sharing voice note:', error);
return res.status(500).json({ error: 'Failed to share voice note' });
}
});
// Public endpoint to access a shared voice note
router.get('/share/:id/:token', async (req, res) => {
try {
const voiceNote = await VoiceNote.findById(req.params.id);
if (!voiceNote || !voiceNote.isShared) {
return res.status(404).json({ error: 'Voice note not found or not shared' });
}
// Extract token from the shareableLink to verify access
const linkToken = voiceNote.shareableLink.split('/').pop();
if (linkToken !== req.params.token) {
return res.status(403).json({ error: 'Invalid share token' });
}
// Increment view count
voiceNote.viewCount += 1;
await voiceNote.save();
// Return metadata for the player
return res.json({
id: voiceNote._id,
duration: voiceNote.duration,
streamUrl: `/api/voice-notes/${voiceNote._id}/stream/shared/${req.params.token}`,
mimeType: voiceNote.mimeType
});
} catch (error) {
console.error('Error accessing shared voice note:', error);
return res.status(500).json({ error: 'Failed to access shared voice note' });
}
});
// Public stream endpoint for shared voice notes
router.get('/:id/stream/shared/:token', async (req, res) => {
try {
const voiceNote = await VoiceNote.findById(req.params.id);
if (!voiceNote || !voiceNote.isShared) {
return res.status(404).json({ error: 'Voice note not found or not shared' });
}
// Verify token
const linkToken = voiceNote.shareableLink.split('/').pop();
if (linkToken !== req.params.token) {
return res.status(403).json({ error: 'Invalid share token' });
}
// Set appropriate headers for streaming
res.set({
'Content-Type': voiceNote.mimeType,
'Content-Length': voiceNote.size,
'Accept-Ranges': 'bytes'
});
// Stream the file
const fileStream = fs.createReadStream(voiceNote.path);
fileStream.pipe(res);
} catch (error) {
console.error('Error streaming shared voice note:', error);
return res.status(500).json({ error: 'Failed to stream shared voice note' });
}
});
Step 6: Building the Sharing Interface
Let's create a React component for sharing voice notes:
import React, { useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
const VoiceNoteSharing = ({ voiceNoteId, isOwner }) => {
const [shareableLink, setShareableLink] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [copied, setCopied] = useState(false);
const [error, setError] = useState(null);
const generateShareLink = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(`/api/voice-notes/${voiceNoteId}/share`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to generate sharing link');
}
setShareableLink(data.shareableLink);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
const handleCopy = () => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const shareToSocialMedia = (platform) => {
if (!shareableLink) return;
let url;
switch (platform) {
case 'twitter':
url = `https://twitter.com/intent/tweet?url=${encodeURIComponent(shareableLink)}&text=Check out this voice note!`;
break;
case 'facebook':
url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareableLink)}`;
break;
case 'whatsapp':
url = `https://wa.me/?text=${encodeURIComponent('Check out this voice note! ' + shareableLink)}`;
break;
default:
return;
}
window.open(url, '_blank');
};
if (!isOwner) {
return null; // Don't show sharing options for non-owners
}
return (
<div className="voice-note-sharing">
{error && <div className="error-message">{error}</div>}
{!shareableLink ? (
<button
onClick={generateShareLink}
disabled={isLoading}
className="share-button"
>
{isLoading ? 'Generating Link...' : 'Share Voice Note'}
</button>
) : (
<div className="sharing-options">
<div className="share-link-container">
<input
type="text"
value={shareableLink}
readOnly
className="share-link-input"
/>
<CopyToClipboard text={shareableLink} onCopy={handleCopy}>
<button className="copy-button">
{copied ? 'Copied!' : 'Copy Link'}
</button>
</CopyToClipboard>
</div>
<div className="social-sharing">
<button onClick={() => shareToSocialMedia('twitter')} className="twitter-share">
Share on Twitter
</button>
<button onClick={() => shareToSocialMedia('facebook')} className="facebook-share">
Share on Facebook
</button>
<button onClick={() => shareToSocialMedia('whatsapp')} className="whatsapp-share">
Share on WhatsApp
</button>
</div>
</div>
)}
</div>
);
};
export default VoiceNoteSharing;
Step 7: Creating a Public Voice Note Player
Now we need a component for playing shared voice notes:
import React, { useState, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
const SharedVoiceNote = () => {
const { id, token } = useParams();
const [voiceNote, setVoiceNote] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const audioRef = useRef(null);
useEffect(() => {
const fetchVoiceNote = async () => {
try {
const response = await fetch(`/api/voice-notes/share/${id}/${token}`);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to load voice note');
}
setVoiceNote(data);
setDuration(data.duration);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchVoiceNote();
}, [id, token]);
const togglePlayPause = () => {
if (audioRef.current) {
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
const handleTimeUpdate = () => {
if (audioRef.current) {
setCurrentTime(audioRef.current.currentTime);
}
};
const handleLoadedMetadata = () => {
if (audioRef.current) {
setDuration(audioRef.current.duration);
}
};
const handleEnded = () => {
setIsPlaying(false);
setCurrentTime(0);
};
const handleSliderChange = (e) => {
const newTime = parseFloat(e.target.value);
setCurrentTime(newTime);
if (audioRef.current) {
audioRef.current.currentTime = newTime;
}
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
};
if (isLoading) {
return <div className="loading">Loading voice note...</div>;
}
if (error) {
return <div className="error-message">{error}</div>;
}
if (!voiceNote) {
return <div className="not-found">Voice note not found</div>;
}
return (
<div className="shared-voice-note-player">
<h3>Shared Voice Note</h3>
<audio
ref={audioRef}
src={voiceNote.streamUrl}
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onEnded={handleEnded}
style={{ display: 'none' }}
/>
<div className="player-controls">
<button
onClick={togglePlayPause}
className={`play-pause-button ${isPlaying ? 'playing' : ''}`}
>
{isPlaying ? 'Pause' : 'Play'}
</button>
<div className="time-display">
{formatTime(currentTime)} / {formatTime(duration)}
</div>
<input
type="range"
min="0"
max={duration || 0}
step="0.01"
value={currentTime}
onChange={handleSliderChange}
className="time-slider"
/>
</div>
<div className="voice-note-info">
<p>Views: {voiceNote.viewCount || 0}</p>
</div>
</div>
);
};
export default SharedVoiceNote;
Step 8: Adding Voice Note Transcription
Let's integrate with a transcription service (using OpenAI's Whisper API as an example):
const { Configuration, OpenAIApi } = require('openai');
const fs = require('fs');
const path = require('path');
// Initialize OpenAI client
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);
// Add a queue for transcription jobs
const transcriptionQueue = require('bull')('voice-note-transcription', {
redis: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
}
});
// API endpoint to request transcription
router.post('/:id/transcribe', async (req, res) => {
try {
const voiceNote = await VoiceNote.findById(req.params.id);
if (!voiceNote) {
return res.status(404).json({ error: 'Voice note not found' });
}
// Verify ownership
if (voiceNote.userId.toString() !== req.user.id) {
return res.status(403).json({ error: 'You do not have permission to transcribe this voice note' });
}
// Check if transcription is already in progress or completed
if (voiceNote.transcription.status === 'completed') {
return res.json({
success: true,
transcription: voiceNote.transcription.text,
status: 'completed'
});
}
if (voiceNote.transcription.status === 'pending') {
return res.json({
success: true,
status: 'pending',
message: 'Transcription is already in progress'
});
}
// Update status to pending
voiceNote.transcription.status = 'pending';
await voiceNote.save();
// Add to transcription queue
await transcriptionQueue.add({
voiceNoteId: voiceNote._id,
filePath: voiceNote.path
});
return res.json({
success: true,
status: 'pending',
message: 'Transcription request added to queue'
});
} catch (error) {
console.error('Error requesting transcription:', error);
return res.status(500).json({ error: 'Failed to request transcription' });
}
});
// Process transcription queue
transcriptionQueue.process(async (job) => {
const { voiceNoteId, filePath } = job.data;
try {
const voiceNote = await VoiceNote.findById(voiceNoteId);
if (!voiceNote) {
throw new Error('Voice note not found');
}
// Convert audio to format supported by OpenAI if needed
const audioFilePath = await convertAudioIfNeeded(filePath);
// Perform transcription with Whisper API
const response = await openai.createTranscription(
fs.createReadStream(audioFilePath),
"whisper-1" // OpenAI's Whisper model
);
// Update voice note with transcription
voiceNote.transcription.text = response.data.text;
voiceNote.transcription.status = 'completed';
await voiceNote.save();
return { success: true, voiceNoteId };
} catch (error) {
console.error('Transcription failed:', error);
// Update voice note with failure status
const voiceNote = await VoiceNote.findById(voiceNoteId);
if (voiceNote) {
voiceNote.transcription.status = 'failed';
await voiceNote.save();
}
throw error;
}
});
// Helper function to convert audio to format supported by Whisper
async function convertAudioIfNeeded(filePath) {
const extension = path.extname(filePath).toLowerCase();
// If already in a supported format, return the original path
if (['.mp3', '.mp4', '.mpeg', '.mpga', '.m4a', '.wav', '.webm'].includes(extension)) {
return filePath;
}
// Convert to MP3 using ffmpeg
const outputPath = filePath.replace(extension, '.mp3');
return new Promise((resolve, reject) => {
const ffmpeg = require('fluent-ffmpeg');
ffmpeg(filePath)
.output(outputPath)
.audioCodec('libmp3lame')
.on('end', () => resolve(outputPath))
.on('error', (err) => reject(err))
.run();
});
}
Step 9: Creating a Transcription UI Component
Let's add a component to display and interact with transcriptions:
import React, { useState, useEffect } from 'react';
const VoiceNoteTranscription = ({ voiceNoteId, isOwner }) => {
const [transcription, setTranscription] = useState(null);
const [status, setStatus] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// Function to fetch current transcription status
const fetchTranscription = async () => {
try {
const response = await fetch(`/api/voice-notes/${voiceNoteId}/transcription`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to fetch transcription');
}
setStatus(data.status);
if (data.transcription) {
setTranscription(data.transcription);
}
} catch (err) {
setError(err.message);
}
};
// Fetch transcription on initial load
useEffect(() => {
fetchTranscription();
}, [voiceNoteId]);
// Poll for updates if transcription is pending
useEffect(() => {
let intervalId;
if (status === 'pending') {
intervalId = setInterval(fetchTranscription, 5000); // Check every 5 seconds
}
return () => {
if (intervalId) clearInterval(intervalId);
};
}, [status]);
// Request a new transcription
const requestTranscription = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(`/api/voice-notes/${voiceNoteId}/transcribe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to request transcription');
}
setStatus(data.status);
if (data.transcription) {
setTranscription(data.transcription);
}
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
// Copy transcription to clipboard
const copyTranscription = () => {
if (transcription) {
navigator.clipboard.writeText(transcription);
// Could add a "Copied!" notification here
}
};
if (!isOwner) {
return null; // Only show transcription to owners
}
return (
<div className="voice-note-transcription">
<h4>Transcription</h4>
{error && <div className="error-message">{error}</div>}
{status === 'pending' && (
<div className="pending-transcription">
<div className="loading-spinner"></div>
<p>Transcription in progress...</p>
</div>
)}
{status === 'failed' && (
<div className="failed-transcription">
<p>Transcription failed. Please try again.</p>
<button
onClick={requestTranscription}
disabled={isLoading}
className="retry-button"
>
Retry Transcription
</button>
</div>
)}
{status === 'completed' && transcription && (
<div className="completed-transcription">
<div className="transcription-text">
<p>{transcription}</p>
</div>
<div className="transcription-actions">
<button onClick={copyTranscription} className="copy-button">
Copy Text
</button>
</div>
</div>
)}
{(!status || status === 'none') && (
<div className="no-transcription">
<p>No transcription available.</p>
<button
onClick={requestTranscription}
disabled={isLoading}
className="transcribe-button"
>
{isLoading ? 'Requesting...' : 'Transcribe Voice Note'}
</button>
</div>
)}
</div>
);
};
export default VoiceNoteTranscription;
Step 10: Building the Complete Voice Note Component
Let's integrate all our components into a cohesive voice note system:
import React, { useState, useEffect } from 'react';
import VoiceNoteRecorder from './VoiceNoteRecorder';
import VoiceNoteSharing from './VoiceNoteSharing';
import VoiceNoteTranscription from './VoiceNoteTranscription';
const VoiceNoteSystem = ({ userId }) => {
const [voiceNotes, setVoiceNotes] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [isRecording, setIsRecording] = useState(false);
// Fetch user's voice notes
const fetchVoiceNotes = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/voice-notes', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to fetch voice notes');
}
setVoiceNotes(data.voiceNotes);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchVoiceNotes();
}, []);
// Handle saving a new voice note
const handleSaveVoiceNote = async (voiceNoteData) => {
try {
const formData = new FormData();
formData.append('voiceNote', new Blob(voiceNoteData.blob, { type: 'audio/webm' }));
formData.append('duration', voiceNoteData.duration);
const response = await fetch('/api/voice-notes/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: formData
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to upload voice note');
}
// Refresh the list of voice notes
fetchVoiceNotes();
setIsRecording(false);
} catch (err) {
setError(err.message);
}
};
// Handle deleting a voice note
const handleDeleteVoiceNote = async (voiceNoteId) => {
try {
const response = await fetch(`/api/voice-notes/${voiceNoteId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to delete voice note');
}
// Update the list
setVoiceNotes(voiceNotes.filter(note => note._id !== voiceNoteId));
} catch (err) {
setError(err.message);
}
};
return (
<div className="voice-note-system">
<h3>Voice Notes</h3>
{error && <div className="error-message">{error}</div>}
<div className="voice-note-actions">
{!isRecording ? (
<button
onClick={() => setIsRecording(true)}
className="new-recording-button"
>
Record New Voice Note
</button>
) : (
<div className="recorder-container">
<h4>New Voice Note</h4>
<VoiceNoteRecorder
onSave={handleSaveVoiceNote}
onCancel={() => setIsRecording(false)}
/>
</div>
)}
</div>
{isLoading ? (
<div className="loading">Loading voice notes...</div>
) : (
<div className="voice-notes-list">
{voiceNotes.length === 0 ? (
<p>You haven't recorded any voice notes yet.</p>
) : (
voiceNotes.map(note => (
<div key={note._id} className="voice-note-item">
<div className="voice-note-player">
<audio src={note.url} controls />
<span className="duration">{formatTime(note.duration)}</span>
<span className="date">{new Date(note.createdAt).toLocaleDateString()}</span>
</div>
<div className="voice-note-options">
<VoiceNoteSharing
voiceNoteId={note._id}
isOwner={note.userId === userId}
/>
<VoiceNoteTranscription
voiceNoteId={note._id}
isOwner={note.userId === userId}
/>
<button
onClick={() => handleDeleteVoiceNote(note._id)}
className="delete-button"
>
Delete
</button>
</div>
</div>
))
)}
</div>
)}
</div>
);
};
export default VoiceNoteSystem;
Storage Solutions for Audio Files
For production applications, storing audio files on the local filesystem is not ideal. Here are better options:
// Example of S3 integration for voice note storage
const AWS = require('aws-sdk');
const multer = require('multer');
const multerS3 = require('multer-s3');
// Configure AWS SDK
AWS.config.update({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION
});
const s3 = new AWS.S3();
// Configure S3 storage
const uploadToS3 = multer({
storage: multerS3({
s3: s3,
bucket: process.env.S3_BUCKET_NAME,
metadata: function (req, file, cb) {
cb(null, { fieldName: file.fieldname });
},
key: function (req, file, cb) {
const uniqueId = uuidv4();
const extension = path.extname(file.originalname) || '.webm';
cb(null, `voice-notes/${req.user.id}/${uniqueId}${extension}`);
}
}),
fileFilter: (req, file, cb) => {
const allowedMimeTypes = ['audio/webm', 'audio/mp4', 'audio/mpeg', 'audio/ogg'];
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only audio files are allowed.'), false);
}
},
limits: {
fileSize: 1024 * 1024 * 10, // 10MB max file size
}
});
// Replace the route with S3 upload
router.post('/upload', uploadToS3.single('voiceNote'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No audio file provided' });
}
// Create database entry
const voiceNote = await VoiceNote.create({
userId: req.user.id,
fileName: req.file.key,
originalName: req.file.originalname,
mimeType: req.file.mimetype,
size: req.file.size,
duration: req.body.duration || 0,
// Store S3 location instead of local path
path: req.file.location,
isS3: true,
createdAt: new Date()
});
return res.status(201).json({
success: true,
voiceNoteId: voiceNote._id,
url: voiceNote.path
});
} catch (error) {
console.error('Error uploading voice note:', error);
return res.status(500).json({ error: 'Failed to upload voice note' });
}
});
Performance Optimizations
User Experience Considerations
By following this comprehensive guide, you'll have a fully functional voice note sharing system that provides a modern, user-friendly experience while being technically sound and scalable. The modular approach allows you to start with basic functionality and add advanced features as your application matures.
Explore the top 3 practical use cases for adding voice note sharing to enhance your web app experience.
Voice notes enable team members across time zones to share nuanced feedback and context without scheduling meetings. They convey tone, emphasis, and emotion that text messages often lose, while being more convenient than video recordings.
For teams working on-site or in the field, voice notes offer hands-free documentation when typing isn't practical. Engineers, healthcare workers, and field researchers can capture observations and data points in real-time without interrupting their workflow.
Voice notes provide an inclusive communication option for users with mobility limitations, visual impairments, or those who struggle with written expression. They can dramatically improve product usability for diverse user populations while simplifying compliance with accessibility regulations.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â