react-webrtc-phone-dialer
Version:
A modern, floating WebRTC phone dialer component for React applications
284 lines (280 loc) • 21.1 kB
JavaScript
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var react = require('react');
var lucideReact = require('lucide-react');
const PhoneDialer = ({ availableNumbers, contacts = [], callHistory = [], activeCall = null, callStatus = "idle", initialPosition = { x: window.innerWidth - 400, y: 50 }, isMinimized: initialMinimized = false, callbacks = {}, className = "", theme = "light", }) => {
const [isMinimized, setIsMinimized] = react.useState(initialMinimized);
const [position, setPosition] = react.useState(initialPosition);
const [isDragging, setIsDragging] = react.useState(false);
const [dragOffset, setDragOffset] = react.useState({ x: 0, y: 0 });
const [activeTab, setActiveTab] = react.useState("dialer");
const [dialNumber, setDialNumber] = react.useState("");
const [selectedFromNumber, setSelectedFromNumber] = react.useState("");
const [isMuted, setIsMuted] = react.useState(false);
const [isSpeakerOn, setIsSpeakerOn] = react.useState(false);
const [callDuration, setCallDuration] = react.useState(0);
const [searchQuery, setSearchQuery] = react.useState("");
const [showFromDropdown, setShowFromDropdown] = react.useState(false);
const [isRingingEnabled, setIsRingingEnabled] = react.useState(true);
const dialerRef = react.useRef(null);
const callTimerRef = react.useRef(null);
const audioContextRef = react.useRef(null);
const ringingOscillatorRef = react.useRef(null);
const ringingGainRef = react.useRef(null);
// Initialize selected number
react.useEffect(() => {
if (!selectedFromNumber && availableNumbers.length > 0) {
setSelectedFromNumber(availableNumbers[0]);
}
}, [availableNumbers, selectedFromNumber]);
// Initialize audio context only when needed
const getAudioContext = () => {
if (!audioContextRef.current &&
typeof window !== "undefined" &&
window.AudioContext) {
audioContextRef.current = new AudioContext();
}
return audioContextRef.current;
};
// Cleanup audio context on unmount
react.useEffect(() => {
return () => {
if (audioContextRef.current) {
audioContextRef.current.close();
}
};
}, []);
// Ringing sound effect
const startRinging = () => {
const audioContext = getAudioContext();
if (!audioContext)
return;
try {
// Resume audio context if suspended
if (audioContext.state === "suspended") {
audioContext.resume();
}
// Create oscillators for classic phone ring tone (440Hz + 480Hz)
const oscillator1 = audioContext.createOscillator();
const oscillator2 = audioContext.createOscillator();
const gainNode = audioContext.createGain();
// Set up frequencies for classic phone ring
oscillator1.frequency.setValueAtTime(440, audioContext.currentTime);
oscillator2.frequency.setValueAtTime(480, audioContext.currentTime);
oscillator1.type = "sine";
oscillator2.type = "sine";
// Set up gain for volume control (lower volume for better UX)
gainNode.gain.setValueAtTime(0.05, audioContext.currentTime);
// Connect oscillators to gain node
oscillator1.connect(gainNode);
oscillator2.connect(gainNode);
gainNode.connect(audioContext.destination);
// Start oscillators
oscillator1.start();
oscillator2.start();
// Create classic phone ringing pattern (1 second on, 2 seconds off)
const ringPattern = () => {
// Fade in
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(0.05, audioContext.currentTime + 0.1);
// Fade out after 1 second
setTimeout(() => {
gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 0.1);
}, 1000);
};
// Start the ringing pattern
ringPattern();
const ringingInterval = setInterval(ringPattern, 3000);
// Store references for cleanup
ringingOscillatorRef.current = oscillator1;
ringingGainRef.current = gainNode;
// Return cleanup function
return () => {
clearInterval(ringingInterval);
oscillator1.stop();
oscillator2.stop();
gainNode.disconnect();
};
}
catch (error) {
console.warn("Could not start ringing sound:", error);
}
};
const stopRinging = () => {
if (ringingOscillatorRef.current) {
ringingOscillatorRef.current.stop();
ringingOscillatorRef.current = null;
}
if (ringingGainRef.current) {
ringingGainRef.current.disconnect();
ringingGainRef.current = null;
}
};
// Handle ringing state changes
react.useEffect(() => {
if (callStatus === "ringing" && isRingingEnabled) {
const cleanup = startRinging();
return cleanup;
}
else {
stopRinging();
}
}, [callStatus, isRingingEnabled]);
// Call timer effect
react.useEffect(() => {
if (callStatus === "in-call") {
const timer = setInterval(() => {
setCallDuration((prev) => prev + 1);
}, 1000);
callTimerRef.current = timer;
return () => {
if (callTimerRef.current) {
clearInterval(callTimerRef.current);
}
};
}
else {
setCallDuration(0);
if (callTimerRef.current) {
clearInterval(callTimerRef.current);
callTimerRef.current = null;
}
}
}, [callStatus]);
// Auto-expand dialer for incoming calls
react.useEffect(() => {
if (callStatus === "ringing") {
setIsMinimized(false);
}
}, [callStatus]);
// Drag functionality
const handleMouseDown = (e) => {
var _a;
if (e.target.closest(".drag-handle")) {
setIsDragging(true);
const rect = (_a = dialerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
if (rect) {
setDragOffset({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
}
}
};
const handleMouseMove = (e) => {
if (isDragging) {
setPosition({
x: e.clientX - dragOffset.x,
y: e.clientY - dragOffset.y,
});
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
react.useEffect(() => {
if (isDragging) {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}
}, [isDragging, dragOffset]);
// Helper functions
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, "0")}`;
};
const handleDialerInput = (digit) => {
setDialNumber((prev) => prev + digit);
};
const handleCall = (number = dialNumber) => {
if (number && callbacks.onCall) {
callbacks.onCall(number, selectedFromNumber);
setDialNumber("");
}
};
const handleEndCall = () => {
if (callbacks.onEndCall) {
callbacks.onEndCall((activeCall === null || activeCall === void 0 ? void 0 : activeCall.id) || "");
}
};
const handleCancelDialing = () => {
if (callbacks.onCancelDialing && (activeCall === null || activeCall === void 0 ? void 0 : activeCall.id)) {
callbacks.onCancelDialing(activeCall.id);
}
};
const handleAnswerCall = () => {
stopRinging(); // Stop ringing when call is answered
if (activeCall && callbacks.onAnswerCall) {
callbacks.onAnswerCall(activeCall.id);
}
};
const handleRejectCall = () => {
stopRinging(); // Stop ringing when call is rejected
if (activeCall && callbacks.onRejectCall) {
callbacks.onRejectCall(activeCall.id);
}
};
const toggleMute = () => {
const newMutedState = !isMuted;
setIsMuted(newMutedState);
if (callbacks.onMute) {
callbacks.onMute(newMutedState);
}
};
const toggleSpeaker = () => {
const newSpeakerState = !isSpeakerOn;
setIsSpeakerOn(newSpeakerState);
if (callbacks.onSpeaker) {
callbacks.onSpeaker(newSpeakerState);
}
};
const filteredContacts = contacts.filter((contact) => contact.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
contact.number.includes(searchQuery));
// Render minimized state
if (isMinimized) {
return (jsxRuntime.jsxs("div", { ref: dialerRef, className: `phone-dialer-minimized ${className}`, style: { left: position.x, top: position.y }, onClick: () => setIsMinimized(false), onMouseDown: handleMouseDown, "data-theme": theme, children: [jsxRuntime.jsx(lucideReact.Phone, { className: "phone-dialer-minimized-icon" }), (callStatus === "ringing" || callStatus === "in-call") && (jsxRuntime.jsx("div", { className: `phone-dialer-notification-dot ${callStatus === "ringing" ? "pulse" : ""}` }))] }));
}
// Ensure AudioContext is initialized on first user interaction
const handleUserInteraction = () => {
getAudioContext();
};
return (jsxRuntime.jsxs("div", { ref: dialerRef, className: `phone-dialer-new ${className}`, style: { left: position.x, top: position.y }, onMouseDown: handleMouseDown, onClick: handleUserInteraction, "data-theme": theme, children: [jsxRuntime.jsxs("div", { className: "phone-dialer-header drag-handle", children: [jsxRuntime.jsxs("div", { className: "phone-dialer-header-top", children: [jsxRuntime.jsxs("div", { className: "phone-dialer-title", children: [jsxRuntime.jsx(lucideReact.Phone, { className: "phone-dialer-title-icon" }), jsxRuntime.jsx("span", { children: "Phone" })] }), jsxRuntime.jsxs("div", { className: "phone-dialer-header-controls", children: [jsxRuntime.jsx("button", { onClick: () => setIsRingingEnabled(!isRingingEnabled), className: `phone-dialer-ringing-toggle ${!isRingingEnabled ? "disabled" : ""}`, title: isRingingEnabled
? "Disable ringing sound"
: "Enable ringing sound", children: jsxRuntime.jsx(lucideReact.Volume2, {}) }), jsxRuntime.jsx("button", { onClick: () => setIsMinimized(true), className: "phone-dialer-minimize-btn", children: jsxRuntime.jsx(lucideReact.Minimize2, {}) })] })] }), callStatus === "idle" && (jsxRuntime.jsxs("div", { className: "phone-dialer-from-selector", children: [jsxRuntime.jsxs("button", { onClick: () => setShowFromDropdown(!showFromDropdown), className: "phone-dialer-from-btn", children: [jsxRuntime.jsxs("span", { children: ["From: ", selectedFromNumber] }), jsxRuntime.jsx(lucideReact.ChevronDown, {})] }), showFromDropdown && (jsxRuntime.jsx("div", { className: "phone-dialer-dropdown", children: availableNumbers.map((number) => (jsxRuntime.jsx("button", { onClick: () => {
setSelectedFromNumber(number);
setShowFromDropdown(false);
}, className: "phone-dialer-dropdown-item", children: number }, number))) }))] }))] }), callStatus === "ringing" && (jsxRuntime.jsxs("div", { className: "phone-dialer-connected-state ringing", children: [jsxRuntime.jsxs("div", { className: "phone-dialer-connected-info", children: [jsxRuntime.jsx("div", { className: "phone-dialer-connected-avatar", children: (activeCall === null || activeCall === void 0 ? void 0 : activeCall.avatar) || "👤" }), jsxRuntime.jsx("h3", { className: "phone-dialer-connected-title", children: "Incoming Call" }), jsxRuntime.jsx("p", { className: "phone-dialer-connected-number", children: (activeCall === null || activeCall === void 0 ? void 0 : activeCall.name) || "Unknown" }), jsxRuntime.jsx("p", { className: "phone-dialer-call-duration", children: activeCall === null || activeCall === void 0 ? void 0 : activeCall.number }), jsxRuntime.jsxs("div", { className: `phone-dialer-ringing-indicator ${!isRingingEnabled ? "disabled" : ""}`, children: [jsxRuntime.jsx(lucideReact.Volume2, { className: "phone-dialer-ringing-sound-icon" }), jsxRuntime.jsx("span", { children: isRingingEnabled ? "Ringing..." : "Silent mode" })] })] }), jsxRuntime.jsxs("div", { className: "phone-dialer-connected-controls", children: [jsxRuntime.jsx("button", { onClick: handleRejectCall, className: "phone-dialer-end-call-btn", children: jsxRuntime.jsx(lucideReact.PhoneOff, {}) }), jsxRuntime.jsx("button", { onClick: handleAnswerCall, className: "phone-dialer-call-btn", style: {
background: "#10b981",
color: "white",
borderRadius: "50%",
width: "48px",
height: "48px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}, children: jsxRuntime.jsx(lucideReact.Phone, {}) })] })] })), callStatus === "dialing" && (jsxRuntime.jsxs("div", { className: "phone-dialer-ringing-state", children: [jsxRuntime.jsxs("div", { className: "phone-dialer-ringing-info", children: [jsxRuntime.jsxs("div", { className: "phone-dialer-ringing-avatar", children: [jsxRuntime.jsx(lucideReact.Phone, { className: "phone-dialer-ringing-icon" }), jsxRuntime.jsx("div", { className: "phone-dialer-pulse-ring" })] }), jsxRuntime.jsx("h3", { className: "phone-dialer-ringing-title", children: "Calling..." }), jsxRuntime.jsx("p", { className: "phone-dialer-ringing-number", children: activeCall === null || activeCall === void 0 ? void 0 : activeCall.number }), jsxRuntime.jsxs("div", { className: "phone-dialer-ringing-dots", children: [jsxRuntime.jsx("div", { className: "phone-dialer-dot" }), jsxRuntime.jsx("div", { className: "phone-dialer-dot" }), jsxRuntime.jsx("div", { className: "phone-dialer-dot" })] })] }), jsxRuntime.jsx("div", { className: "phone-dialer-ringing-controls", children: jsxRuntime.jsx("button", { onClick: handleCancelDialing, className: "phone-dialer-cancel-btn", children: jsxRuntime.jsx(lucideReact.PhoneOff, {}) }) })] })), callStatus === "in-call" && (jsxRuntime.jsxs("div", { className: "phone-dialer-connected-state", children: [jsxRuntime.jsxs("div", { className: "phone-dialer-connected-info", children: [jsxRuntime.jsx("div", { className: "phone-dialer-connected-avatar", children: jsxRuntime.jsx(lucideReact.Phone, {}) }), jsxRuntime.jsx("h3", { className: "phone-dialer-connected-title", children: "Connected" }), jsxRuntime.jsx("p", { className: "phone-dialer-connected-number", children: (activeCall === null || activeCall === void 0 ? void 0 : activeCall.number) || selectedFromNumber }), jsxRuntime.jsx("p", { className: "phone-dialer-call-duration", children: formatTime(callDuration) })] }), jsxRuntime.jsxs("div", { className: "phone-dialer-connected-controls", children: [jsxRuntime.jsx("button", { onClick: toggleMute, className: `phone-dialer-control-btn ${isMuted ? "active" : ""}`, children: isMuted ? jsxRuntime.jsx(lucideReact.MicOff, {}) : jsxRuntime.jsx(lucideReact.Mic, {}) }), jsxRuntime.jsx("button", { onClick: handleEndCall, className: "phone-dialer-end-call-btn", children: jsxRuntime.jsx(lucideReact.PhoneOff, {}) }), jsxRuntime.jsx("button", { onClick: toggleSpeaker, className: `phone-dialer-control-btn ${isSpeakerOn ? "active" : ""}`, children: isSpeakerOn ? jsxRuntime.jsx(lucideReact.Volume2, {}) : jsxRuntime.jsx(lucideReact.VolumeX, {}) })] })] })), callStatus === "idle" && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "phone-dialer-tabs", children: [
{ id: "dialer", icon: lucideReact.Phone, label: "Dial" },
{ id: "contacts", icon: lucideReact.Users, label: "Contacts" },
{ id: "history", icon: lucideReact.History, label: "History" },
].map((tab) => (jsxRuntime.jsxs("button", { onClick: () => setActiveTab(tab.id), className: `phone-dialer-tab ${activeTab === tab.id ? "active" : ""}`, children: [jsxRuntime.jsx(tab.icon, { className: "phone-dialer-tab-icon" }), jsxRuntime.jsx("span", { children: tab.label })] }, tab.id))) }), jsxRuntime.jsxs("div", { className: "phone-dialer-content", children: [activeTab === "dialer" && (jsxRuntime.jsxs("div", { className: "phone-dialer-tab-content", children: [jsxRuntime.jsx("input", { type: "text", value: dialNumber, onChange: (e) => setDialNumber(e.target.value), placeholder: "Enter phone number", className: "phone-dialer-input" }), jsxRuntime.jsx("div", { className: "phone-dialer-keypad", children: [
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"*",
"0",
"#",
].map((digit) => (jsxRuntime.jsx("button", { onClick: () => handleDialerInput(digit), className: "phone-dialer-key", children: digit }, digit))) }), jsxRuntime.jsxs("button", { onClick: () => handleCall(), disabled: !dialNumber, className: "phone-dialer-call-btn", children: [jsxRuntime.jsx(lucideReact.PhoneCall, {}), jsxRuntime.jsx("span", { children: "Call" })] })] })), activeTab === "contacts" && (jsxRuntime.jsxs("div", { className: "phone-dialer-tab-content", children: [jsxRuntime.jsxs("div", { className: "phone-dialer-search", children: [jsxRuntime.jsx(lucideReact.Search, { className: "phone-dialer-search-icon" }), jsxRuntime.jsx("input", { type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search contacts", className: "phone-dialer-search-input" })] }), jsxRuntime.jsx("div", { className: "phone-dialer-contacts-list", children: filteredContacts.map((contact) => (jsxRuntime.jsxs("div", { className: "phone-dialer-contact-item", children: [jsxRuntime.jsxs("div", { className: "phone-dialer-contact-info", children: [jsxRuntime.jsx("div", { className: "phone-dialer-contact-avatar", children: contact.avatar || "👤" }), jsxRuntime.jsxs("div", { className: "phone-dialer-contact-details", children: [jsxRuntime.jsx("div", { className: "phone-dialer-contact-name", children: contact.name }), jsxRuntime.jsx("div", { className: "phone-dialer-contact-number", children: contact.number })] })] }), jsxRuntime.jsx("button", { onClick: () => handleCall(contact.number), className: "phone-dialer-contact-call-btn", children: jsxRuntime.jsx(lucideReact.Phone, {}) })] }, contact.id))) })] })), activeTab === "history" && (jsxRuntime.jsx("div", { className: "phone-dialer-tab-content", children: jsxRuntime.jsx("div", { className: "phone-dialer-history-list", children: callHistory.map((call) => (jsxRuntime.jsxs("div", { className: "phone-dialer-history-item", children: [jsxRuntime.jsxs("div", { className: "phone-dialer-history-info", children: [jsxRuntime.jsx("div", { className: `phone-dialer-history-icon ${call.type}`, children: jsxRuntime.jsx(lucideReact.Phone, {}) }), jsxRuntime.jsxs("div", { className: "phone-dialer-history-details", children: [jsxRuntime.jsx("div", { className: "phone-dialer-history-name", children: call.name }), jsxRuntime.jsxs("div", { className: "phone-dialer-history-meta", children: [call.timestamp.toLocaleString(), " \u2022", " ", formatTime(call.duration)] })] })] }), jsxRuntime.jsx("button", { onClick: () => handleCall(call.number), className: "phone-dialer-history-call-btn", children: jsxRuntime.jsx(lucideReact.Phone, {}) })] }, call.id))) }) }))] })] }))] }));
};
exports.PhoneDialer = PhoneDialer;
//# sourceMappingURL=index.js.map