aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/services
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/services')
-rw-r--r--FrontEnd/src/services/TimelinePostBuilder.ts125
-rw-r--r--FrontEnd/src/services/alert.ts6
-rw-r--r--FrontEnd/src/services/timeline.ts199
-rw-r--r--FrontEnd/src/services/user.ts23
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.");