From 6c4e8f75d4983752ba00978ee228ca70f275be2f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 13 May 2018 22:32:13 -0600 Subject: [PATCH] Add a sticker picker The useful bit for sending stickers. Implements the rest of #156 --- web/app/app.module.ts | 2 + web/app/app.routing.ts | 2 + .../stickerpicker/stickerpicker.component.ts | 37 ++++++- .../shared/models/scalar-widget-actions.ts | 2 +- .../shared/models/server-client-responses.ts | 26 ++--- web/app/shared/models/widget.ts | 1 + .../scalar/scalar-client-api.service.ts | 30 +++++- .../services/scalar/scalar-widget.api.ts | 44 +++++++-- web/app/widget-wrappers/capable-widget.ts | 2 + .../generic/generic.component.ts | 3 - .../sticker-picker.component.html | 26 +++++ .../sticker-picker.component.scss | 69 +++++++++++++ .../sticker-picker.component.ts | 98 +++++++++++++++++++ 13 files changed, 311 insertions(+), 31 deletions(-) create mode 100644 web/app/widget-wrappers/sticker-picker/sticker-picker.component.html create mode 100644 web/app/widget-wrappers/sticker-picker/sticker-picker.component.scss create mode 100644 web/app/widget-wrappers/sticker-picker/sticker-picker.component.ts diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 6dac63c..6f6451f 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -77,6 +77,7 @@ import { AdminStickerPackPreviewComponent } from "./admin/sticker-packs/preview/ import { MediaService } from "./shared/services/media.service"; import { StickerApiService } from "./shared/services/integrations/sticker-api.service"; import { StickerpickerComponent } from "./configs/stickerpicker/stickerpicker.component"; +import { StickerPickerWidgetWrapperComponent } from "./widget-wrappers/sticker-picker/sticker-picker.component"; @NgModule({ imports: [ @@ -143,6 +144,7 @@ import { StickerpickerComponent } from "./configs/stickerpicker/stickerpicker.co AdminStickerPacksComponent, AdminStickerPackPreviewComponent, StickerpickerComponent, + StickerPickerWidgetWrapperComponent, // Vendor ], diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index 4c86546..b46e185 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -26,6 +26,7 @@ import { AdminIrcBridgeComponent } from "./admin/bridges/irc/irc.component"; import { IrcBridgeConfigComponent } from "./configs/bridge/irc/irc.bridge.component"; import { AdminStickerPacksComponent } from "./admin/sticker-packs/sticker-packs.component"; import { StickerpickerComponent } from "./configs/stickerpicker/stickerpicker.component"; +import { StickerPickerWidgetWrapperComponent } from "./widget-wrappers/sticker-picker/sticker-picker.component"; const routes: Routes = [ {path: "", component: HomeComponent}, @@ -179,6 +180,7 @@ const routes: Routes = [ {path: "video", component: VideoWidgetWrapperComponent}, {path: "jitsi", component: JitsiWidgetWrapperComponent}, {path: "gcal", component: GCalWidgetWrapperComponent}, + {path: "stickerpicker", component: StickerPickerWidgetWrapperComponent}, ] }, ]; diff --git a/web/app/configs/stickerpicker/stickerpicker.component.ts b/web/app/configs/stickerpicker/stickerpicker.component.ts index 9a04568..9983de8 100644 --- a/web/app/configs/stickerpicker/stickerpicker.component.ts +++ b/web/app/configs/stickerpicker/stickerpicker.component.ts @@ -3,6 +3,8 @@ import { FE_UserStickerPack } from "../../shared/models/integration"; import { StickerApiService } from "../../shared/services/integrations/sticker-api.service"; import { ToasterService } from "angular2-toaster"; import { MediaService } from "../../shared/services/media.service"; +import { ScalarClientApiService } from "../../shared/services/scalar/scalar-client-api.service"; +import { WIDGET_STICKER_PICKER } from "../../shared/models/widget"; @Component({ templateUrl: "stickerpicker.component.html", @@ -16,7 +18,9 @@ export class StickerpickerComponent implements OnInit { constructor(private stickerApi: StickerApiService, private media: MediaService, - private toaster: ToasterService) { + private scalarClient: ScalarClientApiService, + private toaster: ToasterService, + private window: Window) { this.isLoading = true; this.isUpdating = false; } @@ -41,7 +45,8 @@ export class StickerpickerComponent implements OnInit { this.stickerApi.togglePackSelection(pack.id, pack.isSelected).then(() => { this.isUpdating = false; this.toaster.pop("success", "Stickers updated"); - // TODO: Add the user widget when we have >1 sticker pack selected + + if (this.packs.filter(p => p.isSelected).length > 0) this.addWidget(); }).catch(err => { console.error(err); pack.isSelected = !pack.isSelected; // revert change @@ -49,4 +54,32 @@ export class StickerpickerComponent implements OnInit { this.toaster.pop("error", "Error updating stickers"); }); } + + private async addWidget() { + try { + const widgets = await this.scalarClient.getWidgets(); + const stickerPicker = widgets.response.find(w => w.content && w.content.type === "m.stickerpicker"); + if (stickerPicker && !stickerPicker.content.data.dimension) { + console.log("Deleting non-Dimension sticker picker"); + await this.scalarClient.deleteUserWidget(stickerPicker); + } + + if (stickerPicker) return; // already have a widget + + console.log("Adding Dimension sticker picker"); + await this.scalarClient.setUserWidget({ + id: "dimension-stickerpicker-" + (new Date().getTime()), + type: WIDGET_STICKER_PICKER[0], + url: this.window.location.origin + "/widgets/stickerpicker", + data: { + dimension: { + wrapperId: "stickerpicker", + }, + }, + }); + } catch (e) { + console.error("Failed to check for Dimension sticker picker"); + console.error(e); + } + } } \ No newline at end of file diff --git a/web/app/shared/models/scalar-widget-actions.ts b/web/app/shared/models/scalar-widget-actions.ts index 39e0c8b..7764014 100644 --- a/web/app/shared/models/scalar-widget-actions.ts +++ b/web/app/shared/models/scalar-widget-actions.ts @@ -1,6 +1,6 @@ export interface ScalarToWidgetRequest { api: "to_widget"; - action: "supported_api_versions" | "screenshot" | "capabilities" | "send_event" | "visibility_change" | string; + action: "supported_api_versions" | "screenshot" | "capabilities" | "send_event" | "visibility" | string; requestId: string; widgetId: string; data?: any; diff --git a/web/app/shared/models/server-client-responses.ts b/web/app/shared/models/server-client-responses.ts index 1b5a84f..929257e 100644 --- a/web/app/shared/models/server-client-responses.ts +++ b/web/app/shared/models/server-client-responses.ts @@ -33,18 +33,20 @@ export interface JoinRuleStateResponse extends ScalarRoomResponse { } export interface WidgetsResponse extends ScalarRoomResponse { - response: { - type: "im.vector.modular.widgets"; - state_key: string; - sender: string; - room_id: string; - content: { - type: string; - url: string; - name?: string; - data?: any; - } - }[]; + response: ScalarWidget[]; +} + +export interface ScalarWidget { + type: "im.vector.modular.widgets"; + state_key: string; + sender: string; + room_id: string; + content: { + type: string; + url: string; + name?: string; + data?: any; + } } export interface CanSendEventResponse extends ScalarRoomResponse { diff --git a/web/app/shared/models/widget.ts b/web/app/shared/models/widget.ts index 787bd93..65f7ec8 100644 --- a/web/app/shared/models/widget.ts +++ b/web/app/shared/models/widget.ts @@ -8,6 +8,7 @@ export const WIDGET_JITSI = ["jitsi", "dimension-jitsi"]; export const WIDGET_YOUTUBE = ["youtube", "dimension-youtube"]; export const WIDGET_GRAFANA = ["grafana", "dimension-grafana"]; export const WIDGET_TWITCH = ["twitch", "dimension-twitch"]; +export const WIDGET_STICKER_PICKER = ["m.stickerpicker"]; export interface EditableWidget { /** diff --git a/web/app/shared/services/scalar/scalar-client-api.service.ts b/web/app/shared/services/scalar/scalar-client-api.service.ts index f943cac..3a3865b 100644 --- a/web/app/shared/services/scalar/scalar-client-api.service.ts +++ b/web/app/shared/services/scalar/scalar-client-api.service.ts @@ -5,7 +5,7 @@ import { JoinRuleStateResponse, MembershipStateResponse, RoomEncryptionStatusResponse, - ScalarSuccessResponse, + ScalarSuccessResponse, ScalarWidget, SetPowerLevelResponse, WidgetsResponse } from "../../models/server-client-responses"; @@ -45,7 +45,7 @@ export class ScalarClientApiService { }); } - public getWidgets(roomId: string): Promise { + public getWidgets(roomId?: string): Promise { return this.callAction("get_widgets", { room_id: roomId }); @@ -62,10 +62,32 @@ export class ScalarClientApiService { }); } - public deleteWidget(roomId: string, widget: EditableWidget): Promise { + public setUserWidget(widget: EditableWidget): Promise { + return this.callAction("set_widget", { + userWidget: true, + widget_id: widget.id, + type: widget.type, + url: widget.url, + name: widget.name, + data: widget.data + }); + } + + public deleteWidget(roomId: string, widget: EditableWidget|ScalarWidget): Promise { + const anyWidget: any = widget; return this.callAction("set_widget", { room_id: roomId, - widget_id: widget.id, + widget_id: anyWidget.id || anyWidget.state_key, + type: widget.type, // required for some reason + url: "" + }); + } + + public deleteUserWidget(widget: EditableWidget|ScalarWidget): Promise { + const anyWidget: any = widget; + return this.callAction("set_widget", { + userWidget: true, + widget_id: anyWidget.id || anyWidget.state_key, type: widget.type, // required for some reason url: "" }); diff --git a/web/app/shared/services/scalar/scalar-widget.api.ts b/web/app/shared/services/scalar/scalar-widget.api.ts index 1b62fe4..5cc01f4 100644 --- a/web/app/shared/services/scalar/scalar-widget.api.ts +++ b/web/app/shared/services/scalar/scalar-widget.api.ts @@ -1,23 +1,16 @@ import { ScalarToWidgetRequest } from "../../models/scalar-widget-actions"; import { ReplaySubject } from "rxjs/ReplaySubject"; import { Subject } from "rxjs/Subject"; +import { FE_Sticker, FE_StickerPack } from "../../models/integration"; export class ScalarWidgetApi { public static requestReceived: Subject = new ReplaySubject(); - private static widgetId: string; + public 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}); } @@ -38,6 +31,38 @@ export class ScalarWidgetApi { ScalarWidgetApi.replyEvent(request, {error: {message: message || error.message, _error: error}}); } + public static replyAcknowledge(request: ScalarToWidgetRequest): void { + ScalarWidgetApi.replyEvent(request, {success: true}); + } + + public static sendSticker(sticker: FE_Sticker, pack: FE_StickerPack): void { + ScalarWidgetApi.callAction("m.sticker", { + data: { + description: sticker.description, + content: { + url: sticker.thumbnail.mxc, + info: { + mimetype: sticker.image.mimetype, + w: sticker.thumbnail.width / 2, + h: sticker.thumbnail.height / 2, + thumbnail_url: sticker.thumbnail.mxc, + thumbnail_info: { + mimetype: sticker.image.mimetype, + w: sticker.thumbnail.width / 2, + h: sticker.thumbnail.height / 2, + }, + + // This has to be included in the info object so it makes it to the event + dimension: { + license: pack.license, + author: pack.author, + }, + }, + }, + }, + }); + } + private static callAction(action, payload) { if (!window.opener) { return; @@ -67,6 +92,7 @@ window.addEventListener("message", event => { if (!event.data) return; if (event.data.api === "toWidget" && event.data.action) { + if (event.data.widgetId && !ScalarWidgetApi.widgetId) ScalarWidgetApi.widgetId = event.data.widgetId; ScalarWidgetApi.requestReceived.next(event.data); return; } diff --git a/web/app/widget-wrappers/capable-widget.ts b/web/app/widget-wrappers/capable-widget.ts index 19bb125..eff981a 100644 --- a/web/app/widget-wrappers/capable-widget.ts +++ b/web/app/widget-wrappers/capable-widget.ts @@ -8,6 +8,7 @@ export abstract class CapableWidget implements OnInit, OnDestroy { // The capabilities we support protected supportsScreenshots = false; + protected supportsStickers = false; public ngOnInit() { this.widgetApiSubscription = ScalarWidgetApi.requestReceived.subscribe(request => { @@ -15,6 +16,7 @@ export abstract class CapableWidget implements OnInit, OnDestroy { const capabilities = []; if (this.supportsScreenshots) capabilities.push("m.capability.screenshot"); + if (this.supportsStickers) capabilities.push("m.sticker"); ScalarWidgetApi.replyCapabilities(request, capabilities); } diff --git a/web/app/widget-wrappers/generic/generic.component.ts b/web/app/widget-wrappers/generic/generic.component.ts index 613a152..64e78b6 100644 --- a/web/app/widget-wrappers/generic/generic.component.ts +++ b/web/app/widget-wrappers/generic/generic.component.ts @@ -2,7 +2,6 @@ import { Component } 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"; @Component({ selector: "my-generic-widget-wrapper", @@ -18,8 +17,6 @@ export class GenericWidgetWrapperComponent { 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; diff --git a/web/app/widget-wrappers/sticker-picker/sticker-picker.component.html b/web/app/widget-wrappers/sticker-picker/sticker-picker.component.html new file mode 100644 index 0000000..676f8e8 --- /dev/null +++ b/web/app/widget-wrappers/sticker-picker/sticker-picker.component.html @@ -0,0 +1,26 @@ +
+
+
+ +
+
+ There was a problem authenticating your use of this sticker picker. Please make sure you're using a client + that has Dimension enabled and correctly set up. +
+
+
+
+
+ {{ pack.displayName }} + {{ pack.license.name }} + {{ pack.author.name }} +
+ +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/web/app/widget-wrappers/sticker-picker/sticker-picker.component.scss b/web/app/widget-wrappers/sticker-picker/sticker-picker.component.scss new file mode 100644 index 0000000..0ec2fc0 --- /dev/null +++ b/web/app/widget-wrappers/sticker-picker/sticker-picker.component.scss @@ -0,0 +1,69 @@ +// component styles are encapsulated and only applied to their components +.control-page { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: 0; + padding: 0; + background-color: #eee; + color: #222; +} + +.loading-badge { + text-align: center; + font-size: 20px; + position: relative; + top: calc(50% - 10px); +} + +.auth-error { + text-align: center; + position: relative; + height: 300px; + top: calc(50% - 150px); + color: #bd362f; +} + +.sticker-picker { + margin: 15px 15px 30px; + + .sticker-pack { + .header { + margin-top: 15px; + margin-left: 3px; + + .title { + font-weight: 700; + color: #222222; + } + + .author, .license { + font-size: 0.6em; + font-weight: 300; + color: #b5b5b5; + margin-top: 3px; + float: right; + + a { + color: #b5b5b5; + } + } + } + + .stickers { + display: flex; + flex-wrap: wrap; + + .sticker { + padding: 5px; + margin: 2px; + cursor: pointer; + border-radius: 3px; + background-color: #fff; + box-shadow: 0 2px 6px hsla(0, 0%, 0%, 0.2); + } + } + } +} \ No newline at end of file diff --git a/web/app/widget-wrappers/sticker-picker/sticker-picker.component.ts b/web/app/widget-wrappers/sticker-picker/sticker-picker.component.ts new file mode 100644 index 0000000..1fb8a9d --- /dev/null +++ b/web/app/widget-wrappers/sticker-picker/sticker-picker.component.ts @@ -0,0 +1,98 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { CapableWidget } from "../capable-widget"; +import { Subscription } from "rxjs/Subscription"; +import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api"; +import { StickerApiService } from "../../shared/services/integrations/sticker-api.service"; +import { SessionStorage } from "../../shared/SessionStorage"; +import { ScalarServerApiService } from "../../shared/services/scalar/scalar-server-api.service"; +import { FE_Sticker, FE_UserStickerPack } from "../../shared/models/integration"; +import { MediaService } from "../../shared/services/media.service"; + +@Component({ + selector: "my-generic-widget-wrapper", + templateUrl: "sticker-picker.component.html", + styleUrls: ["sticker-picker.component.scss"], +}) +export class StickerPickerWidgetWrapperComponent extends CapableWidget implements OnInit, OnDestroy { + + public isLoading = true; + public authError = false; + public packs: FE_UserStickerPack[]; + + private stickerWidgetApiSubscription: Subscription; + + constructor(activatedRoute: ActivatedRoute, + private media: MediaService, + private scalarApi: ScalarServerApiService, + private stickerApi: StickerApiService) { + super(); + this.supportsStickers = true; + + let params: any = activatedRoute.snapshot.queryParams; + + let token = params.scalar_token; + if (!token) token = localStorage.getItem("dim-scalar-token"); + else localStorage.setItem("dim-scalar-token", token); + + if (!params.widgetId) { + console.error("No widgetId query parameter"); + this.authError = true; + } else { + ScalarWidgetApi.widgetId = params.widgetId; + } + + SessionStorage.scalarToken = token; + this.authError = !token; + } + + public ngOnInit() { + super.ngOnInit(); + this.stickerWidgetApiSubscription = ScalarWidgetApi.requestReceived.subscribe(request => { + if (request.action === "visibility") { + if ((request).visible) this.loadStickers(); + ScalarWidgetApi.replyAcknowledge(request); + } + }); + this.loadStickers(); + } + + public ngOnDestroy() { + super.ngOnDestroy(); + if (this.stickerWidgetApiSubscription) this.stickerWidgetApiSubscription.unsubscribe(); + } + + public getThumbnailUrl(mxc: string, width: number, height: number, method: "crop" | "scale" = "scale"): string { + return this.media.getThumbnailUrl(mxc, width, height, method, true); + } + + private async loadStickers() { + if (this.authError) return; // Don't bother + + if (!SessionStorage.userId) { + try { + const info = await this.scalarApi.getAccount(); + SessionStorage.userId = info.user_id; + console.log("Dimension scalar_token belongs to " + SessionStorage.userId); + } catch (e) { + console.error(e); + this.authError = true; + return; + } + } + + console.log("Attempting to load available stickers..."); + try { + const packs = await this.stickerApi.getPacks(); + this.packs = packs.filter(p => p.isSelected); + console.log("User has " + this.packs.length + "/" + packs.length + " sticker packs selected"); + this.isLoading = false; + } catch (e) { + console.error(e); + } + } + + public sendSticker(sticker: FE_Sticker, pack: FE_UserStickerPack) { + ScalarWidgetApi.sendSticker(sticker, pack); + } +}