TeaWeb/shared/js/file/Avatars.ts

165 lines
5.1 KiB
TypeScript
Raw Normal View History

import {Registry} from "../events";
import * as hex from "../crypto/hex";
export const kIPCAvatarChannel = "avatars";
export const kLoadingAvatarImage = "img/loading_image.svg";
export const kDefaultAvatarImage = "img/style/avatar.png";
export type AvatarState = "unset" | "loading" | "errored" | "loaded";
export interface AvatarStateData {
"unset": {},
"loading": {},
"errored": { message: string },
"loaded": { url: string }
}
interface AvatarEvents {
avatar_changed: {
newAvatarHash: string
},
avatar_state_changed: {
oldState: AvatarState,
newState: AvatarState,
newStateData: AvatarStateData[keyof AvatarStateData]
}
}
export abstract class ClientAvatar {
readonly events: Registry<AvatarEvents>;
readonly clientAvatarId: string; /* the base64 unique id thing from a-m */
private currentAvatarHash: string | "unknown"; /* the client avatars flag */
private state: AvatarState = "loading";
private stateData: AvatarStateData[AvatarState] = {};
loadingTimestamp: number = 0;
constructor(clientAvatarId: string) {
this.clientAvatarId = clientAvatarId;
this.events = new Registry<AvatarEvents>();
this.events.on("avatar_state_changed", event => { this.state = event.newState; this.stateData = event.newStateData; });
this.events.on("avatar_changed", event => this.currentAvatarHash = event.newAvatarHash);
}
destroy() {
this.setState("unset", {});
this.events.destroy();
}
protected setState<T extends AvatarState>(state: T, data: AvatarStateData[T], force?: boolean) {
if(this.state === state && !force)
return;
this.destroyStateData(this.state, this.stateData);
this.events.fire("avatar_state_changed", { newState: state, oldState: this.state, newStateData: data });
}
public getTypedStateData<T extends AvatarState>(state: T) : AvatarStateData[T] {
if(this.state !== state)
throw "invalid avatar state";
return this.stateData as any;
}
public setUnset() {
this.setState("unset", {});
}
public setLoading() {
this.setState("loading", {});
}
public setLoaded(data: AvatarStateData["loaded"]) {
this.setState("loaded", data);
}
public setErrored(data: AvatarStateData["errored"]) {
this.setState("errored", data);
}
async awaitLoaded() {
if(this.state !== "loading")
return;
await new Promise(resolve => this.events.on("avatar_state_changed", event => event.newState !== "loading" && resolve()));
}
getState() : AvatarState {
return this.state;
}
getStateData() : AvatarStateData[AvatarState] {
return this.stateData;
}
getAvatarHash() : string | "unknown" {
return this.currentAvatarHash;
}
getAvatarUrl() {
if(this.state === "loaded")
return this.getTypedStateData("loaded").url || kDefaultAvatarImage;
return kDefaultAvatarImage;
}
getLoadError() {
return this.getTypedStateData("errored").message;
}
protected abstract destroyStateData(state: AvatarState, data: AvatarStateData[AvatarState]);
}
export abstract class AbstractAvatarManager {
abstract resolveAvatar(clientAvatarId: string, avatarHash?: string) : ClientAvatar;
abstract resolveClientAvatar(client: { id?: number, database_id?: number, clientUniqueId: string });
}
export abstract class AbstractAvatarManagerFactory {
abstract hasManager(handlerId: string) : boolean;
abstract getManager(handlerId: string) : AbstractAvatarManager;
}
let globalAvatarManagerFactory: AbstractAvatarManagerFactory;
export function setGlobalAvatarManagerFactory(factory: AbstractAvatarManagerFactory) {
if(globalAvatarManagerFactory)
throw "global avatar manager factory has already been set";
globalAvatarManagerFactory = factory;
}
export function getGlobalAvatarManagerFactory() {
return globalAvatarManagerFactory;
}
export function uniqueId2AvatarId(unique_id: string) {
function str2ab(str) {
let buf = new ArrayBuffer(str.length); // 2 bytes for each char
let bufView = new Uint8Array(buf);
for (let i=0, strLen = str.length; i<strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
try {
let raw = atob(unique_id);
let input = hex.encode(str2ab(raw));
let result: string = "";
for(let index = 0; index < input.length; index++) {
let c = input.charAt(index);
let offset: number = 0;
if(c >= '0' && c <= '9')
offset = c.charCodeAt(0) - '0'.charCodeAt(0);
else if(c >= 'A' && c <= 'F')
offset = c.charCodeAt(0) - 'A'.charCodeAt(0) + 0x0A;
else if(c >= 'a' && c <= 'f')
offset = c.charCodeAt(0) - 'a'.charCodeAt(0) + 0x0A;
result += String.fromCharCode('a'.charCodeAt(0) + offset);
}
return result;
} catch (e) { //invalid base 64 (like music bot etc)
return undefined;
}
}