ngx-rumbletalk
Version:
RumbleTalk Angular integration
457 lines (450 loc) • 23.3 kB
JavaScript
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