enhanced vodia switcher to add more caching and care about the browser visibility
This commit is contained in:
@@ -38,12 +38,13 @@ document.body.insertAdjacentHTML('beforeend', `
|
||||
`);
|
||||
|
||||
class VodiaIdentitySwitcher {
|
||||
// --- Configuration ---
|
||||
API_BASE_URL = window.baseurl || '/';
|
||||
POLLING_INTERVAL_MS = 60000;
|
||||
CACHE_DURATION_MS = 60000;
|
||||
CACHE_DURATION_MS = 60000; // 60 seconds
|
||||
LOCK_TIMEOUT_MS = 5000; // 5 seconds for a request to complete
|
||||
|
||||
CACHE_KEY = 'vodiaIdentityCache';
|
||||
LOADING_CACHE_KEY = 'vodiaIdentityCache_loading';
|
||||
LOADING_TIMEOUT_MS = 2000;
|
||||
LOCK_KEY = 'vodiaIdentityCache_lock';
|
||||
|
||||
TEXT = {
|
||||
checking: "Prüfe...",
|
||||
@@ -52,52 +53,41 @@ class VodiaIdentitySwitcher {
|
||||
ownExtension: "Eigene Nummer",
|
||||
customIdentity: "Andere Nummer",
|
||||
noActiveCall: "Kein aktiver Anruf gefunden.",
|
||||
lookupError: "Fehler bei der Anrufabfrage."
|
||||
lookupError: "Fehler bei der Anrufabfrage.",
|
||||
fetchError: "Fehler"
|
||||
};
|
||||
|
||||
pollingTimer = null;
|
||||
// --- State ---
|
||||
elements = {};
|
||||
templates = {};
|
||||
|
||||
constructor(parentElement) {
|
||||
if (!parentElement) return;
|
||||
|
||||
this.templates.main = document.getElementById('vodia-identity-template');
|
||||
this.templates.listItem = document.getElementById('vodia-list-item-template');
|
||||
|
||||
if (!this.templates.main || !this.templates.listItem) {
|
||||
return console.error("Vodia Switcher Error: Required HTML <template> tags not found.");
|
||||
}
|
||||
this._initializeTemplates();
|
||||
if (!this.templates.main || !this.templates.listItem) return;
|
||||
|
||||
this._createSwitcherUI(parentElement);
|
||||
this._addEventListeners();
|
||||
this.getVodiaIdentity();
|
||||
}
|
||||
|
||||
_getCache(key) {
|
||||
const cachedString = localStorage.getItem(key);
|
||||
if (!cachedString) return null;
|
||||
try {
|
||||
return JSON.parse(cachedString);
|
||||
} catch (e) {
|
||||
localStorage.removeItem(key);
|
||||
return null;
|
||||
// Initial load if tab is already visible
|
||||
if (document.visibilityState === 'visible') {
|
||||
this.loadIdentity();
|
||||
}
|
||||
}
|
||||
|
||||
_setCache(key, data) {
|
||||
const itemToCache = { data, timestamp: Date.now() };
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(itemToCache));
|
||||
} catch (e) {
|
||||
console.error(`Vodia Cache Error (${key}): Could not write to localStorage.`, e);
|
||||
// --- Private Methods: Initialization ---
|
||||
|
||||
_initializeTemplates() {
|
||||
this.templates.main = document.getElementById('vodia-identity-template');
|
||||
this.templates.listItem = document.getElementById('vodia-list-item-template');
|
||||
if (!this.templates.main || !this.templates.listItem) {
|
||||
console.error("Vodia Switcher Error: Required HTML <template> tags not found.");
|
||||
}
|
||||
}
|
||||
|
||||
_createSwitcherUI(parentElement) {
|
||||
const fragment = this.templates.main.content.cloneNode(true);
|
||||
const container = fragment.querySelector('#vodia-identity-container');
|
||||
|
||||
this.elements = {
|
||||
container,
|
||||
callLookupButton: container.querySelector('[data-ref="callLookupButton"]'),
|
||||
@@ -110,18 +100,19 @@ class VodiaIdentitySwitcher {
|
||||
dropdownMenu: container.querySelector('[data-ref="dropdownMenu"]'),
|
||||
identityList: container.querySelector('[data-ref="identityList"]'),
|
||||
};
|
||||
|
||||
this.elements.dropdownMenu.querySelector('[data-ref="dropdownTitle"]').textContent = this.TEXT.dropdownTitle;
|
||||
parentElement.prepend(fragment);
|
||||
}
|
||||
|
||||
_addEventListeners() {
|
||||
// Dropdown toggle
|
||||
this.elements.toggleButton.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
const isShown = this.elements.container.classList.toggle('show');
|
||||
this.elements.toggleButton.setAttribute('aria-expanded', isShown);
|
||||
this.elements.container.classList.toggle('show');
|
||||
this.elements.toggleButton.setAttribute('aria-expanded', this.elements.container.classList.contains('show'));
|
||||
});
|
||||
|
||||
// Close dropdown on outside click
|
||||
document.addEventListener('click', e => {
|
||||
if (!this.elements.container.contains(e.target)) {
|
||||
this.elements.container.classList.remove('show');
|
||||
@@ -129,35 +120,113 @@ class VodiaIdentitySwitcher {
|
||||
}
|
||||
});
|
||||
|
||||
// Call lookup button
|
||||
this.elements.callLookupButton.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
this._handleCallLookup();
|
||||
});
|
||||
|
||||
// Reload data when tab becomes visible
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
this.loadIdentity();
|
||||
}
|
||||
});
|
||||
|
||||
// Sync state across tabs when another tab updates the cache
|
||||
window.addEventListener('storage', e => {
|
||||
if (e.key === this.CACHE_KEY && e.newValue) {
|
||||
this._updateFromState(JSON.parse(e.newValue).data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_setLookupButtonLoadingState(isLoading) {
|
||||
const { callLookupButton, callLookupIconStack, callLookupSpinner } = this.elements;
|
||||
if (isLoading) {
|
||||
callLookupButton.setAttribute('disabled', 'true');
|
||||
callLookupIconStack.classList.add('d-none');
|
||||
callLookupSpinner.classList.remove('d-none');
|
||||
} else {
|
||||
callLookupButton.removeAttribute('disabled');
|
||||
callLookupIconStack.classList.remove('d-none');
|
||||
callLookupSpinner.classList.add('d-none');
|
||||
// --- Private Methods: API & Data Handling ---
|
||||
|
||||
_getCache(key) {
|
||||
const cachedString = localStorage.getItem(key);
|
||||
if (!cachedString) return null;
|
||||
try {
|
||||
return JSON.parse(cachedString);
|
||||
} catch (e) {
|
||||
localStorage.removeItem(key); // Clear corrupted cache
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_setCache(key, data) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify({ data, timestamp: Date.now() }));
|
||||
} catch (e) {
|
||||
console.error(`Vodia Cache Error (${key}): Could not write to localStorage.`, e);
|
||||
}
|
||||
}
|
||||
|
||||
async _fetchJSON(endpoint, options = {}) {
|
||||
const response = await fetch(`${this.API_BASE_URL}${endpoint}`, options);
|
||||
if (!response.ok) throw new Error(`Network response was not ok (${response.status})`);
|
||||
const data = await response.json();
|
||||
if (data.status !== "OK") throw new Error(data.message || 'API returned an error');
|
||||
return data;
|
||||
}
|
||||
|
||||
async loadIdentity() {
|
||||
// 1. Use fresh cache if available (handles re-focus)
|
||||
const cached = this._getCache(this.CACHE_KEY);
|
||||
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION_MS) {
|
||||
return this._updateFromState(cached.data);
|
||||
}
|
||||
|
||||
// 2. Check for a lock from another tab to prevent multiple requests
|
||||
const lock = this._getCache(this.LOCK_KEY);
|
||||
if (lock && Date.now() - lock.timestamp < this.LOCK_TIMEOUT_MS) {
|
||||
return; // Another tab is fetching, the 'storage' event will update this tab.
|
||||
}
|
||||
|
||||
// 3. This tab will fetch the data. Set a lock.
|
||||
this._setCache(this.LOCK_KEY, {}); // Set lock with current timestamp
|
||||
this._renderState('loading');
|
||||
|
||||
try {
|
||||
const { result } = await this._fetchJSON('User/Api/do=getVodiaIdentity');
|
||||
this._setCache(this.CACHE_KEY, result); // This triggers 'storage' event for other tabs
|
||||
this._updateFromState(result);
|
||||
} catch (error) {
|
||||
console.error("Vodia Fetch Error:", error.message);
|
||||
this._renderState('error');
|
||||
} finally {
|
||||
localStorage.removeItem(this.LOCK_KEY); // Release lock
|
||||
}
|
||||
}
|
||||
|
||||
async setVodiaOutboundIdentity(number) {
|
||||
this._renderState('setting');
|
||||
this.elements.container.classList.remove('show');
|
||||
this.elements.toggleButton.setAttribute('aria-expanded', 'false');
|
||||
|
||||
try {
|
||||
await this._fetchJSON('User/Api/do=setVodiaIdentity', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ 'number': number })
|
||||
});
|
||||
// Clear cache and lock, then force reload across all tabs
|
||||
localStorage.removeItem(this.CACHE_KEY);
|
||||
localStorage.removeItem(this.LOCK_KEY);
|
||||
this.loadIdentity();
|
||||
} catch (error) {
|
||||
console.error("Vodia Set Error:", error.message);
|
||||
if (window.notify) window.notify('error', "Fehler beim Ändern der ID!");
|
||||
this._renderState('error'); // Revert to error state, but previous data will be loaded on next focus
|
||||
}
|
||||
}
|
||||
|
||||
async _handleCallLookup() {
|
||||
this._setLookupButtonLoadingState(true);
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE_URL}User/Api/do=getVodiaCall`);
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
|
||||
const data = await response.json();
|
||||
if (data.status === 'OK' && data.result.number && data.result.number.length >= 5) {
|
||||
const url = `${this.API_BASE_URL}Address/Index?filter[pfm]=${data.result.number}`;
|
||||
window.open(url, '_blank');
|
||||
const { result } = await this._fetchJSON('User/Api/do=getVodiaCall');
|
||||
if (result.number && result.number.length >= 5) {
|
||||
window.open(`${this.API_BASE_URL}Address/Index?filter[pfm]=${result.number}`, '_blank');
|
||||
} else {
|
||||
if (window.notify) window.notify('info', this.TEXT.noActiveCall);
|
||||
}
|
||||
@@ -169,130 +238,89 @@ class VodiaIdentitySwitcher {
|
||||
}
|
||||
}
|
||||
|
||||
_setLoadingState(isLoading, message = '') {
|
||||
const { phoneIcon, currentName, currentNumber } = this.elements;
|
||||
phoneIcon.className = 'phone-icon fas fa-phone';
|
||||
// --- Private Methods: UI Rendering ---
|
||||
|
||||
if (isLoading) {
|
||||
phoneIcon.classList.add('fa-spin', 'text-warning');
|
||||
currentName.textContent = message;
|
||||
currentNumber.textContent = '';
|
||||
} else {
|
||||
phoneIcon.classList.add('text-success');
|
||||
_setLookupButtonLoadingState(isLoading) {
|
||||
this.elements.callLookupIconStack.classList.toggle('d-none', isLoading);
|
||||
this.elements.callLookupSpinner.classList.toggle('d-none', !isLoading);
|
||||
this.elements.callLookupButton.toggleAttribute('disabled', isLoading);
|
||||
}
|
||||
|
||||
_renderState(state, message = '') {
|
||||
const { phoneIcon, currentName, currentNumber } = this.elements;
|
||||
phoneIcon.className = 'phone-icon fas fa-phone'; // Reset classes
|
||||
currentNumber.textContent = '';
|
||||
|
||||
switch(state) {
|
||||
case 'loading':
|
||||
phoneIcon.classList.add('fa-spin', 'text-warning');
|
||||
currentName.textContent = this.TEXT.checking;
|
||||
break;
|
||||
case 'setting':
|
||||
phoneIcon.classList.add('fa-spin', 'text-warning');
|
||||
currentName.textContent = this.TEXT.setting;
|
||||
break;
|
||||
case 'error':
|
||||
phoneIcon.classList.add('text-danger');
|
||||
currentName.textContent = this.TEXT.fetchError;
|
||||
break;
|
||||
case 'success':
|
||||
phoneIcon.classList.add('text-success');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_updateFromState(vodiaState) {
|
||||
if (vodiaState?.enabled) {
|
||||
this.elements.container.style.display = 'flex';
|
||||
this._updateUI(vodiaState);
|
||||
this._setLoadingState(false);
|
||||
} else {
|
||||
if (!vodiaState?.enabled) {
|
||||
this.elements.container.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async getVodiaIdentity() {
|
||||
clearTimeout(this.pollingTimer);
|
||||
|
||||
const cachedData = this._getCache(this.CACHE_KEY);
|
||||
if (cachedData && (Date.now() - cachedData.timestamp < this.CACHE_DURATION_MS)) {
|
||||
this._updateFromState(cachedData.data);
|
||||
this.pollingTimer = setTimeout(() => this.getVodiaIdentity(), this.POLLING_INTERVAL_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingLock = this._getCache(this.LOADING_CACHE_KEY);
|
||||
if (loadingLock && (Date.now() - loadingLock.timestamp < this.LOADING_TIMEOUT_MS)) {
|
||||
setTimeout(() => this.getVodiaIdentity(), 500);
|
||||
return;
|
||||
}
|
||||
|
||||
this._setLoadingState(true, this.TEXT.checking);
|
||||
this._setCache(this.LOADING_CACHE_KEY, true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE_URL}User/Api/do=getVodiaIdentity`);
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
const data = await response.json();
|
||||
if (data.status !== "OK") throw new Error(data.message || 'API returned an error');
|
||||
|
||||
this._setCache(this.CACHE_KEY, data.result);
|
||||
this._updateFromState(data.result);
|
||||
} catch (error) {
|
||||
console.error("Vodia Fetch Error:", error.message);
|
||||
this.elements.phoneIcon.className = 'phone-icon fas fa-phone text-danger';
|
||||
this.elements.currentName.textContent = 'Fehler';
|
||||
} finally {
|
||||
localStorage.removeItem(this.LOADING_CACHE_KEY);
|
||||
this.pollingTimer = setTimeout(() => this.getVodiaIdentity(), this.POLLING_INTERVAL_MS);
|
||||
}
|
||||
this.elements.container.style.display = 'flex';
|
||||
this._renderState('success');
|
||||
this._updateCurrentIdentityDisplay(vodiaState);
|
||||
this._renderIdentityList(vodiaState);
|
||||
}
|
||||
|
||||
async setVodiaOutboundIdentity(number) {
|
||||
this._setLoadingState(true, this.TEXT.setting);
|
||||
this.elements.container.classList.remove('show');
|
||||
this.elements.toggleButton.setAttribute('aria-expanded', 'false');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE_URL}User/Api/do=setVodiaIdentity`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ 'number': number })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Network response was not ok.");
|
||||
const data = await response.json();
|
||||
if (data.status !== "OK") throw new Error(data.message || 'API returned an error.');
|
||||
|
||||
localStorage.removeItem(this.CACHE_KEY);
|
||||
localStorage.removeItem(this.LOADING_CACHE_KEY);
|
||||
} catch (error) {
|
||||
console.error("Vodia Set Error:", error.message);
|
||||
if (window.notify) window.notify.error("Fehler beim Ändern der ID!");
|
||||
} finally {
|
||||
await this.getVodiaIdentity();
|
||||
}
|
||||
}
|
||||
|
||||
_updateUI(vodiaState) {
|
||||
const { 'default': defaultDisplay, default_number, current, identities } = vodiaState;
|
||||
_updateCurrentIdentityDisplay(vodiaState) {
|
||||
const { 'default': defaultDisplay, default_number, current, identities = {} } = vodiaState;
|
||||
const currentId = current.replaceAll(' ', "");
|
||||
const defaultId = default_number.replaceAll(' ', "");
|
||||
|
||||
let activeName = this.TEXT.customIdentity;
|
||||
let activeNumberDisplay = `(${current})`;
|
||||
|
||||
if (currentId === default_number.replaceAll(' ', "")) {
|
||||
if (currentId === defaultId) {
|
||||
activeName = this.TEXT.ownExtension;
|
||||
activeNumberDisplay = `(${defaultDisplay})`;
|
||||
} else if (identities) {
|
||||
const found = Object.entries(identities).find(([, ident]) => ident.number === currentId);
|
||||
if (found) {
|
||||
[activeName, activeNumberDisplay] = [found[0], `(${found[1].display})`];
|
||||
} else {
|
||||
const foundName = Object.keys(identities).find(name => identities[name].number === currentId);
|
||||
if (foundName) {
|
||||
activeName = foundName;
|
||||
activeNumberDisplay = `(${identities[foundName].display})`;
|
||||
}
|
||||
}
|
||||
|
||||
this.elements.currentName.textContent = activeName;
|
||||
this.elements.currentNumber.textContent = activeNumberDisplay;
|
||||
this.elements.toggleButton.title = `Aktive ID: ${activeName} ${activeNumberDisplay}`;
|
||||
|
||||
this._renderIdentityList(vodiaState);
|
||||
}
|
||||
|
||||
_renderIdentityList(vodiaState) {
|
||||
this.elements.identityList.innerHTML = '';
|
||||
const { 'default': defaultDisplay, default_number, current, identities } = vodiaState;
|
||||
const { 'default': defaultDisplay, default_number, current, identities = {} } = vodiaState;
|
||||
const currentId = current.replaceAll(' ', "");
|
||||
const defaultId = default_number.replaceAll(' ', "");
|
||||
|
||||
// Add own extension
|
||||
this.elements.identityList.appendChild(this._createListItem({
|
||||
name: this.TEXT.ownExtension,
|
||||
number: default_number.replaceAll(' ', ""),
|
||||
number: defaultId,
|
||||
display: defaultDisplay,
|
||||
color: 'blue',
|
||||
isActive: currentId === default_number.replaceAll(' ', "")
|
||||
isActive: currentId === defaultId
|
||||
}));
|
||||
|
||||
if (!identities) return;
|
||||
// Add other identities
|
||||
for (const name in identities) {
|
||||
const ident = identities[name];
|
||||
this.elements.identityList.appendChild(this._createListItem({
|
||||
@@ -309,13 +337,12 @@ class VodiaIdentitySwitcher {
|
||||
const fragment = this.templates.listItem.content.cloneNode(true);
|
||||
const item = fragment.querySelector('li');
|
||||
|
||||
item.querySelector('[data-ref="colorBlock"]').classList.add(`vodia-identity-color-${color || 'grey'}`);
|
||||
item.querySelector('[data-ref="colorBlock"]').className = `vodia-list-item-icon fas fa-circle mr-3 vodia-identity-color-${color || 'grey'}`;
|
||||
item.querySelector('[data-ref="name"]').textContent = name;
|
||||
item.querySelector('[data-ref="numberDisplay"]').textContent = display;
|
||||
|
||||
if (isActive) {
|
||||
item.classList.add('active');
|
||||
item.querySelector('[data-ref="numberDisplay"]').classList.remove('text-muted');
|
||||
} else {
|
||||
item.classList.add('pointer');
|
||||
item.addEventListener('click', () => this.setVodiaOutboundIdentity(number));
|
||||
@@ -324,6 +351,7 @@ class VodiaIdentitySwitcher {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Bootstrap ---
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const topbar = document.querySelector("#topbar");
|
||||
if (topbar) new VodiaIdentitySwitcher(topbar);
|
||||
|
||||
Reference in New Issue
Block a user