diff options
Diffstat (limited to 'FrontEnd/src/app/services')
-rw-r--r-- | FrontEnd/src/app/services/TimelinePostBuilder.ts | 116 | ||||
-rw-r--r-- | FrontEnd/src/app/services/alert.ts | 63 | ||||
-rw-r--r-- | FrontEnd/src/app/services/timeline.ts | 85 | ||||
-rw-r--r-- | FrontEnd/src/app/services/user.ts | 228 |
4 files changed, 0 insertions, 492 deletions
diff --git a/FrontEnd/src/app/services/TimelinePostBuilder.ts b/FrontEnd/src/app/services/TimelinePostBuilder.ts deleted file mode 100644 index 40279eca..00000000 --- a/FrontEnd/src/app/services/TimelinePostBuilder.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Remarkable } from "remarkable"; - -import { UiLogicError } from "@/common"; - -import { base64 } from "@/http/common"; -import { HttpTimelinePostPostRequest } from "@/http/timeline"; - -export default class TimelinePostBuilder { - private _onChange: () => void; - private _text = ""; - private _images: { file: File; url: string }[] = []; - private _md: Remarkable = new Remarkable(); - - constructor(onChange: () => void) { - this._onChange = onChange; - const oldImageRenderer = this._md.renderer.rules.image; - this._md.renderer.rules.image = (( - _t: TimelinePostBuilder - ): Remarkable.Rule<Remarkable.ImageToken, string> => - function (tokens, idx, options /*, env */) { - const i = parseInt(tokens[idx].src); - if (!isNaN(i) && i > 0 && i <= _t._images.length) { - tokens[idx].src = _t._images[i - 1].url; - } - return oldImageRenderer(tokens, idx, options); - })(this); - } - - setMarkdownText(text: string): void { - this._text = text; - this._onChange(); - } - - appendImage(file: File): void { - this._images = this._images.slice(); - this._images.push({ - file, - url: URL.createObjectURL(file), - }); - this._onChange(); - } - - moveImage(oldIndex: number, newIndex: number): void { - if (oldIndex < 0 || oldIndex >= this._images.length) { - throw new UiLogicError("Old index out of range."); - } - - if (newIndex < 0) { - newIndex = 0; - } - - if (newIndex >= this._images.length) { - newIndex = this._images.length - 1; - } - - this._images = this._images.slice(); - - const [old] = this._images.splice(oldIndex, 1); - this._images.splice(newIndex, 0, old); - - this._onChange(); - } - - deleteImage(index: number): void { - if (index < 0 || index >= this._images.length) { - throw new UiLogicError("Old index out of range."); - } - - this._images = this._images.slice(); - - URL.revokeObjectURL(this._images[index].url); - this._images.splice(index, 1); - - this._onChange(); - } - - get text(): string { - return this._text; - } - - get images(): { file: File; url: string }[] { - return this._images; - } - - get isEmpty(): boolean { - return this._text.length === 0 && this._images.length === 0; - } - - renderHtml(): string { - return this._md.render(this._text); - } - - dispose(): void { - for (const image of this._images) { - URL.revokeObjectURL(image.url); - } - this._images = []; - } - - async build(): Promise<HttpTimelinePostPostRequest["dataList"]> { - return [ - { - contentType: "text/markdown", - data: await base64(this._text), - }, - ...(await Promise.all( - this._images.map((image) => - base64(image.file).then((data) => ({ - contentType: image.file.type, - data, - })) - ) - )), - ]; - } -} diff --git a/FrontEnd/src/app/services/alert.ts b/FrontEnd/src/app/services/alert.ts deleted file mode 100644 index 48d482ea..00000000 --- a/FrontEnd/src/app/services/alert.ts +++ /dev/null @@ -1,63 +0,0 @@ -import React from "react"; -import pull from "lodash/pull"; - -import { BootstrapThemeColor, I18nText } from "@/common"; - -export interface AlertInfo { - type?: BootstrapThemeColor; - message: React.FC<unknown> | I18nText; - dismissTime?: number | "never"; -} - -export interface AlertInfoEx extends AlertInfo { - id: number; -} - -export type AlertConsumer = (alerts: AlertInfoEx) => void; - -export class AlertService { - private consumers: AlertConsumer[] = []; - private savedAlerts: AlertInfoEx[] = []; - private currentId = 1; - - private produce(alert: AlertInfoEx): void { - for (const consumer of this.consumers) { - consumer(alert); - } - } - - registerConsumer(consumer: AlertConsumer): void { - this.consumers.push(consumer); - if (this.savedAlerts.length !== 0) { - for (const alert of this.savedAlerts) { - this.produce(alert); - } - this.savedAlerts = []; - } - } - - unregisterConsumer(consumer: AlertConsumer): void { - pull(this.consumers, consumer); - } - - push(alert: AlertInfo): void { - const newAlert: AlertInfoEx = { ...alert, id: this.currentId++ }; - if (this.consumers.length === 0) { - this.savedAlerts.push(newAlert); - } else { - this.produce(newAlert); - } - } -} - -export const alertService = new AlertService(); - -export function pushAlert(alert: AlertInfo): void { - alertService.push(alert); -} - -export const kAlertHostId = "alert-host"; - -export function getAlertHost(): HTMLElement | null { - return document.getElementById(kAlertHostId); -} diff --git a/FrontEnd/src/app/services/timeline.ts b/FrontEnd/src/app/services/timeline.ts deleted file mode 100644 index d8c0ae00..00000000 --- a/FrontEnd/src/app/services/timeline.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { TimelineVisibility } from "@/http/timeline"; -import XRegExp from "xregexp"; -import { Observable } from "rxjs"; -import { HubConnectionBuilder, HubConnectionState } from "@microsoft/signalr"; - -import { getHttpToken } from "@/http/common"; - -const timelineNameReg = XRegExp("^[-_\\p{L}]*$", "u"); - -export function validateTimelineName(name: string): boolean { - return timelineNameReg.test(name); -} - -export const timelineVisibilityTooltipTranslationMap: Record< - TimelineVisibility, - string -> = { - Public: "timeline.visibilityTooltip.public", - Register: "timeline.visibilityTooltip.register", - Private: "timeline.visibilityTooltip.private", -}; - -export function getTimelinePostUpdate$( - timelineName: string -): Observable<{ update: boolean; state: HubConnectionState }> { - return new Observable((subscriber) => { - subscriber.next({ - update: false, - state: HubConnectionState.Connecting, - }); - - const token = getHttpToken(); - const connection = new HubConnectionBuilder() - .withUrl("/api/hub/timeline", { - accessTokenFactory: token == null ? undefined : () => token, - }) - .withAutomaticReconnect() - .build(); - - const handler = (tn: string): void => { - if (timelineName === tn) { - subscriber.next({ update: true, state: connection.state }); - } - }; - - connection.onclose(() => { - subscriber.next({ - update: false, - state: HubConnectionState.Disconnected, - }); - }); - - connection.onreconnecting(() => { - subscriber.next({ - update: false, - state: HubConnectionState.Reconnecting, - }); - }); - - connection.onreconnected(() => { - subscriber.next({ - update: false, - state: HubConnectionState.Connected, - }); - }); - - connection.on("OnTimelinePostChanged", handler); - - void connection.start().then(() => { - subscriber.next({ update: false, state: HubConnectionState.Connected }); - - return connection.invoke("SubscribeTimelinePostChange", timelineName); - }); - - return () => { - connection.off("OnTimelinePostChanged", handler); - - if (connection.state === HubConnectionState.Connected) { - void connection - .invoke("UnsubscribeTimelinePostChange", timelineName) - .then(() => connection.stop()); - } - }; - }); -} diff --git a/FrontEnd/src/app/services/user.ts b/FrontEnd/src/app/services/user.ts deleted file mode 100644 index 9a8e5687..00000000 --- a/FrontEnd/src/app/services/user.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { useState, useEffect } from "react"; -import { BehaviorSubject, Observable } from "rxjs"; - -import { UiLogicError } from "@/common"; - -import { HttpNetworkError, setHttpToken } from "@/http/common"; -import { - getHttpTokenClient, - HttpCreateTokenBadCredentialError, -} from "@/http/token"; -import { getHttpUserClient, HttpUser, UserPermission } from "@/http/user"; - -import { pushAlert } from "./alert"; - -interface IAuthUser extends HttpUser { - token: string; -} - -export class AuthUser implements IAuthUser { - constructor(user: HttpUser, public token: string) { - this.uniqueId = user.uniqueId; - this.username = user.username; - this.permissions = user.permissions; - this.nickname = user.nickname; - } - - uniqueId: string; - username: string; - permissions: UserPermission[]; - nickname: string; - - get hasAdministrationPermission(): boolean { - return this.permissions.length !== 0; - } - - get hasAllTimelineAdministrationPermission(): boolean { - return this.permissions.includes("AllTimelineManagement"); - } - - get hasHighlightTimelineAdministrationPermission(): boolean { - return this.permissions.includes("HighlightTimelineManagement"); - } -} - -export interface LoginCredentials { - username: string; - password: string; -} - -export class BadCredentialError { - message = "login.badCredential"; -} - -const USER_STORAGE_KEY = "currentuser"; - -export class UserService { - constructor() { - this.userSubject.subscribe((u) => { - setHttpToken(u?.token ?? null); - }); - } - - private userSubject = new BehaviorSubject<AuthUser | null | undefined>( - undefined - ); - - get user$(): Observable<AuthUser | null | undefined> { - return this.userSubject; - } - - get currentUser(): AuthUser | null | undefined { - return this.userSubject.value; - } - - async checkLoginState(): Promise<AuthUser | null> { - if (this.currentUser !== undefined) { - console.warn("Already checked user. Can't check twice."); - } - - const savedUserString = localStorage.getItem(USER_STORAGE_KEY); - - const savedAuthUserData = - savedUserString == null - ? null - : (JSON.parse(savedUserString) as IAuthUser); - - const savedUser = - savedAuthUserData == null - ? null - : new AuthUser(savedAuthUserData, savedAuthUserData.token); - - if (savedUser == null) { - this.userSubject.next(null); - return null; - } - - this.userSubject.next(savedUser); - - const savedToken = savedUser.token; - try { - const res = await getHttpTokenClient().verify({ token: savedToken }); - const user = new AuthUser(res.user, savedToken); - localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user)); - this.userSubject.next(user); - pushAlert({ - type: "success", - message: "user.welcomeBack", - }); - return user; - } catch (error) { - if (error instanceof HttpNetworkError) { - pushAlert({ - type: "danger", - message: "user.verifyTokenFailedNetwork", - }); - return savedUser; - } else { - localStorage.removeItem(USER_STORAGE_KEY); - this.userSubject.next(null); - pushAlert({ - type: "danger", - message: "user.verifyTokenFailed", - }); - return null; - } - } - } - - async login( - credentials: LoginCredentials, - rememberMe: boolean - ): Promise<void> { - if (this.currentUser) { - throw new UiLogicError("Already login."); - } - try { - const res = await getHttpTokenClient().create({ - ...credentials, - expire: 30, - }); - const user = new AuthUser(res.user, res.token); - if (rememberMe) { - localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user)); - } - this.userSubject.next(user); - } catch (e) { - if (e instanceof HttpCreateTokenBadCredentialError) { - throw new BadCredentialError(); - } else { - throw e; - } - } - } - - logout(): Promise<void> { - if (this.currentUser === undefined) { - throw new UiLogicError("Please check user first."); - } - if (this.currentUser === null) { - throw new UiLogicError("No login."); - } - localStorage.removeItem(USER_STORAGE_KEY); - this.userSubject.next(null); - return Promise.resolve(); - } - - changePassword(oldPassword: string, newPassword: string): Promise<void> { - if (this.currentUser == undefined) { - throw new UiLogicError("Not login or checked now, can't log out."); - } - - return getHttpUserClient() - .changePassword({ - oldPassword, - newPassword, - }) - .then(() => this.logout()); - } -} - -export const userService = new UserService(); - -export function useRawUser(): AuthUser | null | undefined { - const [user, setUser] = useState<AuthUser | null | undefined>( - userService.currentUser - ); - useEffect(() => { - const subscription = userService.user$.subscribe((u) => setUser(u)); - return () => { - subscription.unsubscribe(); - }; - }); - return user; -} - -export function useUser(): AuthUser | null { - const [user, setUser] = useState<AuthUser | null>(() => { - const initUser = userService.currentUser; - if (initUser === undefined) { - throw new UiLogicError( - "This is a logic error in user module. Current user can't be undefined in useUser." - ); - } - return initUser; - }); - useEffect(() => { - const sub = userService.user$.subscribe((u) => { - if (u === undefined) { - throw new UiLogicError( - "This is a logic error in user module. User emitted can't be undefined later." - ); - } - setUser(u); - }); - return () => { - sub.unsubscribe(); - }; - }); - return user; -} - -export function useUserLoggedIn(): AuthUser { - const user = useUser(); - if (user == null) { - throw new UiLogicError("You assert user has logged in but actually not."); - } - return user; -} |