Sticker pack selection (without widget)

This is the UI where the user can pick which stickers they want. This does not add the widget yet though.

Helps towards #156
This commit is contained in:
Travis Ralston 2018-05-12 23:51:31 -06:00
parent e8274c9d87
commit 7a0af05ac4
13 changed files with 366 additions and 84 deletions

View file

@ -1,40 +1,9 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
import { AdminService } from "./AdminService";
import { Cache, CACHE_STICKERS } from "../../MemoryCache";
import StickerPack from "../../db/models/StickerPack";
import Sticker from "../../db/models/Sticker";
import { ApiError } from "../ApiError";
export interface MemoryStickerPack {
id: number;
displayName: string;
avatarUrl: string;
description: string;
isEnabled: boolean;
author: {
type: string;
name: string;
reference: string;
};
license: {
name: string;
urlPath: string;
};
stickers: {
id: number;
name: string;
description: string;
image: {
mxc: string;
mimetype: string;
};
thumbnail: {
mxc: string;
width: number;
height: number;
};
}[];
}
import { DimensionStickerService, MemoryStickerPack } from "../dimension/DimensionStickerService";
import { Cache, CACHE_STICKERS } from "../../MemoryCache";
interface SetEnabledRequest {
isEnabled: boolean;
@ -46,61 +15,11 @@ interface SetEnabledRequest {
@Path("/api/v1/dimension/admin/stickers")
export class AdminStickerService {
public static async getStickerPacks(enabledOnly: boolean = false): Promise<MemoryStickerPack[]> {
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs");
if (cachedPacks) {
if (enabledOnly) return cachedPacks.filter(p => p.isEnabled);
return cachedPacks;
}
const dbPacks = await StickerPack.findAll();
const packs: MemoryStickerPack[] = [];
for (const pack of dbPacks) {
const stickers = await Sticker.findAll({where: {packId: pack.id}});
packs.push(<MemoryStickerPack>{
id: pack.id,
displayName: pack.name,
avatarUrl: pack.avatarUrl,
description: pack.description,
isEnabled: pack.isEnabled,
author: {
type: pack.authorType,
name: pack.authorName,
reference: pack.authorReference,
},
license: {
name: pack.license,
urlPath: pack.licensePath,
},
stickers: stickers.map(s => {
return {
id: s.id,
name: s.name,
description: s.description,
image: {
mxc: s.imageMxc,
mimetype: s.mimetype,
},
thumbnail: {
mxc: s.thumbnailMxc,
width: s.thumbnailWidth,
height: s.thumbnailHeight,
},
}
}),
});
}
Cache.for(CACHE_STICKERS).put("packs", packs);
if (enabledOnly) return packs.filter(p => p.isEnabled);
return packs;
}
@GET
@Path("packs")
public async getStickerPacks(@QueryParam("scalar_token") scalarToken: string): Promise<MemoryStickerPack[]> {
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
return await AdminStickerService.getStickerPacks();
return await DimensionStickerService.getStickerPacks(false);
}
@POST
@ -112,6 +31,7 @@ export class AdminStickerService {
pack.isEnabled = request.isEnabled;
await pack.save();
Cache.for(CACHE_STICKERS).clear();
return {}; // 200 OK
}

View file

@ -0,0 +1,156 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
import { Cache, CACHE_STICKERS } from "../../MemoryCache";
import StickerPack from "../../db/models/StickerPack";
import Sticker from "../../db/models/Sticker";
import { ScalarService } from "../scalar/ScalarService";
import UserStickerPack from "../../db/models/UserStickerPack";
import { ApiError } from "../ApiError";
export interface MemoryStickerPack {
id: number;
displayName: string;
avatarUrl: string;
description: string;
isEnabled: boolean;
author: {
type: string;
name: string;
reference: string;
};
license: {
name: string;
urlPath: string;
};
stickers: {
id: number;
name: string;
description: string;
image: {
mxc: string;
mimetype: string;
};
thumbnail: {
mxc: string;
width: number;
height: number;
};
}[];
}
export interface MemoryUserStickerPack extends MemoryStickerPack {
isSelected: boolean;
}
interface SetSelectedRequest {
isSelected: boolean;
}
/**
* API for stickers
*/
@Path("/api/v1/dimension/stickers")
export class DimensionStickerService {
public static async getStickerPacks(enabledOnly: boolean = false): Promise<MemoryStickerPack[]> {
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs");
if (cachedPacks) {
if (enabledOnly) return cachedPacks.filter(p => p.isEnabled);
return cachedPacks;
}
const dbPacks = await StickerPack.findAll();
const packs: MemoryStickerPack[] = [];
for (const pack of dbPacks) {
packs.push(await DimensionStickerService.packToMemory(pack));
}
Cache.for(CACHE_STICKERS).put("packs", packs);
if (enabledOnly) return packs.filter(p => p.isEnabled);
return packs;
}
@GET
@Path("packs")
public async getStickerPacks(@QueryParam("scalar_token") scalarToken: string): Promise<MemoryStickerPack[]> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs_" + userId);
if (cachedPacks) return cachedPacks;
const allPacks = await DimensionStickerService.getStickerPacks(true);
if (allPacks.length === 0) return []; // We can just skip the database call
const userPacks = await UserStickerPack.findAll({where: {userId: userId, isSelected: true}});
const packs: MemoryUserStickerPack[] = [];
for (const pack of allPacks) {
const userPack = userPacks.find(p => p.packId === pack.id);
const selectedPack = JSON.parse(JSON.stringify(pack));
selectedPack.isSelected = userPack ? userPack.isSelected : false;
packs.push(selectedPack);
}
Cache.for(CACHE_STICKERS).put("packs_" + userId, packs);
return packs;
}
@POST
@Path("packs/:packId/selected")
public async setPackSelected(@QueryParam("scalar_token") scalarToken: string, @PathParam("packId") packId: number, request: SetSelectedRequest): Promise<any> {
const userId = await ScalarService.getTokenOwner(scalarToken);
const pack = await StickerPack.findByPrimary(packId);
if (!pack) throw new ApiError(404, "Sticker pack not found");
let userPack = await UserStickerPack.findOne({where: {userId: userId, packId: packId}});
if (!userPack) {
userPack = await UserStickerPack.create({
packId: packId,
userId: userId,
isSelected: false,
});
}
userPack.isSelected = request.isSelected;
await userPack.save();
return {}; // 200 OK
}
private static async packToMemory(pack: StickerPack): Promise<MemoryStickerPack> {
const stickers = await Sticker.findAll({where: {packId: pack.id}});
return {
id: pack.id,
displayName: pack.name,
avatarUrl: pack.avatarUrl,
description: pack.description,
isEnabled: pack.isEnabled,
author: {
type: pack.authorType,
name: pack.authorName,
reference: pack.authorReference,
},
license: {
name: pack.license,
urlPath: pack.licensePath,
},
stickers: stickers.map(s => {
return {
id: s.id,
name: s.name,
description: s.description,
image: {
mxc: s.imageMxc,
mimetype: s.mimetype,
},
thumbnail: {
mxc: s.thumbnailMxc,
width: s.thumbnailWidth,
height: s.thumbnailHeight,
},
}
}),
};
}
}

View file

@ -20,6 +20,7 @@ import IrcBridgeRecord from "./models/IrcBridgeRecord";
import IrcBridgeNetwork from "./models/IrcBridgeNetwork";
import StickerPack from "./models/StickerPack";
import Sticker from "./models/Sticker";
import UserStickerPack from "./models/UserStickerPack";
class _DimensionStore {
private sequelize: Sequelize;
@ -51,6 +52,7 @@ class _DimensionStore {
IrcBridgeNetwork,
StickerPack,
Sticker,
UserStickerPack,
]);
}

View file

@ -0,0 +1,24 @@
import { QueryInterface } from "sequelize";
import { DataType } from "sequelize-typescript";
export default {
up: (queryInterface: QueryInterface) => {
return queryInterface.createTable("dimension_user_sticker_packs", {
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
"packId": {
type: DataType.INTEGER, allowNull: false,
references: {model: "dimension_sticker_packs", key: "id"},
onUpdate: "cascade", onDelete: "cascade",
},
"userId": {
type: DataType.STRING, allowNull: false,
references: {model: "dimension_users", key: "userId"},
onUpdate: "cascade", onDelete: "cascade",
},
"isSelected": {type: DataType.BOOLEAN, allowNull: false},
})
},
down: (queryInterface: QueryInterface) => {
return queryInterface.dropTable("dimension_user_sticker_packs");
}
}

View file

@ -0,0 +1,26 @@
import { AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript";
import StickerPack from "./StickerPack";
import User from "./User";
@Table({
tableName: "dimension_user_sticker_packs",
underscoredAll: false,
timestamps: false,
})
export default class UserStickerPack extends Model<UserStickerPack> {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@Column
@ForeignKey(() => StickerPack)
packId: number;
@Column
@ForeignKey(() => User)
userId: string;
@Column
isSelected: boolean;
}

View file

@ -75,6 +75,8 @@ import { AdminStickersApiService } from "./shared/services/admin/admin-stickers-
import { AdminStickerPacksComponent } from "./admin/sticker-packs/sticker-packs.component";
import { AdminStickerPackPreviewComponent } from "./admin/sticker-packs/preview/preview.component";
import { MediaService } from "./shared/services/media.service";
import { StickerApiService } from "./shared/services/integrations/sticker-api.service";
import { StickerpickerComponent } from "./configs/stickerpicker/stickerpicker.component";
@NgModule({
imports: [
@ -140,6 +142,7 @@ import { MediaService } from "./shared/services/media.service";
ScreenshotCapableDirective,
AdminStickerPacksComponent,
AdminStickerPackPreviewComponent,
StickerpickerComponent,
// Vendor
],
@ -158,6 +161,7 @@ import { MediaService } from "./shared/services/media.service";
IrcApiService,
AdminStickersApiService,
MediaService,
StickerApiService,
{provide: Window, useValue: window},
// Vendor

View file

@ -25,6 +25,7 @@ import { AdminBridgesComponent } from "./admin/bridges/bridges.component";
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";
const routes: Routes = [
{path: "", component: HomeComponent},
@ -164,6 +165,11 @@ const routes: Routes = [
},
],
},
{
path: "stickerpicker",
component: StickerpickerComponent,
data: {breadcrumb: "Your Sticker Packs", name: "Your Sticker Packs"},
},
],
},
{

View file

@ -0,0 +1,25 @@
<div *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div *ngIf="!isLoading">
<my-ibox title="Sticker Packs">
<div class="my-ibox-content" *ngIf="packs.length <= 0">
<h5 style="text-align: center;">Sticker packs are not enabled on this Dimension instance.</h5>
</div>
<div class="my-ibox-content" *ngIf="packs.length > 0">
<div class="pack" *ngFor="let pack of packs trackById">
<img [src]="getThumbnailUrl(pack.avatarUrl, 120, 120)" width="120" height="120"/>
<div class="caption">
<ui-switch [checked]="pack.isSelected" size="medium" [disabled]="isUpdating"
(change)="toggleSelected(pack)" class="toggle-switch"></ui-switch>
<span class="name">{{ pack.displayName }}</span>
<span class="description">{{ pack.description }}</span>
<span class="author" *ngIf="pack.author.type !== 'none'">Created by <a [href]="pack.author.reference">{{ pack.author.name }}</a> under </span>
<span class="license"><a [href]="pack.license.urlPath">{{ pack.license.name }}</a></span>
</div>
</div>
</div>
</my-ibox>
</div>

View file

@ -0,0 +1,38 @@
.pack {
display: flex;
margin: 20px;
padding: 5px;
background-color: #f6fbff;
.caption {
flex: 1;
margin-left: 20px;
padding-top: 20px;
.name {
font-size: 1.1em;
font-weight: bold;
display: block;
}
.description {
color: #7d7d7d;
display: block;
}
.toggle-switch {
margin-top: 20px;
margin-right: 10px;
float: right;
}
.author, .license {
font-size: 0.8em;
color: #7d7d7d;
a {
color: #7d7d7d;
}
}
}
}

View file

@ -0,0 +1,52 @@
import { Component, OnInit } from "@angular/core";
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";
@Component({
templateUrl: "stickerpicker.component.html",
styleUrls: ["stickerpicker.component.scss"],
})
export class StickerpickerComponent implements OnInit {
public isLoading = true;
public isUpdating = false;
public packs: FE_UserStickerPack[];
constructor(private stickerApi: StickerApiService,
private media: MediaService,
private toaster: ToasterService) {
this.isLoading = true;
this.isUpdating = false;
}
public async ngOnInit() {
try {
this.packs = await this.stickerApi.getPacks();
this.isLoading = false;
} catch (e) {
console.error(e);
this.toaster.pop("error", "Failed to load sticker packs");
}
}
public getThumbnailUrl(mxc: string, width: number, height: number, method: "crop" | "scale" = "scale"): string {
return this.media.getThumbnailUrl(mxc, width, height, method, true);
}
public toggleSelected(pack: FE_UserStickerPack) {
pack.isSelected = !pack.isSelected;
this.isUpdating = true;
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
}).catch(err => {
console.error(err);
pack.isSelected = !pack.isSelected; // revert change
this.isUpdating = false;
this.toaster.pop("error", "Error updating stickers");
});
}
}

