Add a sticker picker

The useful bit for sending stickers. Implements the rest of #156
This commit is contained in:
Travis Ralston 2018-05-13 22:32:13 -06:00
parent d2c672cf00
commit 6c4e8f75d4
13 changed files with 311 additions and 31 deletions

View File

@ -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
],

View File

@ -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},
]
},
];

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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 {

View File

@ -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 {
/**

View File

@ -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<WidgetsResponse> {
public getWidgets(roomId?: string): Promise<WidgetsResponse> {
return this.callAction("get_widgets", {
room_id: roomId
});
@ -62,10 +62,32 @@ export class ScalarClientApiService {
});
}
public deleteWidget(roomId: string, widget: EditableWidget): Promise<ScalarSuccessResponse> {
public setUserWidget(widget: EditableWidget): Promise<ScalarSuccessResponse> {
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<ScalarSuccessResponse> {
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<ScalarSuccessResponse> {
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: ""
});

View File

@ -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<ScalarToWidgetRequest> = 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;
}

View File

@ -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);
}

View File

@ -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;

View File

@ -0,0 +1,26 @@
<div class="wrapper">
<div class="control-page" *ngIf="isLoading || authError">
<div class="loading-badge" *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div class="auth-error" *ngIf="!isLoading && authError">
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.
</div>
</div>
<div class="sticker-picker" *ngIf="!isLoading && !authError">
<div class="sticker-pack" *ngFor="let pack of packs trackById">
<div class="header">
<span class="title">{{ pack.displayName }}</span>
<span class="license"><a [href]="pack.license.urlPath">{{ pack.license.name }}</a></span>
<span class="author" *ngIf="pack.author.type !== 'none'"><a [href]="pack.author.reference">{{ pack.author.name }}</a> |&nbsp;</span>
</div>
<div class="stickers">
<div class="sticker" *ngFor="let sticker of pack.stickers trackById" (click)="sendSticker(sticker, pack)">
<img [src]="getThumbnailUrl(sticker.thumbnail.mxc, 48, 48)" width="48" height="48" class="image" ngbTooltip="{{ sticker.name }}" placement="bottom" />
</div>
</div>
</div>
</div>
</div>

View File

@ -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);
}
}
}
}

View File

@ -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 ((<any>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);
}
}