Implement basic postMessage widget API components

This implements a 'screenshot' button on the generic widget wrapper, although there's several bugs with the current Riot implementation that prevent it from working and that is why it is commented out.

Fixes #155. Even if it doesn't work.
This commit is contained in:
Travis Ralston 2018-05-11 21:58:10 -06:00
parent d6b4645cb9
commit 17656e8cf7
8 changed files with 151 additions and 3 deletions

6
package-lock.json generated
View file

@ -2268,6 +2268,12 @@
}
}
},
"dom-to-image": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/dom-to-image/-/dom-to-image-2.6.0.tgz",
"integrity": "sha1-ilA2CAiMh7HCL5A0rgMuGJiVWGc=",
"dev": true
},
"domain-browser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",

View file

@ -73,6 +73,7 @@
"core-js": "^2.5.3",
"css-loader": "^0.28.11",
"cssnano": "^3.10.0",
"dom-to-image": "^2.6.0",
"embed-video": "^2.0.0",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.11",

View file

@ -70,6 +70,7 @@ import { AdminIrcBridgeNetworksComponent } from "./admin/bridges/irc/networks/ne
import { AdminIrcBridgeAddSelfhostedComponent } from "./admin/bridges/irc/add-selfhosted/add-selfhosted.component";
import { IrcBridgeConfigComponent } from "./configs/bridge/irc/irc.bridge.component";
import { IrcApiService } from "./shared/services/integrations/irc-api.service";
import { ScreenshotCapableDirective } from "./shared/directives/screenshot-capable.directive";
@NgModule({
imports: [
@ -132,6 +133,7 @@ import { IrcApiService } from "./shared/services/integrations/irc-api.service";
AdminIrcBridgeNetworksComponent,
AdminIrcBridgeAddSelfhostedComponent,
IrcBridgeConfigComponent,
ScreenshotCapableDirective,
// Vendor
],

View file

@ -0,0 +1,36 @@
import { Directive, ElementRef, OnDestroy, OnInit } from "@angular/core";
import { Subscription } from "rxjs/Subscription";
import { ScalarWidgetApi } from "../services/scalar/scalar-widget.api";
import { ScalarToWidgetRequest } from "../models/scalar-widget-actions";
import * as domtoimage from "dom-to-image";
@Directive({
selector: "[myScreenshotCapable]",
})
export class ScreenshotCapableDirective implements OnInit, OnDestroy {
private widgetApiSubscription: Subscription;
constructor(private el: ElementRef) {
}
public ngOnInit() {
this.widgetApiSubscription = ScalarWidgetApi.requestReceived.subscribe(request => {
if (request.action === "screenshot") this.takeScreenshot(request);
})
}
public ngOnDestroy() {
if (this.widgetApiSubscription) this.widgetApiSubscription.unsubscribe();
}
private takeScreenshot(request: ScalarToWidgetRequest) {
domtoimage.toBlob(this.el.nativeElement).then(b => {
ScalarWidgetApi.replyScreenshot(request, b);
}).catch(error => {
console.error(error);
ScalarWidgetApi.replyError(request, error, "Failed to take screenshot");
});
}
}

View file

@ -0,0 +1,7 @@
export interface ScalarToWidgetRequest {
api: "to_widget";
action: "supported_api_versions" | "screenshot" | "capabilities" | "send_event" | "visibility_change" | string;
requestId: string;
widgetId: string;
data?: any;
}

View file

@ -0,0 +1,74 @@
import { ScalarToWidgetRequest } from "../../models/scalar-widget-actions";
import { ReplaySubject } from "rxjs/ReplaySubject";
import { Subject } from "rxjs/Subject";
export class ScalarWidgetApi {
public static requestReceived: Subject<ScalarToWidgetRequest> = new ReplaySubject();
private static widgetId: string;
private constructor() {
}
public static setWidgetId(id: string) {
ScalarWidgetApi.widgetId = id;
}
public static test() {
ScalarWidgetApi.callAction(null, null);
}
public static replyScreenshot(request: ScalarToWidgetRequest, data: Blob): void {
ScalarWidgetApi.replyEvent(request, {screenshot: data});
}
public static replySupportedVersions(request: ScalarToWidgetRequest, versions: string[]): void {
ScalarWidgetApi.replyEvent(request, {supported_versions: versions});
}
public static replyCapabilities(request: ScalarToWidgetRequest,
capabilities: (
"m.text" | "m.image" | "m.sticker" | "m.capability.screenshot" | "m.capability.request_data" |
"m.capability.request_messages" | "m.capability.room_membership" | string)[]
): void {
ScalarWidgetApi.replyEvent(request, {capabilities: capabilities});
}
public static replyError(request: ScalarToWidgetRequest, error: Error, message: string = null): void {
ScalarWidgetApi.replyEvent(request, {error: {message: message || error.message, error: error}});
}
private static callAction(action, payload) {
if (!window.opener) {
return;
}
let request = JSON.parse(JSON.stringify(payload));
request["api"] = "fromWidget";
request["widgetId"] = ScalarWidgetApi.widgetId;
request["action"] = action;
window.opener.postMessage(request, "*");
}
private static replyEvent(request: ScalarToWidgetRequest, payload) {
if (!window.opener) {
return;
}
let response = JSON.parse(JSON.stringify(payload));
let requestClone = JSON.parse(JSON.stringify(request));
requestClone["response"] = response;
window.opener.postMessage(requestClone, "*");
}
}
// Register the event listener here to ensure it gets created
window.addEventListener("message", event => {
if (!event.data) return;
if (event.data.api === "toWidget" && event.data.action) {
ScalarWidgetApi.requestReceived.next(event.data);
return;
}
});

View file

@ -8,6 +8,6 @@
<h4>Sorry, this content cannot be embedded</h4>
</div>
</div>
<iframe [src]="embedUrl" *ngIf="!isLoading && canEmbed" frameborder="0" allowfullscreen></iframe>
<iframe [src]="embedUrl" *ngIf="!isLoading && canEmbed" frameborder="0" allowfullscreen myScreenshotCapable=""></iframe>
<my-fullscreen-button *ngIf="!isLoading && canEmbed" class="toggleFullscreen"></my-fullscreen-button>
</div>

View file

@ -1,22 +1,29 @@
import { Component } from "@angular/core";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
import { WidgetApiService } from "../../shared/services/integrations/widget-api.service";
import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api";
import { Subscription } from "rxjs/Subscription";
import { ScalarToWidgetRequest } from "../../shared/models/scalar-widget-actions";
@Component({
selector: "my-generic-widget-wrapper",
templateUrl: "generic.component.html",
styleUrls: ["generic.component.scss"],
})
export class GenericWidgetWrapperComponent {
export class GenericWidgetWrapperComponent implements OnInit, OnDestroy {
public isLoading = true;
public canEmbed = false;
public embedUrl: SafeUrl = null;
private widgetApiSubscription: Subscription;
constructor(widgetApi: WidgetApiService, activatedRoute: ActivatedRoute, sanitizer: DomSanitizer) {
let params: any = activatedRoute.snapshot.queryParams;
ScalarWidgetApi.setWidgetId("test");
widgetApi.isEmbeddable(params.url).then(result => {
this.canEmbed = result.canEmbed;
this.isLoading = false;
@ -28,4 +35,19 @@ export class GenericWidgetWrapperComponent {
});
}
public ngOnInit() {
this.widgetApiSubscription = ScalarWidgetApi.requestReceived.subscribe(e => this.onWidgetRequest(e));
}
public ngOnDestroy() {
if (this.widgetApiSubscription) this.widgetApiSubscription.unsubscribe();
}
private onWidgetRequest(request: ScalarToWidgetRequest) {
if (request.action === "capabilities") {
// Taking of the screenshot is handled elsewhere
// TODO: Re-enable screenshots via a configuration flag when Riot has better support for them (and move it to an abstract class)
ScalarWidgetApi.replyCapabilities(request, [/*"m.capability.screenshot"*/]);
}
}
}