View file

@ -195,6 +195,12 @@ export class RiotHomeComponent {
let type = null;
if (!this.requestedScreen) return;
if (this.requestedScreen === "type_m.stickerpicker") {
console.log("Intercepting config screen handling to open sticker picker config");
this.router.navigate(['riot-app', 'stickerpicker']);
return;
}
const targetIntegration = IntegrationsRegistry.getIntegrationForScreen(this.requestedScreen);
if (targetIntegration) {
category = targetIntegration.category;

View file

@ -44,6 +44,10 @@ export interface FE_StickerPack extends FE_Integration {
stickers: FE_Sticker[];
}
export interface FE_UserStickerPack extends FE_StickerPack {
isSelected: boolean;
}
export interface FE_Sticker {
id: number;
name: string;

View file

@ -0,0 +1,19 @@
import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { AuthedApi } from "../authed-api";
import { FE_UserStickerPack } from "../../models/integration";
@Injectable()
export class StickerApiService extends AuthedApi {
constructor(http: Http) {
super(http);
}
public getPacks(): Promise<FE_UserStickerPack[]> {
return this.authedGet("/api/v1/dimension/stickers/packs").map(r => r.json()).toPromise();
}
public togglePackSelection(packId: number, isSelected: boolean): Promise<any> {
return this.authedPost("/api/v1/dimension/stickers/packs/" + packId + "/selected", {isSelected: isSelected}).map(r => r.json()).toPromise();
}
}