UNPKG

react-webrtc-phone-dialer

Version:

A modern, floating WebRTC phone dialer component for React applications

284 lines (280 loc) 21.1 kB
'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