UNPKG

ngx-rumbletalk

Version:

RumbleTalk Angular integration

457 lines (450 loc) 23.3 kB
import * as i0 from '@angular/core'; import { Injectable, Component, ChangeDetectionStrategy, ViewChild, Input, NgModule } from '@angular/core'; import { map } from 'rxjs/operators'; import * as i1 from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http'; import * as i2 from '@angular/common'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; class NgxRumbletalkService { constructor(http) { this.http = http; this.server = ''; this.iframeLoaded = new Promise((resolve, reject) => { this.handleResolve = resolve; this.handleReject = reject; }); this.postMessageEvents = { LOGOUT_CB: 'pm.1', LOGOUT_CB_RECEIVED: 'pm.2', LOGIN: 'pm.3', LOGIN_SUCCESS: 'pm.4', LOGIN_ALREADY_LOGGED_IN: 'pm.5', LOGOUT: 'pm.6', OPEN_PRIVATE_CHAT: 'pm.7', }; } address(hash) { return this.http .get(`https://rumbletalk.com/client/service.php?hash=${hash}`) .pipe(map((data) => data.address)); } reload(url) { return this.http.get(url); } login(data) { return new Promise((resolve, reject) => { const message = {}; /* handle username value */ message.username = this.trim(data.username); if (!this.validateUsername(message.username)) { reject('Error: invalid username in "login" function'); } /* handle, if set, password value */ if (data.password) { if (!this.validatePassword(data.password)) { reject('Error: invalid password in "login" function'); } message.password = data.password; } /* handle, if set, image URL */ if (data.image) { if (!this.validateUrl(data.image)) { reject('Error: invalid image in "login" function'); } message.image = data.image; } message.type = this.postMessageEvents.LOGIN; message.hash = data.hash; message.forceLogin = data.forceLogin; /* keep sending the data to the chat until the chat responds */ const intervalHandle = setInterval(() => { this.postMessage(message); }, 1000); window.addEventListener('message', function handlePostMessage(event) { /* validates the origin to be from a chat */ if (!this.validateChatOrigin(event.origin)) { console.log('Error: invalid origin in "login" function'); } if (typeof event.data !== 'object') { console.log(`Error: invalid data received in RumbleTalk SDK: ${event.data}`); } /* different chat callback */ if (event.data.hash !== data.hash) { reject('Error: chat hash mismatch'); } /* validate that the message is of a successful login of the specific chat */ if (event.data.type === this.postMessageEvents.LOGIN_SUCCESS || event.data.type === this.postMessageEvents.LOGIN_ALREADY_LOGGED_IN) { clearInterval(intervalHandle); window.removeEventListener('message', handlePostMessage); resolve({ status: event.data.type, message: event.data.type === this.postMessageEvents.LOGIN_SUCCESS ? 'success' : 'already logged in', }); } }.bind(this), false); }); } logout(data) { const message = { type: this.postMessageEvents.LOGOUT, hash: data.hash, }; if (data.userId) { message.userId = data.userId; } if (data.username) { message.username = data.username; } this.postMessage(message); } logoutCB(data) { const intervalHandle = setInterval(() => { this.postMessage({ type: this.postMessageEvents.LOGOUT_CB }); }, 1000); window.addEventListener('message', (event) => { /* validates the origin to be from a chat */ if (!this.validateChatOrigin(event.origin)) { return; } /* expecting an object */ if (typeof event.data !== 'object') { return; } /* different chat callback */ if (event.data.hash !== data.hash) { return; } /* callback registered */ if (event.data.type === this.postMessageEvents.LOGOUT_CB_RECEIVED) { clearInterval(intervalHandle); return; } /* validate event type */ if (event.data.type !== this.postMessageEvents.LOGOUT_CB) { return; } data.callback(event.data.reason); }, false); } openPrivateChat(data) { const message = { type: this.postMessageEvents.OPEN_PRIVATE_CHAT, hash: data.hash, }; if (data.userId) { message.userId = data.userId; } if (data.username) { message.username = data.username; } this.postMessage(message); } postMessage(data) { try { const target = this.iframe instanceof HTMLIFrameElement ? this.iframe.contentWindow : this.iframe; target.postMessage(data, `https://${this.server}`); } catch (error) { console.log(error.name, error.message); } } trim(str) { return str.replace(/^\s+|\s+$/g, ''); } validateUsername(username) { return !/^-?\d+$/.test(username) && username.length < 64; } validatePassword(password) { return 0 < password.length && password.length < 51; } validateUrl(url) { return /(https?:)?\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/.test(url); } /** * checks if the given origin is of a chat service * @param origin - the URL of the origin * returns boolean */ validateChatOrigin(origin) { return /^https:\/\/.+\.rumbletalk\.(net|com)(:4433)?$/.test(origin); } } NgxRumbletalkService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.1.4", ngImport: i0, type: NgxRumbletalkService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); NgxRumbletalkService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.1.4", ngImport: i0, type: NgxRumbletalkService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.1.4", ngImport: i0, type: NgxRumbletalkService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: function () { return [{ type: i1.HttpClient }]; } }); const protocol = 'https://'; const baseWebUrl = 'https://rumbletalk.com/'; const serviceRelativeUrl = 'client/service.php?hash='; let server; let messageInterval; class NgxRumbletalkComponent { constructor(service) { this.service = service; this.floating = false; this.side = 'right'; this.image = 'https://d1pfint8izqszg.cloudfront.net/images/toolbar/toolbar.png'; this.counter = 'false'; this.cdn = 'https://d1pfint8izqszg.cloudfront.net/'; this.counterTop = 14; this.counterLeft = 23; } ngOnInit() { const ua = navigator.userAgent.toUpperCase(); this.mobile = ua.indexOf('MOBILE') !== -1 || ua.indexOf('ANDROID') !== -1 || ua.indexOf('IOS') !== -1; if (this.floating) { if (!this.width) { this.width = 700; } if (!this.height) { this.height = 500; } if (this.side !== 'right' && this.side !== 'left') { this.side = 'right'; } if (this.counter !== 'false') { const counterArr = this.counter.split(':'); const counterTop = Number(counterArr[0]); const counterLeft = Number(counterArr[1]); if (!isNaN(counterTop)) { this.counterTop = counterTop; } if (!isNaN(counterLeft)) { this.counterLeft = counterLeft; } } } this.loadIframe(); } ngOnChanges(changes) { const hashVal = changes['hash']; if (hashVal.currentValue !== hashVal.previousValue) { this.reload(); } } ngOnDestroy() { clearInterval(messageInterval); } /** * Loads the iframe */ loadIframe() { this.service.address(this.hash).subscribe((address) => { server = address; if (!this.mobile) { this.iframeElement.nativeElement.src = `https://${address}/${this.hash}/`; } this.addListeners(); }); } /** * add the event listeners based on the embed type and device */ addListeners() { window.addEventListener('message', this.info.bind(this), false); if (!this.mobile && this.floating) { window.addEventListener('keyup', this.escClose.bind(this), false); } } /** * hides the chat when the "Esc" key is clicked * @param {KeyboardEvent} event - the initiating event */ escClose(event) { if (event.key === 'Escape') { this.toggleFloatingChat(null, true); } } /** * handles postMessage requests * @param event - the event object */ info(event) { if (isFinite(event.data)) { clearInterval(messageInterval); if (this.counter !== 'false') { this.counterElement.nativeElement.innerHTML = event.data.toString(); } } else if (typeof event.data === 'object') { if (event.data.reload) { this.reload(); } } } /** * reloads the iframe (or parent page) in case of a server request */ reload() { const url = `${baseWebUrl}${serviceRelativeUrl}${this.hash}`; this.service.reload(url).subscribe((res) => { if (res.status) { server = res.address; const address = `${protocol}${server}/${this.hash}/`; if (this.iframeElement.nativeElement instanceof HTMLIFrameElement) { this.iframeElement.nativeElement.src = address; } else { this.iframeElement.nativeElement.location.href = address; } } }, (ignore) => { }); } /** * starts [repeatedly] trying to connect to the chat using postMessage */ instantiateQuery() { messageInterval = setInterval(this.query.bind(this), 1000); } /** * instantiate a postppMessage connection with the chat */ query() { try { let target; let origin; if (this.iframeElement.nativeElement instanceof HTMLIFrameElement) { target = this.iframeElement.nativeElement.contentWindow; origin = protocol + server; } else { target = this.iframeElement.nativeElement; origin = baseWebUrl; } target.postMessage('toolbar', origin); } catch (ignore) { } } /** * checks if the given origin is of a chat service * @param origin - the URL of the origin * returns boolean */ validateOrigin(origin) { return /^https:\/\/.+\.rumbletalk\.(net|com)(:4433)?$/.test(origin); } handleImageLoad(event) { /* image element */ const target = event.target; /* parent div */ const parent = event.currentTarget.parentNode; /* match the dimensions of the image and the wrapping div */ parent.style.height = `${target.height}px`; parent.style.width = `${target.width}px`; /* place the chat div right above the image */ if (!this.mobile) { this.chatDivElement.nativeElement.style.bottom = `${target.height}px`; } } /** * hides or shows the floating chat * @param boolean [esc] - if set to true, will force hide */ toggleFloatingChat(event = null, esc = false) { if (event) { event.stopPropagation(); } const chatDiv = this.chatDivElement.nativeElement; if (chatDiv.classList.contains('chat-div-in')) { chatDiv.classList.remove('chat-div-in'); chatDiv.classList.add('chat-div-out'); chatDiv.style.overflow = 'hidden'; } else { if (esc) { return; } chatDiv.classList.remove('chat-div-out'); chatDiv.classList.add('chat-div-in'); chatDiv.style.overflow = 'visible'; } } /** * attaches the open chat event to the given target * @param Element target */ openChat() { const link = `${baseWebUrl}client/chat.php?${this.hash}`; let iframeInterval; const iframe = this.iframeElement; if (iframe) { iframe.nativeElement.focus(); } else { const tempIframe = window.open(link); iframeInterval = setInterval(() => { if (tempIframe.closed) { clearInterval(iframeInterval); } }, 100); } } handleIframeLoad() { if (this.iframeElement && this.iframeElement.nativeElement) { this.service.iframe = this.iframeElement.nativeElement; this.service.server = server; this.service.handleResolve(); this.instantiateQuery(); } } handleIframeError() { this.service.handleReject('Iframe could not be loaded'); } } NgxRumbletalkComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.1.4", ngImport: i0, type: NgxRumbletalkComponent, deps: [{ token: NgxRumbletalkService }], target: i0.ɵɵFactoryTarget.Component }); NgxRumbletalkComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.1.4", type: NgxRumbletalkComponent, selector: "ngx-rumbletalk", inputs: { hash: "hash", width: "width", height: "height", floating: "floating", side: "side", image: "image", counter: "counter" }, viewQueries: [{ propertyName: "iframeElement", first: true, predicate: ["iframe"], descendants: true }, { propertyName: "chatDivElement", first: true, predicate: ["chatDiv"], descendants: true }, { propertyName: "counterElement", first: true, predicate: ["counter"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"rumbletalk-embed\" [style.width.px]=\"width\" [style.height.px]=\"height\" *ngIf=\"!floating\">\n <iframe\n #iframe\n frameBorder=\"0\"\n allow=\"microphone; camera\"\n allowtransparency=\"true\"\n (load)=\"handleIframeLoad()\"\n ></iframe>\n</div>\n\n<div\n class=\"rumbletalk-floating\"\n [ngClass]=\"'rumbletalk-floating-' + side\"\n (click)=\"mobile ? openChat() : toggleFloatingChat()\"\n *ngIf=\"floating\"\n>\n <img\n alt=\"Click to join the conversation\"\n title=\"Click to join the conversation\"\n [src]=\"image\"\n [ngClass]=\"side\"\n (load)=\"handleImageLoad($event)\"\n />\n\n <div\n #counter\n class=\"counter-div\"\n [ngStyle]=\"{ top: counterTop + 'px', left: counterLeft + 'px' }\"\n *ngIf=\"counter !== 'false'\"\n >\n <img alt=\"loading\" [src]=\"cdn + 'images/toolbar/mini_wait.gif'\" *ngIf=\"!mobile\" />\n </div>\n\n <div #chatDiv [ngClass]=\"'chat-div chat-div-out chat-div-' + side\" *ngIf=\"!mobile\">\n <img\n class=\"close-button\"\n alt=\"close\"\n [src]=\"cdn + 'images/c.png'\"\n [ngStyle]=\"side === 'right' ? { left: '-8px' } : { right: '-8px' }\"\n (click)=\"toggleFloatingChat($event)\"\n />\n\n <iframe\n #iframe\n frameBorder=\"0\"\n allow=\"microphone; camera\"\n allowtransparency=\"true\"\n [ngStyle]=\"{\n height: height ? height + 'px' : '500px',\n width: width ? width + 'px' : '700px'\n }\"\n (load)=\"handleIframeLoad()\"\n (error)=\"handleIframeError()\"\n ></iframe>\n </div>\n</div>\n", styles: [".rumbletalk-embed{overflow:hidden}iframe{overflow:hidden;background-color:transparent;height:100%;width:100%}.rumbletalk-floating{position:fixed;bottom:5px;z-index:2147483647;cursor:pointer}.rumbletalk-floating-left{left:5px}.rumbletalk-floating-right{right:5px}.rumbletalk-floating>img{max-width:none;position:absolute;bottom:0}.rumbletalk-floating>img.left{left:0}.rumbletalk-floating>img.right{right:0}.counter-div{position:absolute;width:28px;text-align:center;font:700 12px arial;color:#000;-webkit-user-select:none;user-select:none}.chat-div{position:absolute;overflow:hidden;bottom:75px;padding:0;transition:transform .1s linear;transform-style:preserve-3D}.chat-div-left{left:0;transform-origin:bottom left}.chat-div-right{right:0;transform-origin:bottom right}.chat-div-out{transform:scale(0)}.chat-div-in{transform:scale(1)}.close-button{cursor:pointer;position:absolute;top:-9px;z-index:1000}.rumbletalk-embed-image{text-align:center}.rumbletalk-embed-image img{cursor:pointer}\n"], dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.1.4", ngImport: i0, type: NgxRumbletalkComponent, decorators: [{ type: Component, args: [{ selector: 'ngx-rumbletalk', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"rumbletalk-embed\" [style.width.px]=\"width\" [style.height.px]=\"height\" *ngIf=\"!floating\">\n <iframe\n #iframe\n frameBorder=\"0\"\n allow=\"microphone; camera\"\n allowtransparency=\"true\"\n (load)=\"handleIframeLoad()\"\n ></iframe>\n</div>\n\n<div\n class=\"rumbletalk-floating\"\n [ngClass]=\"'rumbletalk-floating-' + side\"\n (click)=\"mobile ? openChat() : toggleFloatingChat()\"\n *ngIf=\"floating\"\n>\n <img\n alt=\"Click to join the conversation\"\n title=\"Click to join the conversation\"\n [src]=\"image\"\n [ngClass]=\"side\"\n (load)=\"handleImageLoad($event)\"\n />\n\n <div\n #counter\n class=\"counter-div\"\n [ngStyle]=\"{ top: counterTop + 'px', left: counterLeft + 'px' }\"\n *ngIf=\"counter !== 'false'\"\n >\n <img alt=\"loading\" [src]=\"cdn + 'images/toolbar/mini_wait.gif'\" *ngIf=\"!mobile\" />\n </div>\n\n <div #chatDiv [ngClass]=\"'chat-div chat-div-out chat-div-' + side\" *ngIf=\"!mobile\">\n <img\n class=\"close-button\"\n alt=\"close\"\n [src]=\"cdn + 'images/c.png'\"\n [ngStyle]=\"side === 'right' ? { left: '-8px' } : { right: '-8px' }\"\n (click)=\"toggleFloatingChat($event)\"\n />\n\n <iframe\n #iframe\n frameBorder=\"0\"\n allow=\"microphone; camera\"\n allowtransparency=\"true\"\n [ngStyle]=\"{\n height: height ? height + 'px' : '500px',\n width: width ? width + 'px' : '700px'\n }\"\n (load)=\"handleIframeLoad()\"\n (error)=\"handleIframeError()\"\n ></iframe>\n </div>\n</div>\n", styles: [".rumbletalk-embed{overflow:hidden}iframe{overflow:hidden;background-color:transparent;height:100%;width:100%}.rumbletalk-floating{position:fixed;bottom:5px;z-index:2147483647;cursor:pointer}.rumbletalk-floating-left{left:5px}.rumbletalk-floating-right{right:5px}.rumbletalk-floating>img{max-width:none;position:absolute;bottom:0}.rumbletalk-floating>img.left{left:0}.rumbletalk-floating>img.right{right:0}.counter-div{position:absolute;width:28px;text-align:center;font:700 12px arial;color:#000;-webkit-user-select:none;user-select:none}.chat-div{position:absolute;overflow:hidden;bottom:75px;padding:0;transition:transform .1s linear;transform-style:preserve-3D}.chat-div-left{left:0;transform-origin:bottom left}.chat-div-right{right:0;transform-origin:bottom right}.chat-div-out{transform:scale(0)}.chat-div-in{transform:scale(1)}.close-button{cursor:pointer;position:absolute;top:-9px;z-index:1000}.rumbletalk-embed-image{text-align:center}.rumbletalk-embed-image img{cursor:pointer}\n"] }] }], ctorParameters: function () { return [{ type: NgxRumbletalkService }]; }, propDecorators: { iframeElement: [{ type: ViewChild, args: ['iframe'] }], chatDivElement: [{ type: ViewChild, args: ['chatDiv'] }], counterElement: [{ type: ViewChild, args: ['counter'] }], hash: [{ type: Input }], width: [{ type: Input }], height: [{ type: Input }], floating: [{ type: Input }], side: [{ type: Input }], image: [{ type: Input }], counter: [{ type: Input }] } }); class NgxRumbletalkModule { } NgxRumbletalkModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.1.4", ngImport: i0, type: NgxRumbletalkModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); NgxRumbletalkModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.1.4", ngImport: i0, type: NgxRumbletalkModule, declarations: [NgxRumbletalkComponent], imports: [FormsModule, CommonModule, HttpClientModule], exports: [NgxRumbletalkComponent] }); NgxRumbletalkModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.1.4", ngImport: i0, type: NgxRumbletalkModule, providers: [NgxRumbletalkService], imports: [FormsModule, CommonModule, HttpClientModule] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.1.4", ngImport: i0, type: NgxRumbletalkModule, decorators: [{ type: NgModule, args: [{ declarations: [NgxRumbletalkComponent], imports: [FormsModule, CommonModule, HttpClientModule], exports: [NgxRumbletalkComponent], providers: [NgxRumbletalkService], }] }] }); /* * Public API Surface of ngx-rumbletalk */ /** * Generated bundle index. Do not edit. */ export { NgxRumbletalkComponent, NgxRumbletalkModule, NgxRumbletalkService }; //# sourceMappingURL=ngx-rumbletalk.mjs.map