diff options
author | crupest <crupest@outlook.com> | 2023-09-20 20:26:42 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-20 20:26:42 +0800 |
commit | f836d77e73f3ea0af45c5f71dae7268143d6d86f (patch) | |
tree | 573cfafd972106d69bef0d41ff5f270ec3c43ec2 /FrontEnd/src/services | |
parent | 4a069bf1268f393d5467166356f691eb89963152 (diff) | |
parent | 901fe3d7c032d284da5c9bce24c4aaee9054c7ac (diff) | |
download | timeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.tar.gz timeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.tar.bz2 timeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.zip |
Merge pull request #1395 from crupest/dev
Refector 2023 v0.1
Diffstat (limited to 'FrontEnd/src/services')
-rw-r--r-- | FrontEnd/src/services/TimelinePostBuilder.ts | 125 | ||||
-rw-r--r-- | FrontEnd/src/services/alert.ts | 6 | ||||
-rw-r--r-- | FrontEnd/src/services/timeline.ts | 199 | ||||
-rw-r--r-- | FrontEnd/src/services/user.ts | 23 |
4 files changed, 166 insertions, 187 deletions
diff --git a/FrontEnd/src/services/TimelinePostBuilder.ts b/FrontEnd/src/services/TimelinePostBuilder.ts deleted file mode 100644 index 83d63abe..00000000 --- a/FrontEnd/src/services/TimelinePostBuilder.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { marked } from "marked"; - -import { UiLogicError } from "@/common"; - -import base64 from "@/utilities/base64"; - -import { HttpTimelinePostPostRequest } from "@/http/timeline"; - -class TimelinePostMarkedRenderer extends marked.Renderer { - constructor(private _images: { file: File; url: string }[]) { - super(); - } - - image(href: string | null, title: string | null, text: string): string { - if (href != null) { - const i = parseInt(href); - if (!isNaN(i) && i > 0 && i <= this._images.length) { - href = this._images[i - 1].url; - } - } - return this.image(href, title, text); - } -} - -export default class TimelinePostBuilder { - private _onChange: () => void; - private _text = ""; - private _images: { file: File; url: string }[] = []; - private _markedOptions: marked.MarkedOptions; - - constructor(onChange: () => void) { - this._onChange = onChange; - this._markedOptions = { - renderer: new TimelinePostMarkedRenderer(this._images), - }; - } - - 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 marked.parse(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/services/alert.ts b/FrontEnd/src/services/alert.ts index 42b14451..0fa37848 100644 --- a/FrontEnd/src/services/alert.ts +++ b/FrontEnd/src/services/alert.ts @@ -1,10 +1,10 @@ import pull from "lodash/pull"; -import { I18nText } from "@/common"; -import { PaletteColorType } from "@/palette"; +import { I18nText } from "~src/common"; +import { ThemeColor } from "~src/components/common"; export interface AlertInfo { - type?: PaletteColorType; + type?: ThemeColor; message?: I18nText; customMessage?: React.ReactElement; dismissTime?: number | "never"; diff --git a/FrontEnd/src/services/timeline.ts b/FrontEnd/src/services/timeline.ts index 707c956f..41a7bff0 100644 --- a/FrontEnd/src/services/timeline.ts +++ b/FrontEnd/src/services/timeline.ts @@ -1,9 +1,22 @@ -import { TimelineVisibility } from "@/http/timeline"; import XRegExp from "xregexp"; -import { Observable } from "rxjs"; -import { HubConnectionBuilder, HubConnectionState } from "@microsoft/signalr"; - -import { getHttpToken } from "@/http/common"; +import { + Observable, + BehaviorSubject, + switchMap, + filter, + first, + distinctUntilChanged, +} from "rxjs"; +import { + HubConnection, + HubConnectionBuilder, + HubConnectionState, +} from "@microsoft/signalr"; + +import { TimelineVisibility } from "~src/http/timeline"; +import { token$ } from "~src/http/common"; + +// cSpell:ignore onreconnected onreconnecting const timelineNameReg = XRegExp("^[-_\\p{L}]*$", "u"); @@ -20,17 +33,23 @@ export const timelineVisibilityTooltipTranslationMap: Record< Private: "timeline.visibilityTooltip.private", }; -export function getTimelinePostUpdate$( - owner: string, - timeline: string, -): Observable<{ update: boolean; state: HubConnectionState }> { - return new Observable((subscriber) => { - subscriber.next({ - update: false, - state: HubConnectionState.Connecting, - }); +type ConnectionState = + | "Connecting" + | "Reconnecting" + | "Disconnected" + | "Connected"; + +type Connection = { + connection: HubConnection; + state$: Observable<ConnectionState>; +}; + +function createConnection$(token: string | null): Observable<Connection> { + return new Observable<Connection>((subscriber) => { + const connectionStateSubject = new BehaviorSubject<ConnectionState>( + "Connecting", + ); - const token = getHttpToken(); const connection = new HubConnectionBuilder() .withUrl("/api/hub/timeline", { accessTokenFactory: token == null ? undefined : () => token, @@ -38,56 +57,138 @@ export function getTimelinePostUpdate$( .withAutomaticReconnect() .build(); - const o = owner; - const t = timeline; + connection.onclose = () => { + connectionStateSubject.next("Disconnected"); + }; + + connection.onreconnecting = () => { + connectionStateSubject.next("Reconnecting"); + }; + + connection.onreconnected = () => { + connectionStateSubject.next("Connected"); + }; + + let requestStopped = false; + + void connection.start().then( + () => { + connectionStateSubject.next("Connected"); + }, + (e) => { + if (!requestStopped) { + throw e; + } + }, + ); + + subscriber.next({ + connection, + state$: connectionStateSubject.asObservable(), + }); + + return () => { + void connection.stop(); + requestStopped = true; + }; + }); +} + +const connectionSubject = new BehaviorSubject<Connection | null>(null); + +token$ + .pipe(distinctUntilChanged(), switchMap(createConnection$)) + .subscribe(connectionSubject); + +const connection$ = connectionSubject + .asObservable() + .pipe(filter((c): c is Connection => c != null)); + +function createTimelinePostUpdateCount$( + connection: Connection, + owner: string, + timeline: string, +): Observable<number> { + const [o, t] = [owner, timeline]; + return new Observable<number>((subscriber) => { + const hubConnection = connection.connection; + let count = 0; const handler = (owner: string, timeline: string): void => { if (owner === o && timeline === t) { - subscriber.next({ update: true, state: connection.state }); + subscriber.next(count++); } }; - connection.onclose(() => { - subscriber.next({ - update: false, - state: HubConnectionState.Disconnected, + let hubOn = false; + + const subscription = connection.state$ + .pipe(first((state) => state === "Connected")) + .subscribe(() => { + hubConnection.on("OnTimelinePostChangedV2", handler); + void hubConnection.invoke( + "SubscribeTimelinePostChangeV2", + owner, + timeline, + ); + hubOn = true; }); - }); - connection.onreconnecting(() => { - subscriber.next({ - update: false, - state: HubConnectionState.Reconnecting, - }); - }); + return () => { + if (hubOn) { + void hubConnection.invoke( + "UnsubscribeTimelinePostChangeV2", + owner, + timeline, + ); + hubConnection.off("OnTimelinePostChangedV2", handler); + } + + subscription.unsubscribe(); + }; + }); +} + +type OldUpdateInfo = { update: boolean; state: HubConnectionState }; - connection.onreconnected(() => { +function createTimelinePostOldUpdateInfo$( + connection: Connection, + owner: string, + timeline: string, +): Observable<OldUpdateInfo> { + return new Observable<OldUpdateInfo>((subscriber) => { + let savedState: ConnectionState = "Connecting"; + + const postUpdateSubscription = createTimelinePostUpdateCount$( + connection, + owner, + timeline, + ).subscribe(() => { subscriber.next({ - update: false, - state: HubConnectionState.Connected, + update: true, + state: savedState as HubConnectionState, }); }); - connection.on("OnTimelinePostChangedV2", handler); - - void connection.start().then(() => { - subscriber.next({ update: false, state: HubConnectionState.Connected }); - - return connection.invoke( - "SubscribeTimelinePostChangeV2", - owner, - timeline, - ); + const stateSubscription = connection.state$.subscribe((state) => { + savedState = state; + subscriber.next({ update: false, state: state as HubConnectionState }); }); return () => { - connection.off("OnTimelinePostChangedV2", handler); - - if (connection.state === HubConnectionState.Connected) { - void connection - .invoke("UnsubscribeTimelinePostChangeV2", owner, timeline) - .then(() => connection.stop()); - } + stateSubscription.unsubscribe(); + postUpdateSubscription.unsubscribe(); }; }); } + +export function getTimelinePostUpdate$( + owner: string, + timeline: string, +): Observable<OldUpdateInfo> { + return connection$.pipe( + switchMap((connection) => + createTimelinePostOldUpdateInfo$(connection, owner, timeline), + ), + ); +} diff --git a/FrontEnd/src/services/user.ts b/FrontEnd/src/services/user.ts index c89ca893..5f682a36 100644 --- a/FrontEnd/src/services/user.ts +++ b/FrontEnd/src/services/user.ts @@ -2,20 +2,23 @@ import { useState, useEffect } from "react"; import { BehaviorSubject, Observable } from "rxjs"; import { AxiosError } from "axios"; -import { UiLogicError } from "@/common"; +import { UiLogicError } from "~src/common"; -import { setHttpToken, axios, HttpBadRequestError } from "@/http/common"; -import { getHttpTokenClient } from "@/http/token"; -import { getHttpUserClient, HttpUser, UserPermission } from "@/http/user"; +import { setHttpToken, axios, HttpBadRequestError } from "~src/http/common"; +import { getHttpTokenClient } from "~src/http/token"; +import { getHttpUserClient, HttpUser, UserPermission } from "~src/http/user"; -import { pushAlert } from "./alert"; +import { pushAlert } from "~src/components/alert"; interface IAuthUser extends HttpUser { token: string; } export class AuthUser implements IAuthUser { - constructor(user: HttpUser, public token: string) { + constructor( + user: HttpUser, + public token: string, + ) { this.uniqueId = user.uniqueId; this.username = user.username; this.permissions = user.permissions; @@ -61,7 +64,7 @@ export class UserService { if (e.isAxiosError && e.response && e.response.status === 401) { this.userSubject.next(null); pushAlert({ - type: "danger", + color: "danger", message: "user.tokenInvalid", }); } else { @@ -97,11 +100,11 @@ export class UserService { localStorage.removeItem(USER_STORAGE_KEY); this.userSubject.next(null); pushAlert({ - type: "danger", + color: "danger", message: "user.tokenInvalid", }); } - } + }, ); } } @@ -118,7 +121,7 @@ export class UserService { async login( credentials: LoginCredentials, - rememberMe: boolean + rememberMe: boolean, ): Promise<void> { if (this.currentUser) { throw new UiLogicError("Already login."); |