diff --git a/env.example b/env.example index b30366f..59a70cb 100644 --- a/env.example +++ b/env.example @@ -2,6 +2,7 @@ SIGNALWIRE_PROJECT_KEY=YOURPROJECTKEY SIGNALWIRE_TOKEN=YOURTOKEN SIGNALWIRE_SPACE=YOURSPACE.signalwire.com DEFAULT_DESTINATION=SOMETARGET +SIGNALWIRE_FABRIC_API_URL=https://fabric.swire.io/api #Firebase Initilization Params FIREBASE_API_KEY= diff --git a/index.js b/index.js index fcd4a4e..4c1ebf4 100644 --- a/index.js +++ b/index.js @@ -29,6 +29,7 @@ const FIREBASE_CONFIG = JSON.stringify({ }) const host = process.env.RELAY_HOST +const fabricApiUrl = process.env.SIGNALWIRE_FABRIC_API_URL async function apiRequest(uri, options) { const response = await fetch(uri, options) @@ -97,6 +98,7 @@ app.get('/', async (req, res) => { res.render('index', { host, token: token, + fabricApiUrl: fabricApiUrl, destination: process.env.DEFAULT_DESTINATION, firebaseConfig: FIREBASE_CONFIG, }) @@ -111,6 +113,7 @@ app.get('/minimal', async (req, res) => { res.render('minimal', { host, token: token, + fabricApiUrl: fabricApiUrl, destination: process.env.DEFAULT_DESTINATION, firebaseConfig: FIREBASE_CONFIG, }) @@ -164,7 +167,6 @@ app.post('/subscriber', async (req, res) => { console.log('process subscriber') const { reference, password } = req.body; - console.log("reference: ", reference, "password: ", password) try { const tokenData = await getSubscriberToken(reference, password) diff --git a/package-lock.json b/package-lock.json index f97621e..2d57c88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1823,9 +1823,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.0.tgz", - "integrity": "sha512-WvImr5kpN5NGNn7KaDjJnLTh5rDVLZiDf/YLA8T1ZEZEBZNEDOE+mnkS0PVjPax8ZxBP5zC5SLMB3/9VV5de9g==", + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", + "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", "engines": { "node": ">=12" }, @@ -2027,12 +2027,12 @@ } }, "node_modules/express-session": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", - "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", "dependencies": { - "cookie": "0.4.2", - "cookie-signature": "1.0.6", + "cookie": "0.6.0", + "cookie-signature": "1.0.7", "debug": "2.6.9", "depd": "~2.0.0", "on-headers": "~1.0.2", @@ -2045,13 +2045,18 @@ } }, "node_modules/express-session/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", diff --git a/public/full.js b/public/full.js index b4561de..88cbb49 100644 --- a/public/full.js +++ b/public/full.js @@ -13,6 +13,9 @@ const { createMicrophoneAnalyzer, } = SignalWire +const searchInput = document.getElementById('searchInput') +const searchType = document.getElementById('searchType') + window.getMicrophoneDevices = getMicrophoneDevices window.getCameraDevices = getCameraDevices window.getSpeakerDevices = getSpeakerDevices @@ -878,6 +881,66 @@ const escapeHTML = (str) => { return div.innerHTML } +function isBlank(str) { + return str === null || str === undefined || str === '' || str === 'null'; +} + +function setupAddressModal() { + const addressModal = document.getElementById('addressModal') + if (!addressModal) return + + addressModal.addEventListener('show.bs.modal', event => { + const button = event.relatedTarget + const addressName = button.getAttribute('data-bs-name') + const address = __addressData.addresses.find(address => address.name === addressName) + updateAddressModal(address) + + // TODO: load recent conversations associated with address + // messages = await fetchConversationHistory(__subscriberId, address.id) + // renderConversationHistory(messages) + }) + + addressModal.addEventListener('hidden.bs.modal', event => { + updateAddressModal({name:'',display_name:'',resouce_id:null,cover_url:null,preview_url:null,type:null,channels: []}) + }) +} + +function updateAddressModal(address) { + const addressModal = document.getElementById('addressModal') + if (!addressModal) return + + const addressDisplayName = addressModal.querySelector('.modal-body .address-display-name') + const addressAvatar = addressModal.querySelector('.modal-body .address-avatar') + const addressBadge = addressModal.querySelector('.modal-body .address-badge') + const channelButtons = { + audio: addressModal.querySelector('.modal-body .btn-address-dial-audio'), + video: addressModal.querySelector('.modal-body .btn-address-dial-video'), + messaging: addressModal.querySelector('.modal-body .btn-address-dial-messaging') + }; + + addressDisplayName.textContent = address.display_name + addressBadge.textContent = address.type + addressAvatar.src = address.cover_url || address.preview_url || `https://i.pravatar.cc/125?u=${address.resource_id}` + + // disable all channel buttons + for (let channelButton in channelButtons) { + channelButtons[channelButton].disabled = true + } + + // re-enable appropriate channel buttons + Object.entries(address.channels).forEach(([channelName, channelValue]) => { + let button = channelButtons[channelName] + let clone = button.cloneNode(true) + clone.disabled = false + button.parentNode.replaceChild(clone, button) + clone.addEventListener('click', () => { + dialAddress(channelValue) + const addressModalInstance = bootstrap.Modal.getInstance(addressModal) + addressModalInstance.hide() + }) + }) +} + function updateAddressUI() { addressesCard.classList.remove('d-none') const addressesDiv = document.getElementById('addresses') @@ -913,9 +976,13 @@ function updateAddressUI() { badge.textContent = type; col1.appendChild(badge); - const b = document.createElement('b'); - b.textContent = displayName; - col1.appendChild(b); + const addressNameLink = document.createElement('a'); + addressNameLink.textContent = displayName; + addressNameLink.href = '#'; + addressNameLink.dataset.bsToggle = 'modal'; + addressNameLink.dataset.bsTarget = '#addressModal'; + addressNameLink.dataset.bsName = address.name; + col1.appendChild(addressNameLink); const col2 = document.createElement('div'); col2.className = 'col'; @@ -958,14 +1025,43 @@ function updateAddressUI() { async function fetchAddresses() { if (!client) return try { - const addressData = await client.getAddresses() + const searchText = searchInput.value + const selectedType = searchType.value + + const addressData = await client.getAddresses({ + type: selectedType === 'all' ? undefined : selectedType, + displayName: !searchText.length ? undefined : searchText, + }) window.__addressData = addressData updateAddressUI() + setupAddressModal() } catch (error) { console.error('Unable to fetch addresses', error) } } +// Just a placeholder until ready. We can prepare `client` methods as well +async function fetchConversationHistory(subscriberId, addressId) { + const queryParams = new URLSearchParams({ + subscriber_id: subscriberId, + address_id: addressId, + limit: 10, + }) + + const response = await fetch(`${_fabricApiUrl}/conversations?${queryParams}`, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${_token}` + } + }) + + if (!response.ok) { + throw new Error('Unable to fetch conversation history') + } + + return await response.json() +} + window.dialAddress = async (address) => { const destinationInput = document.getElementById('destination') destinationInput.value = address @@ -993,3 +1089,12 @@ window.fetchPrevAddresses = async () => { console.error('Unable to fetch prev addresses', error) } } + +let debounceTimeout +searchInput.addEventListener('input', () => { + clearTimeout(debounceTimeout) + // Search after 1 seconds when user stops typing + debounceTimeout = setTimeout(fetchAddresses, 1000) +}) + +searchType.addEventListener('change', fetchAddresses) diff --git a/views/index.ejs b/views/index.ejs index 83bc72a..624312f 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -11,8 +11,8 @@ - + @@ -179,7 +179,29 @@
+ +
+ + +
+ +
+ +
+
@@ -333,6 +356,88 @@

+ + + + + diff --git a/views/minimal.ejs b/views/minimal.ejs index db6dd6d..e7f3f88 100644 --- a/views/minimal.ejs +++ b/views/minimal.ejs @@ -66,6 +66,7 @@