Add availability indicators on people tiles and share fetch service
New Availability module decorates each people-tile with a vertical status bar on the avatar and a dot+timestamp under the status text, showing ZeitConsens loggedIn state per extension. Status and Availability now share a single AvailabilityService that polls /availability every 30s, halving the API load. Avatars are enlarged to 57px and vertically centered to fit the new layout.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
export class AvailabilityInfo {
|
||||
public user: string;
|
||||
public loggedIn: boolean;
|
||||
public extension: string;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Config } from './config'
|
||||
import { AvailabilityInfo } from './availability-info'
|
||||
import GM_fetch from '@trim21/gm-fetch'
|
||||
|
||||
type Listener = (availabilities: AvailabilityInfo[]) => void
|
||||
|
||||
export class AvailabilityService {
|
||||
private _availabilities: AvailabilityInfo[] = []
|
||||
private _listeners: Listener[] = []
|
||||
private _started = false
|
||||
|
||||
public start() {
|
||||
if (this._started) {
|
||||
return
|
||||
}
|
||||
this._started = true
|
||||
this.fetch()
|
||||
setInterval(() => this.fetch(), 30000)
|
||||
}
|
||||
|
||||
public subscribe(listener: Listener) {
|
||||
this._listeners.push(listener)
|
||||
if (this._availabilities.length > 0) {
|
||||
listener(this._availabilities)
|
||||
}
|
||||
}
|
||||
|
||||
public get availabilities(): AvailabilityInfo[] {
|
||||
return this._availabilities
|
||||
}
|
||||
|
||||
private async fetch() {
|
||||
try {
|
||||
var response = await GM_fetch(Config.tapi_server_url + '/availability')
|
||||
if (response.status === 200) {
|
||||
this._availabilities = await response.json() as AvailabilityInfo[]
|
||||
this._listeners.forEach(l => l(this._availabilities))
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
.tapi-availability {
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
font-size: 9px;
|
||||
opacity: 0.7;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin-top: -4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.tapi-availability small {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.tapi-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tapi-dot-on {
|
||||
background-color: #5cb85c;
|
||||
}
|
||||
|
||||
.tapi-dot-off {
|
||||
background-color: #d9534f;
|
||||
}
|
||||
|
||||
.tapi-avatar-square {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 5px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
people-tile .avatar-size-m,
|
||||
people-tile .avatar-size-m .avatar-container,
|
||||
people-tile .avatar-size-m .avatar-content {
|
||||
width: 57px !important;
|
||||
height: 57px !important;
|
||||
line-height: 57px !important;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
people-tile {
|
||||
grid-template-rows: 29px 23px;
|
||||
}
|
||||
|
||||
people-tile app-avatar {
|
||||
grid-row: 1 / span 2 !important;
|
||||
align-self: center !important;
|
||||
}
|
||||
|
||||
.tapi-square-on {
|
||||
background-color: #5cb85c;
|
||||
}
|
||||
|
||||
.tapi-square-off {
|
||||
background-color: #d9534f;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import './availability.css'
|
||||
import { AvailabilityInfo } from './availability-info'
|
||||
import { AvailabilityService } from './availability-service'
|
||||
|
||||
declare function waitForKeyElements(selectorOrFunction: any, callback: any, waitOnce: boolean): any;
|
||||
|
||||
export class Availability {
|
||||
private _service: AvailabilityService
|
||||
private _availabilities: AvailabilityInfo[] = []
|
||||
|
||||
constructor(service: AvailabilityService) {
|
||||
this._service = service
|
||||
}
|
||||
|
||||
public start() {
|
||||
this._service.subscribe((avs) => {
|
||||
this._availabilities = avs
|
||||
this.updateAllIndicators()
|
||||
})
|
||||
waitForKeyElements('people-tile', (element: HTMLElement) => { this.decorateTile(element) }, false)
|
||||
}
|
||||
|
||||
private decorateTile(tile: HTMLElement) {
|
||||
if (tile.querySelector('.tapi-availability')) {
|
||||
return
|
||||
}
|
||||
var extEl = tile.querySelector('span[data-id="extPhone"]') as HTMLElement
|
||||
if (!extEl) {
|
||||
return
|
||||
}
|
||||
var extension = extEl.textContent.trim()
|
||||
|
||||
var indicator = document.createElement('div')
|
||||
indicator.classList.add('tapi-availability')
|
||||
indicator.dataset.tapiExtension = extension
|
||||
tile.appendChild(indicator)
|
||||
this.updateIndicator(indicator)
|
||||
|
||||
var avatarContainer = tile.querySelector('app-avatar .avatar-container') as HTMLElement
|
||||
if (avatarContainer) {
|
||||
var square = document.createElement('i')
|
||||
square.classList.add('tapi-avatar-square')
|
||||
square.dataset.tapiExtension = extension
|
||||
avatarContainer.appendChild(square)
|
||||
this.updateSquare(square)
|
||||
}
|
||||
}
|
||||
|
||||
private updateAllIndicators() {
|
||||
var indicators = document.getElementsByClassName('tapi-availability')
|
||||
for (var i of indicators) {
|
||||
this.updateIndicator(i as HTMLElement)
|
||||
}
|
||||
var squares = document.getElementsByClassName('tapi-avatar-square')
|
||||
for (var s of squares) {
|
||||
this.updateSquare(s as HTMLElement)
|
||||
}
|
||||
}
|
||||
|
||||
private updateIndicator(indicator: HTMLElement) {
|
||||
var extension = indicator.dataset.tapiExtension
|
||||
var entry = this._availabilities.find(a => a.extension === extension)
|
||||
if (!entry) {
|
||||
indicator.innerHTML = ''
|
||||
return
|
||||
}
|
||||
var dotClass = entry.loggedIn ? 'tapi-dot-on' : 'tapi-dot-off'
|
||||
var mockedTime = '13.04. 08:30'
|
||||
indicator.innerHTML = '<span class="tapi-dot ' + dotClass + '"></span><small>' + mockedTime + '</small>'
|
||||
}
|
||||
|
||||
private updateSquare(square: HTMLElement) {
|
||||
var extension = square.dataset.tapiExtension
|
||||
var entry = this._availabilities.find(a => a.extension === extension)
|
||||
square.classList.remove('tapi-square-on', 'tapi-square-off')
|
||||
if (!entry) {
|
||||
square.style.display = 'none'
|
||||
return
|
||||
}
|
||||
square.style.display = ''
|
||||
square.classList.add(entry.loggedIn ? 'tapi-square-on' : 'tapi-square-off')
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -1,5 +1,7 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import * as chrono from 'chrono-node'
|
||||
import { Availability } from './availability'
|
||||
import { AvailabilityService } from './availability-service'
|
||||
import { CallHistory } from './call-history'
|
||||
import { CallNotification } from './call-notification'
|
||||
import { Presence } from './presence'
|
||||
@@ -24,8 +26,14 @@ const callHistory = new CallHistory()
|
||||
// eslint-disable-next-line no-undef
|
||||
waitForKeyElements('call', (element) => { callHistory.showCallHistory(element) }, false)
|
||||
|
||||
const status = new Status()
|
||||
const availabilityService = new AvailabilityService()
|
||||
availabilityService.start()
|
||||
|
||||
const status = new Status(availabilityService)
|
||||
// eslint-disable-next-line no-undef
|
||||
waitForKeyElements('wc-account-menu', (element) => { status.showStatus(element) }, false)
|
||||
|
||||
waitForKeyElements('wc-account-menu i.status-indicator', (element) => { status.watchStatus(element) }, false)
|
||||
|
||||
const availability = new Availability(availabilityService)
|
||||
availability.start()
|
||||
|
||||
+39
-28
@@ -1,17 +1,22 @@
|
||||
import { Config } from './config';
|
||||
import './status.css';
|
||||
import { ZcStatus } from './zc-status';
|
||||
import GM_fetch from "@trim21/gm-fetch";
|
||||
import { AvailabilityInfo } from './availability-info';
|
||||
import { AvailabilityService } from './availability-service';
|
||||
const zcIcon = require('./stopwatch-regular-full.svg');
|
||||
|
||||
declare function waitForKeyElements(selectorOrFunction: any, callback: any, waitOnce: boolean): any;
|
||||
|
||||
export class Status {
|
||||
private _service: AvailabilityService;
|
||||
private _user: string;
|
||||
private _enabled = false;
|
||||
private _statusOn = 'menuAvailable';
|
||||
private _statusOff = 'menuAway';
|
||||
private _currentStatus: boolean = undefined;
|
||||
private _subscribed = false;
|
||||
|
||||
constructor(service: AvailabilityService) {
|
||||
this._service = service;
|
||||
}
|
||||
|
||||
public async showStatus(element: HTMLElement) {
|
||||
this._user = await GM.getValue('tapi-zc-user', '');
|
||||
@@ -20,34 +25,38 @@ export class Status {
|
||||
this._statusOff = await GM.getValue('tapi-zc-off', 'menuAvailable');
|
||||
console.log('tapi-zc-user', this._user, 'tapi-zc-enabled', this._enabled, 'tapi-zc-on', this._statusOn, 'tapi-zc-off', this._statusOff);
|
||||
|
||||
this.checkStatus();
|
||||
this.subscribeOnce();
|
||||
|
||||
waitForKeyElements("wc-account-menu > div > ul", (element: HTMLElement) => { this.addZcStatusPopup(element) }, true);
|
||||
}
|
||||
|
||||
private async checkStatus() {
|
||||
if (this._enabled) {
|
||||
try {
|
||||
var response = await GM_fetch(Config.tapi_server_url + '/availability/' + encodeURIComponent(this._user));
|
||||
if (response.status == 200) {
|
||||
var status = await response.json() as ZcStatus;
|
||||
if (this._currentStatus !== status.loggedIn) {
|
||||
this._currentStatus = status.loggedIn;
|
||||
console.log('New status, loggedIn', this._currentStatus);
|
||||
var accMenu = document.getElementsByTagName("wc-account-menu")[0];
|
||||
var avatar = accMenu.getElementsByTagName("app-avatar")[0] as HTMLAnchorElement;
|
||||
avatar.click();
|
||||
setTimeout(() => {
|
||||
var statusId = this._currentStatus ? this._statusOn : this._statusOff;
|
||||
console.log('Clicking status', statusId);
|
||||
(document.getElementById(statusId) as HTMLSpanElement).click();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
setTimeout(() => this.checkStatus(), 30000);
|
||||
private subscribeOnce() {
|
||||
if (this._subscribed) {
|
||||
return;
|
||||
}
|
||||
this._subscribed = true;
|
||||
this._service.subscribe((avs) => this.onAvailabilities(avs));
|
||||
}
|
||||
|
||||
private onAvailabilities(avs: AvailabilityInfo[]) {
|
||||
if (!this._enabled || !this._user) {
|
||||
return;
|
||||
}
|
||||
var entry = avs.find(a => a.user === this._user);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
if (this._currentStatus !== entry.loggedIn) {
|
||||
this._currentStatus = entry.loggedIn;
|
||||
console.log('New status, loggedIn', this._currentStatus);
|
||||
var accMenu = document.getElementsByTagName("wc-account-menu")[0];
|
||||
var avatar = accMenu.getElementsByTagName("app-avatar")[0] as HTMLAnchorElement;
|
||||
avatar.click();
|
||||
setTimeout(() => {
|
||||
var statusId = this._currentStatus ? this._statusOn : this._statusOff;
|
||||
console.log('Clicking status', statusId);
|
||||
(document.getElementById(statusId) as HTMLSpanElement).click();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +158,9 @@ export class Status {
|
||||
GM.setValue('tapi-zc-enabled', this._enabled);
|
||||
console.log('tapi-zc-enabled', this._enabled);
|
||||
this._currentStatus = undefined;
|
||||
this.checkStatus();
|
||||
if (this._enabled) {
|
||||
this.onAvailabilities(this._service.availabilities);
|
||||
}
|
||||
}
|
||||
|
||||
var zcOn = document.getElementById('tapi-zc-on') as HTMLSelectElement;
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export class ZcStatus {
|
||||
public user: string;
|
||||
public loggedIn: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user