mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-08-31 14:12:08 +00:00
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:
parent
e8274c9d87
commit
7a0af05ac4
@ -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
|
||||
}
|
||||
|
156
src/api/dimension/DimensionStickerService.ts
Normal file
156
src/api/dimension/DimensionStickerService.ts
Normal 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,
|
||||
},
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
24
src/db/migrations/20180512232045-AddUserStickerPacks.ts
Normal file
24
src/db/migrations/20180512232045-AddUserStickerPacks.ts
Normal 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");
|
||||
}
|
||||
}
|
26
src/db/models/UserStickerPack.ts
Normal file
26
src/db/models/UserStickerPack.ts
Normal 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;
|
||||
}
|
@ -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
|
||||
|
@ -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"},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
25
web/app/configs/stickerpicker/stickerpicker.component.html
Normal file
25
web/app/configs/stickerpicker/stickerpicker.component.html
Normal 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>
|
38
web/app/configs/stickerpicker/stickerpicker.component.scss
Normal file
38
web/app/configs/stickerpicker/stickerpicker.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
web/app/configs/stickerpicker/stickerpicker.component.ts
Normal file
52
web/app/configs/stickerpicker/stickerpicker.component.ts
Normal 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");
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
19
web/app/shared/services/integrations/sticker-api.service.ts
Normal file
19
web/app/shared/services/integrations/sticker-api.service.ts
Normal 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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user