From 47587812b809fee2a95c76266d9d0e42fc4ac1ca Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 14:14:28 +0800 Subject: ... --- FrontEnd/src/services/TimelinePostBuilder.ts | 116 ++++++++++++++ FrontEnd/src/services/alert.ts | 63 ++++++++ FrontEnd/src/services/timeline.ts | 85 ++++++++++ FrontEnd/src/services/user.ts | 228 +++++++++++++++++++++++++++ 4 files changed, 492 insertions(+) create mode 100644 FrontEnd/src/services/TimelinePostBuilder.ts create mode 100644 FrontEnd/src/services/alert.ts create mode 100644 FrontEnd/src/services/timeline.ts create mode 100644 FrontEnd/src/services/user.ts (limited to 'FrontEnd/src/services') diff --git a/FrontEnd/src/services/TimelinePostBuilder.ts b/FrontEnd/src/services/TimelinePostBuilder.ts new file mode 100644 index 00000000..fe4c7a9a --- /dev/null +++ b/FrontEnd/src/services/TimelinePostBuilder.ts @@ -0,0 +1,116 @@ +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 => + 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 { + 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 new file mode 100644 index 00000000..48d482ea --- /dev/null +++ b/FrontEnd/src/services/alert.ts @@ -0,0 +1,63 @@ +import React from "react"; +import pull from "lodash/pull"; + +import { BootstrapThemeColor, I18nText } from "@/common"; + +export interface AlertInfo { + type?: BootstrapThemeColor; + message: React.FC | 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/services/timeline.ts b/FrontEnd/src/services/timeline.ts new file mode 100644 index 00000000..4ebb705d --- /dev/null +++ b/FrontEnd/src/services/timeline.ts @@ -0,0 +1,85 @@ +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/services/user.ts b/FrontEnd/src/services/user.ts new file mode 100644 index 00000000..3375c88a --- /dev/null +++ b/FrontEnd/src/services/user.ts @@ -0,0 +1,228 @@ +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( + undefined + ); + + get user$(): Observable { + return this.userSubject; + } + + get currentUser(): AuthUser | null | undefined { + return this.userSubject.value; + } + + async checkLoginState(): Promise { + 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 { + 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 { + 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 { + 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( + 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(() => { + 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; +} -- cgit v1.2.3 From e0b766203d7576ab67b16ba556ba14120d0bc876 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 15:02:49 +0800 Subject: ... --- BackEnd/Timeline/FrontEndMode.cs | 1 - BackEnd/Timeline/Properties/launchSettings.json | 2 +- BackEnd/Timeline/Startup.cs | 4 --- FrontEnd/index.html | 29 ++++++++++++++++++++++ FrontEnd/package.json | 5 ++-- FrontEnd/postcss.config.js | 11 +++----- FrontEnd/src/index.ejs | 29 ---------------------- FrontEnd/src/services/TimelinePostBuilder.ts | 4 +-- FrontEnd/src/services/timeline.ts | 4 +-- FrontEnd/src/services/user.ts | 6 ++--- FrontEnd/src/tsconfig.json | 23 ----------------- FrontEnd/src/views/admin/UserAdmin.tsx | 2 +- FrontEnd/src/views/center/CenterBoards.tsx | 6 ++--- FrontEnd/src/views/center/TimelineBoard.tsx | 4 +-- FrontEnd/src/views/center/TimelineCreateDialog.tsx | 2 +- FrontEnd/src/views/common/user/UserAvatar.tsx | 2 +- FrontEnd/src/views/home/TimelineListView.tsx | 4 +-- FrontEnd/src/views/home/index.tsx | 4 +-- FrontEnd/src/views/search/index.tsx | 10 ++++---- FrontEnd/src/views/settings/ChangeAvatarDialog.tsx | 2 +- .../src/views/settings/ChangeNicknameDialog.tsx | 2 +- .../timeline-common/ConnectionStatusBadge.tsx | 2 +- .../src/views/timeline-common/MarkdownPostEdit.tsx | 2 +- .../timeline-common/PostPropertyChangeDialog.tsx | 2 +- FrontEnd/src/views/timeline-common/Timeline.tsx | 6 ++--- .../src/views/timeline-common/TimelineMember.tsx | 8 +++--- .../timeline-common/TimelinePageCardTemplate.tsx | 4 +-- .../views/timeline-common/TimelinePageTemplate.tsx | 4 +-- .../timeline-common/TimelinePagedPostListView.tsx | 2 +- .../timeline-common/TimelinePostContentView.tsx | 4 +-- .../src/views/timeline-common/TimelinePostEdit.tsx | 6 ++--- .../views/timeline-common/TimelinePostListView.tsx | 2 +- .../src/views/timeline-common/TimelinePostView.tsx | 2 +- .../TimelinePropertyChangeDialog.tsx | 2 +- FrontEnd/src/views/timeline/TimelineCard.tsx | 2 +- .../src/views/timeline/TimelineDeleteDialog.tsx | 2 +- FrontEnd/src/views/user/index.tsx | 2 +- FrontEnd/tsconfig.json | 25 +++++++++++++++++++ FrontEnd/vite.config.js | 3 +++ 39 files changed, 116 insertions(+), 120 deletions(-) create mode 100644 FrontEnd/index.html delete mode 100644 FrontEnd/src/index.ejs delete mode 100644 FrontEnd/src/tsconfig.json create mode 100644 FrontEnd/tsconfig.json (limited to 'FrontEnd/src/services') diff --git a/BackEnd/Timeline/FrontEndMode.cs b/BackEnd/Timeline/FrontEndMode.cs index 63503292..eb718028 100644 --- a/BackEnd/Timeline/FrontEndMode.cs +++ b/BackEnd/Timeline/FrontEndMode.cs @@ -4,7 +4,6 @@ { Disable, Mock, - Proxy, Normal } } diff --git a/BackEnd/Timeline/Properties/launchSettings.json b/BackEnd/Timeline/Properties/launchSettings.json index f683ca2d..7f68b709 100644 --- a/BackEnd/Timeline/Properties/launchSettings.json +++ b/BackEnd/Timeline/Properties/launchSettings.json @@ -4,7 +4,7 @@ "commandName": "Project", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_FRONTEND": "Proxy" + "ASPNETCORE_FRONTEND": "Disable" } }, "Dev-Mock": { diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index 18097e2c..26ffb4b1 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -166,10 +166,6 @@ namespace Timeline { app.UseSpa(spa => { - if (_frontEndMode == FrontEndMode.Proxy) - { - spa.UseProxyToSpaDevelopmentServer(new UriBuilder("http", "localhost", 3000).Uri); - } }); } } diff --git a/FrontEnd/index.html b/FrontEnd/index.html new file mode 100644 index 00000000..87e19743 --- /dev/null +++ b/FrontEnd/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + Timeline + + + +
+ + + diff --git a/FrontEnd/package.json b/FrontEnd/package.json index 1afb5ad0..5d12899e 100644 --- a/FrontEnd/package.json +++ b/FrontEnd/package.json @@ -33,8 +33,9 @@ "xregexp": "^5.0.2" }, "scripts": { - "start": "webpack serve --config ./webpack.config.dev.js", - "build": "webpack --config ./webpack.config.prod.js", + "start": "vite", + "build": "tsc && vite build", + "preview": "vite preview", "lint": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx", "lint:fix": "eslint src/ --ext .js --ext .jsx --ext .ts --ext .tsx --fix" }, diff --git a/FrontEnd/postcss.config.js b/FrontEnd/postcss.config.js index 74ee8155..9129aa1f 100644 --- a/FrontEnd/postcss.config.js +++ b/FrontEnd/postcss.config.js @@ -1,10 +1,5 @@ +import postcssPresetEnv from "postcss-preset-env"; + module.exports = { - plugins: [ - [ - "postcss-preset-env", - { - // Options - }, - ], - ], + plugins: [postcssPresetEnv()], }; diff --git a/FrontEnd/src/index.ejs b/FrontEnd/src/index.ejs deleted file mode 100644 index c2ff4182..00000000 --- a/FrontEnd/src/index.ejs +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - <%= htmlWebpackPlugin.options.title %> - - - -
- - - diff --git a/FrontEnd/src/services/TimelinePostBuilder.ts b/FrontEnd/src/services/TimelinePostBuilder.ts index fe4c7a9a..40279eca 100644 --- a/FrontEnd/src/services/TimelinePostBuilder.ts +++ b/FrontEnd/src/services/TimelinePostBuilder.ts @@ -2,8 +2,8 @@ import { Remarkable } from "remarkable"; import { UiLogicError } from "@/common"; -import { base64 } from "http/common"; -import { HttpTimelinePostPostRequest } from "http/timeline"; +import { base64 } from "@/http/common"; +import { HttpTimelinePostPostRequest } from "@/http/timeline"; export default class TimelinePostBuilder { private _onChange: () => void; diff --git a/FrontEnd/src/services/timeline.ts b/FrontEnd/src/services/timeline.ts index 4ebb705d..d8c0ae00 100644 --- a/FrontEnd/src/services/timeline.ts +++ b/FrontEnd/src/services/timeline.ts @@ -1,9 +1,9 @@ -import { TimelineVisibility } from "http/timeline"; +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 { getHttpToken } from "@/http/common"; const timelineNameReg = XRegExp("^[-_\\p{L}]*$", "u"); diff --git a/FrontEnd/src/services/user.ts b/FrontEnd/src/services/user.ts index 3375c88a..9a8e5687 100644 --- a/FrontEnd/src/services/user.ts +++ b/FrontEnd/src/services/user.ts @@ -3,12 +3,12 @@ import { BehaviorSubject, Observable } from "rxjs"; import { UiLogicError } from "@/common"; -import { HttpNetworkError, setHttpToken } from "http/common"; +import { HttpNetworkError, setHttpToken } from "@/http/common"; import { getHttpTokenClient, HttpCreateTokenBadCredentialError, -} from "http/token"; -import { getHttpUserClient, HttpUser, UserPermission } from "http/user"; +} from "@/http/token"; +import { getHttpUserClient, HttpUser, UserPermission } from "@/http/user"; import { pushAlert } from "./alert"; diff --git a/FrontEnd/src/tsconfig.json b/FrontEnd/src/tsconfig.json deleted file mode 100644 index 817c50bd..00000000 --- a/FrontEnd/src/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "react", - "types": ["vite/client"], - "sourceMap": true, - "baseUrl": "./", - "paths": { - "@/*": ["*"] - }, - "lib": ["dom", "dom.iterable", "esnext"] - }, - "include": ["."] -} diff --git a/FrontEnd/src/views/admin/UserAdmin.tsx b/FrontEnd/src/views/admin/UserAdmin.tsx index 4e9cd600..558d3aee 100644 --- a/FrontEnd/src/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/views/admin/UserAdmin.tsx @@ -12,7 +12,7 @@ import { HttpUser, kUserPermissionList, UserPermission, -} from "http/user"; +} from "@/http/user"; import { Trans, useTranslation } from "react-i18next"; interface DialogProps { diff --git a/FrontEnd/src/views/center/CenterBoards.tsx b/FrontEnd/src/views/center/CenterBoards.tsx index 431d1e9a..f5200415 100644 --- a/FrontEnd/src/views/center/CenterBoards.tsx +++ b/FrontEnd/src/views/center/CenterBoards.tsx @@ -5,9 +5,9 @@ import { useTranslation } from "react-i18next"; import { pushAlert } from "@/services/alert"; import { useUserLoggedIn } from "@/services/user"; -import { getHttpTimelineClient } from "http/timeline"; -import { getHttpBookmarkClient } from "http/bookmark"; -import { getHttpHighlightClient } from "http/highlight"; +import { getHttpTimelineClient } from "@/http/timeline"; +import { getHttpBookmarkClient } from "@/http/bookmark"; +import { getHttpHighlightClient } from "@/http/highlight"; import TimelineBoard from "./TimelineBoard"; diff --git a/FrontEnd/src/views/center/TimelineBoard.tsx b/FrontEnd/src/views/center/TimelineBoard.tsx index bb80266b..840c0415 100644 --- a/FrontEnd/src/views/center/TimelineBoard.tsx +++ b/FrontEnd/src/views/center/TimelineBoard.tsx @@ -4,7 +4,7 @@ import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Spinner } from "react-bootstrap"; -import { HttpTimelineInfo } from "http/timeline"; +import { HttpTimelineInfo } from "@/http/timeline"; import TimelineLogo from "../common/TimelineLogo"; import UserTimelineLogo from "../common/UserTimelineLogo"; @@ -34,7 +34,7 @@ const TimelineBoardItem: React.FC = ({ actions, }) => { const { name, title } = timeline; - const isPersonal = name.startsWith("@"); + const isPersonal = name.startsWith("src"); const url = isPersonal ? `/users/${timeline.owner.username}` : `/timelines/${name}`; diff --git a/FrontEnd/src/views/center/TimelineCreateDialog.tsx b/FrontEnd/src/views/center/TimelineCreateDialog.tsx index a2437ae5..b4e25ba1 100644 --- a/FrontEnd/src/views/center/TimelineCreateDialog.tsx +++ b/FrontEnd/src/views/center/TimelineCreateDialog.tsx @@ -3,7 +3,7 @@ import { useHistory } from "react-router"; import { validateTimelineName } from "@/services/timeline"; import OperationDialog from "../common/OperationDialog"; -import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; interface TimelineCreateDialogProps { open: boolean; diff --git a/FrontEnd/src/views/common/user/UserAvatar.tsx b/FrontEnd/src/views/common/user/UserAvatar.tsx index 901697db..9e822528 100644 --- a/FrontEnd/src/views/common/user/UserAvatar.tsx +++ b/FrontEnd/src/views/common/user/UserAvatar.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { getHttpUserClient } from "http/user"; +import { getHttpUserClient } from "@/http/user"; export interface UserAvatarProps extends React.ImgHTMLAttributes { diff --git a/FrontEnd/src/views/home/TimelineListView.tsx b/FrontEnd/src/views/home/TimelineListView.tsx index 975875af..2fb54820 100644 --- a/FrontEnd/src/views/home/TimelineListView.tsx +++ b/FrontEnd/src/views/home/TimelineListView.tsx @@ -2,7 +2,7 @@ import React from "react"; import { convertI18nText, I18nText } from "@/common"; -import { HttpTimelineInfo } from "http/timeline"; +import { HttpTimelineInfo } from "@/http/timeline"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; @@ -13,7 +13,7 @@ interface TimelineListItemProps { const TimelineListItem: React.FC = ({ timeline }) => { const url = React.useMemo( () => - timeline.name.startsWith("@") + timeline.name.startsWith("src") ? `/users/${timeline.owner.username}` : `/timelines/${timeline.name}`, [timeline] diff --git a/FrontEnd/src/views/home/index.tsx b/FrontEnd/src/views/home/index.tsx index efc364d7..0eca23ee 100644 --- a/FrontEnd/src/views/home/index.tsx +++ b/FrontEnd/src/views/home/index.tsx @@ -1,8 +1,8 @@ import React from "react"; import { useHistory } from "react-router"; -import { HttpTimelineInfo } from "http/timeline"; -import { getHttpHighlightClient } from "http/highlight"; +import { HttpTimelineInfo } from "@/http/timeline"; +import { getHttpHighlightClient } from "@/http/highlight"; import SearchInput from "../common/SearchInput"; import TimelineListView from "./TimelineListView"; diff --git a/FrontEnd/src/views/search/index.tsx b/FrontEnd/src/views/search/index.tsx index 14a9709c..9a26802d 100644 --- a/FrontEnd/src/views/search/index.tsx +++ b/FrontEnd/src/views/search/index.tsx @@ -4,9 +4,9 @@ import { Container, Row } from "react-bootstrap"; import { useHistory, useLocation } from "react-router"; import { Link } from "react-router-dom"; -import { HttpNetworkError } from "http/common"; -import { getHttpSearchClient } from "http/search"; -import { HttpTimelineInfo } from "http/timeline"; +import { HttpNetworkError } from "@/http/common"; +import { getHttpSearchClient } from "@/http/search"; +import { HttpTimelineInfo } from "@/http/timeline"; import SearchInput from "../common/SearchInput"; import UserAvatar from "../common/user/UserAvatar"; @@ -14,7 +14,7 @@ import UserAvatar from "../common/user/UserAvatar"; const TimelineSearchResultItemView: React.FC<{ timeline: HttpTimelineInfo; }> = ({ timeline }) => { - const link = timeline.name.startsWith("@") + const link = timeline.name.startsWith("src") ? `users/${timeline.owner.username}` : `timelines/${timeline.name}`; @@ -33,7 +33,7 @@ const TimelineSearchResultItemView: React.FC<{ /> {timeline.owner.nickname} - @{timeline.owner.username} + src{timeline.owner.username} diff --git a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx index 338d2112..c4f6f492 100644 --- a/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/views/settings/ChangeAvatarDialog.tsx @@ -7,7 +7,7 @@ import { UiLogicError } from "@/common"; import { useUserLoggedIn } from "@/services/user"; -import { getHttpUserClient } from "http/user"; +import { getHttpUserClient } from "@/http/user"; import ImageCropper, { Clip, applyClipToImage } from "../common/ImageCropper"; diff --git a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx b/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx index e6420f36..4b44cdd6 100644 --- a/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/views/settings/ChangeNicknameDialog.tsx @@ -1,4 +1,4 @@ -import { getHttpUserClient } from "http/user"; +import { getHttpUserClient } from "@/http/user"; import { useUserLoggedIn } from "@/services/user"; import React from "react"; diff --git a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx index df43d8d2..1b9d6d2a 100644 --- a/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx +++ b/FrontEnd/src/views/timeline-common/ConnectionStatusBadge.tsx @@ -1,6 +1,6 @@ import React from "react"; import classnames from "classnames"; -import { HubConnectionState } from "@microsoft/signalr"; +import { HubConnectionState } from "srcmicrosoft/signalr"; import { useTranslation } from "react-i18next"; export interface ConnectionStatusBadgeProps { diff --git a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx index 1514d28f..685e17be 100644 --- a/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/views/timeline-common/MarkdownPostEdit.tsx @@ -4,7 +4,7 @@ import { Form, Spinner } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { Prompt } from "react-router"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import FlatButton from "../common/FlatButton"; import TabPages from "../common/TabPages"; diff --git a/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx b/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx index 21c5272e..001e52d7 100644 --- a/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline-common/PostPropertyChangeDialog.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import OperationDialog from "../common/OperationDialog"; diff --git a/FrontEnd/src/views/timeline-common/Timeline.tsx b/FrontEnd/src/views/timeline-common/Timeline.tsx index 40619e64..31ea5870 100644 --- a/FrontEnd/src/views/timeline-common/Timeline.tsx +++ b/FrontEnd/src/views/timeline-common/Timeline.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { HubConnectionState } from "@microsoft/signalr"; +import { HubConnectionState } from "srcmicrosoft/signalr"; import { HttpForbiddenError, HttpNetworkError, HttpNotFoundError, -} from "http/common"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +} from "@/http/common"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import { getTimelinePostUpdate$ } from "@/services/timeline"; diff --git a/FrontEnd/src/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/views/timeline-common/TimelineMember.tsx index 3d4de8b8..1ef085a7 100644 --- a/FrontEnd/src/views/timeline-common/TimelineMember.tsx +++ b/FrontEnd/src/views/timeline-common/TimelineMember.tsx @@ -4,12 +4,12 @@ import { Container, ListGroup, Modal, Row, Col, Button } from "react-bootstrap"; import { convertI18nText, I18nText } from "@/common"; -import { HttpUser } from "http/user"; -import { getHttpSearchClient } from "http/search"; +import { HttpUser } from "@/http/user"; +import { getHttpSearchClient } from "@/http/search"; import SearchInput from "../common/SearchInput"; import UserAvatar from "../common/user/UserAvatar"; -import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; const TimelineMemberItem: React.FC<{ user: HttpUser; @@ -27,7 +27,7 @@ const TimelineMemberItem: React.FC<{ {user.nickname} - {"@" + user.username} + {"src" + user.username} {onAction ? ( diff --git a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx index 038ea3ab..623d643f 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePageCardTemplate.tsx @@ -2,8 +2,8 @@ import React from "react"; import classnames from "classnames"; import { useTranslation } from "react-i18next"; -import { getHttpHighlightClient } from "http/highlight"; -import { getHttpBookmarkClient } from "http/bookmark"; +import { getHttpHighlightClient } from "@/http/highlight"; +import { getHttpBookmarkClient } from "@/http/bookmark"; import { useUser } from "@/services/user"; import { pushAlert } from "@/services/alert"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx index 44926cc6..658ce502 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePageTemplate.tsx @@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next"; import { Container } from "react-bootstrap"; import { HubConnectionState } from "@microsoft/signalr"; -import { HttpNetworkError, HttpNotFoundError } from "http/common"; -import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; +import { HttpNetworkError, HttpNotFoundError } from "@/http/common"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; import { getAlertHost } from "@/services/alert"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx b/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx index d569a2d7..37f02a82 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePagedPostListView.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { HttpTimelinePostInfo } from "http/timeline"; +import { HttpTimelinePostInfo } from "@/http/timeline"; import useScrollToTop from "@/utilities/useScrollToTop"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx index f1b53335..607b72c9 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostContentView.tsx @@ -4,8 +4,8 @@ import { Remarkable } from "remarkable"; import { UiLogicError } from "@/common"; -import { HttpNetworkError } from "http/common"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +import { HttpNetworkError } from "@/http/common"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import { useUser } from "@/services/user"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx index 0f470fd6..1f9f02a5 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostEdit.tsx @@ -10,10 +10,10 @@ import { HttpTimelineInfo, HttpTimelinePostInfo, HttpTimelinePostPostRequestData, -} from "http/timeline"; +} from "@/http/timeline"; import { pushAlert } from "@/services/alert"; -import { base64 } from "http/common"; +import { base64 } from "@/http/common"; import BlobImage from "../common/BlobImage"; import LoadingButton from "../common/LoadingButton"; @@ -138,7 +138,7 @@ const TimelinePostEdit: React.FC = (props) => { (kind === "text" && text.length !== 0) || (kind === "image" && image != null); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + // eslint-disable-next-line srctypescript-eslint/no-non-null-assertion const containerRef = React.useRef(null!); const notifyHeightChange = (): void => { diff --git a/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx index 49284720..ba204b72 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostListView.tsx @@ -1,7 +1,7 @@ import React, { Fragment } from "react"; import classnames from "classnames"; -import { HttpTimelinePostInfo } from "http/timeline"; +import { HttpTimelinePostInfo } from "@/http/timeline"; import TimelinePostView from "./TimelinePostView"; import TimelineDateLabel from "./TimelineDateLabel"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx index e8b32c71..f7b81478 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePostView.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePostView.tsx @@ -3,7 +3,7 @@ import classnames from "classnames"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; import { pushAlert } from "@/services/alert"; diff --git a/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx index 83b24d01..70f72025 100644 --- a/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/views/timeline-common/TimelinePropertyChangeDialog.tsx @@ -6,7 +6,7 @@ import { HttpTimelinePatchRequest, kTimelineVisibilities, TimelineVisibility, -} from "http/timeline"; +} from "@/http/timeline"; import OperationDialog from "../common/OperationDialog"; diff --git a/FrontEnd/src/views/timeline/TimelineCard.tsx b/FrontEnd/src/views/timeline/TimelineCard.tsx index e031b565..86063843 100644 --- a/FrontEnd/src/views/timeline/TimelineCard.tsx +++ b/FrontEnd/src/views/timeline/TimelineCard.tsx @@ -29,7 +29,7 @@ const TimelineCard: React.FC = (props) => { /> {timeline.owner.nickname} - @{timeline.owner.username} + src{timeline.owner.username} diff --git a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx index 8821507d..dbca62ca 100644 --- a/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx +++ b/FrontEnd/src/views/timeline/TimelineDeleteDialog.tsx @@ -2,7 +2,7 @@ import React from "react"; import { useHistory } from "react-router"; import { Trans } from "react-i18next"; -import { getHttpTimelineClient, HttpTimelineInfo } from "http/timeline"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; import OperationDialog from "../common/OperationDialog"; diff --git a/FrontEnd/src/views/user/index.tsx b/FrontEnd/src/views/user/index.tsx index 57454d0d..0013b254 100644 --- a/FrontEnd/src/views/user/index.tsx +++ b/FrontEnd/src/views/user/index.tsx @@ -14,7 +14,7 @@ const UserPage: React.FC = () => { return ( <> setReloadKey(reloadKey + 1)} diff --git a/FrontEnd/tsconfig.json b/FrontEnd/tsconfig.json new file mode 100644 index 00000000..3afe2c3e --- /dev/null +++ b/FrontEnd/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react", + "noEmit": true, + "types": ["vite/client"], + "sourceMap": true, + "baseUrl": "./", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["./src"] +} diff --git a/FrontEnd/vite.config.js b/FrontEnd/vite.config.js index 6e8cde1b..2e85c36a 100644 --- a/FrontEnd/vite.config.js +++ b/FrontEnd/vite.config.js @@ -7,6 +7,9 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [reactRefresh()], + resolve: { + alias: [{ find: "@", replacement: "/src" }], + }, server: { port: 13000, proxy: { -- cgit v1.2.3