aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src')
-rw-r--r--FrontEnd/src/app/http/bookmark.ts34
-rw-r--r--FrontEnd/src/app/http/common.ts69
-rw-r--r--FrontEnd/src/app/http/highlight.ts34
-rw-r--r--FrontEnd/src/app/http/search.ts22
-rw-r--r--FrontEnd/src/app/http/timeline.ts324
-rw-r--r--FrontEnd/src/app/http/token.ts7
-rw-r--r--FrontEnd/src/app/http/user.ts52
-rw-r--r--FrontEnd/src/app/locales/en/translation.json1
-rw-r--r--FrontEnd/src/app/locales/zh/translation.json1
-rw-r--r--FrontEnd/src/app/services/DataHub2.ts191
-rw-r--r--FrontEnd/src/app/services/common.ts24
-rw-r--r--FrontEnd/src/app/services/timeline.ts508
-rw-r--r--FrontEnd/src/app/services/user.ts222
-rw-r--r--FrontEnd/src/app/views/admin/UserAdmin.tsx6
-rw-r--r--FrontEnd/src/app/views/home/OfflineBoard.tsx61
-rw-r--r--FrontEnd/src/app/views/home/TimelineCreateDialog.tsx15
16 files changed, 144 insertions, 1427 deletions
diff --git a/FrontEnd/src/app/http/bookmark.ts b/FrontEnd/src/app/http/bookmark.ts
index 15e55d98..3e5be229 100644
--- a/FrontEnd/src/app/http/bookmark.ts
+++ b/FrontEnd/src/app/http/bookmark.ts
@@ -1,15 +1,6 @@
-import {
- axios,
- apiBaseUrl,
- convertToNetworkError,
- extractResponseData,
-} from "./common";
+import { axios, apiBaseUrl, extractResponseData } from "./common";
-import {
- HttpTimelineInfo,
- processRawTimelineInfo,
- RawHttpTimelineInfo,
-} from "./timeline";
+import { HttpTimelineInfo } from "./timeline";
export interface HttpHighlightMoveRequest {
timeline: string;
@@ -26,31 +17,20 @@ export interface IHttpBookmarkClient {
export class HttpHighlightClient implements IHttpBookmarkClient {
list(): Promise<HttpTimelineInfo[]> {
return axios
- .get<RawHttpTimelineInfo[]>(`${apiBaseUrl}/bookmarks`)
- .then(extractResponseData)
- .then((list) => list.map(processRawTimelineInfo))
- .catch(convertToNetworkError);
+ .get<HttpTimelineInfo[]>(`${apiBaseUrl}/bookmarks`)
+ .then(extractResponseData);
}
put(timeline: string): Promise<void> {
- return axios
- .put(`${apiBaseUrl}/bookmarks/${timeline}`)
- .catch(convertToNetworkError)
- .then();
+ return axios.put(`${apiBaseUrl}/bookmarks/${timeline}`).then();
}
delete(timeline: string): Promise<void> {
- return axios
- .delete(`${apiBaseUrl}/bookmarks/${timeline}`)
- .catch(convertToNetworkError)
- .then();
+ return axios.delete(`${apiBaseUrl}/bookmarks/${timeline}`).then();
}
move(req: HttpHighlightMoveRequest): Promise<void> {
- return axios
- .post(`${apiBaseUrl}/bookmarkop/move`, req)
- .catch(convertToNetworkError)
- .then();
+ return axios.post(`${apiBaseUrl}/bookmarkop/move`, req).then();
}
}
diff --git a/FrontEnd/src/app/http/common.ts b/FrontEnd/src/app/http/common.ts
index 0f46280c..5c44e8e3 100644
--- a/FrontEnd/src/app/http/common.ts
+++ b/FrontEnd/src/app/http/common.ts
@@ -4,6 +4,45 @@ export const apiBaseUrl = "/api";
export const axios = rawAxios.create();
+function convertToNetworkError(error: AxiosError): never {
+ if (error.isAxiosError && error.response == null) {
+ throw new HttpNetworkError(error);
+ } else {
+ throw error;
+ }
+}
+
+function convertToForbiddenError(error: AxiosError): never {
+ if (
+ error.isAxiosError &&
+ error.response != null &&
+ (error.response.status == 401 || error.response.status == 403)
+ ) {
+ throw new HttpForbiddenError(error);
+ } else {
+ throw error;
+ }
+}
+
+function convertToNotFoundError(error: AxiosError): never {
+ if (
+ error.isAxiosError &&
+ error.response != null &&
+ error.response.status == 404
+ ) {
+ throw new HttpNotFoundError(error);
+ } else {
+ throw error;
+ }
+}
+
+rawAxios.interceptors.response.use(undefined, convertToNetworkError);
+rawAxios.interceptors.response.use(undefined, convertToForbiddenError);
+rawAxios.interceptors.response.use(undefined, convertToNotFoundError);
+axios.interceptors.response.use(undefined, convertToNetworkError);
+axios.interceptors.response.use(undefined, convertToForbiddenError);
+axios.interceptors.response.use(undefined, convertToNotFoundError);
+
let _token: string | null = null;
export function getHttpToken(): string | null {
@@ -71,6 +110,12 @@ export class HttpForbiddenError extends Error {
}
}
+export class HttpNotFoundError extends Error {
+ constructor(public innerError?: AxiosError) {
+ super();
+ }
+}
+
export class NotModified {}
export interface BlobWithEtag {
@@ -135,30 +180,6 @@ export function convertToIfErrorCodeIs<NewError>(
});
}
-export function convertToNetworkError(
- error: AxiosError<CommonErrorResponse>
-): never {
- if (error.isAxiosError && error.response == null) {
- throw new HttpNetworkError(error);
- } else {
- throw error;
- }
-}
-
-export function convertToForbiddenError(
- error: AxiosError<CommonErrorResponse>
-): never {
- if (
- error.isAxiosError &&
- error.response != null &&
- (error.response.status == 401 || error.response.status == 403)
- ) {
- throw new HttpForbiddenError(error);
- } else {
- throw error;
- }
-}
-
export function convertToNotModified(
error: AxiosError<CommonErrorResponse>
): NotModified {
diff --git a/FrontEnd/src/app/http/highlight.ts b/FrontEnd/src/app/http/highlight.ts
index 851d52ce..fddf0729 100644
--- a/FrontEnd/src/app/http/highlight.ts
+++ b/FrontEnd/src/app/http/highlight.ts
@@ -1,15 +1,6 @@
-import {
- axios,
- apiBaseUrl,
- convertToNetworkError,
- extractResponseData,
-} from "./common";
+import { axios, apiBaseUrl, extractResponseData } from "./common";
-import {
- HttpTimelineInfo,
- processRawTimelineInfo,
- RawHttpTimelineInfo,
-} from "./timeline";
+import { HttpTimelineInfo } from "./timeline";
export interface HttpHighlightMoveRequest {
timeline: string;
@@ -26,31 +17,20 @@ export interface IHttpHighlightClient {
export class HttpHighlightClient implements IHttpHighlightClient {
list(): Promise<HttpTimelineInfo[]> {
return axios
- .get<RawHttpTimelineInfo[]>(`${apiBaseUrl}/highlights`)
- .then(extractResponseData)
- .then((list) => list.map(processRawTimelineInfo))
- .catch(convertToNetworkError);
+ .get<HttpTimelineInfo[]>(`${apiBaseUrl}/highlights`)
+ .then(extractResponseData);
}
put(timeline: string): Promise<void> {
- return axios
- .put(`${apiBaseUrl}/highlights/${timeline}`)
- .catch(convertToNetworkError)
- .then();
+ return axios.put(`${apiBaseUrl}/highlights/${timeline}`).then();
}
delete(timeline: string): Promise<void> {
- return axios
- .delete(`${apiBaseUrl}/highlights/${timeline}`)
- .catch(convertToNetworkError)
- .then();
+ return axios.delete(`${apiBaseUrl}/highlights/${timeline}`).then();
}
move(req: HttpHighlightMoveRequest): Promise<void> {
- return axios
- .post(`${apiBaseUrl}/highlightop/move`, req)
- .catch(convertToNetworkError)
- .then();
+ return axios.post(`${apiBaseUrl}/highlightop/move`, req).then();
}
}
diff --git a/FrontEnd/src/app/http/search.ts b/FrontEnd/src/app/http/search.ts
index 2da9295e..8ca48fe9 100644
--- a/FrontEnd/src/app/http/search.ts
+++ b/FrontEnd/src/app/http/search.ts
@@ -1,14 +1,5 @@
-import {
- apiBaseUrl,
- axios,
- convertToNetworkError,
- extractResponseData,
-} from "./common";
-import {
- HttpTimelineInfo,
- processRawTimelineInfo,
- RawHttpTimelineInfo,
-} from "./timeline";
+import { apiBaseUrl, axios, extractResponseData } from "./common";
+import { HttpTimelineInfo } from "./timeline";
import { HttpUser } from "./user";
export interface IHttpSearchClient {
@@ -19,17 +10,14 @@ export interface IHttpSearchClient {
export class HttpSearchClient implements IHttpSearchClient {
searchTimelines(query: string): Promise<HttpTimelineInfo[]> {
return axios
- .get<RawHttpTimelineInfo[]>(`${apiBaseUrl}/search/timelines?q=${query}`)
- .then(extractResponseData)
- .then((ts) => ts.map(processRawTimelineInfo))
- .catch(convertToNetworkError);
+ .get<HttpTimelineInfo[]>(`${apiBaseUrl}/search/timelines?q=${query}`)
+ .then(extractResponseData);
}
searchUsers(query: string): Promise<HttpUser[]> {
return axios
.get<HttpUser[]>(`${apiBaseUrl}/search/users?q=${query}`)
- .then(extractResponseData)
- .catch(convertToNetworkError);
+ .then(extractResponseData);
}
}
diff --git a/FrontEnd/src/app/http/timeline.ts b/FrontEnd/src/app/http/timeline.ts
index 68fee5ae..a84a40ef 100644
--- a/FrontEnd/src/app/http/timeline.ts
+++ b/FrontEnd/src/app/http/timeline.ts
@@ -6,15 +6,7 @@ import {
axios,
apiBaseUrl,
extractResponseData,
- convertToNetworkError,
- base64,
- convertToIfStatusCodeIs,
convertToIfErrorCodeIs,
- BlobWithEtag,
- NotModified,
- convertToNotModified,
- convertToForbiddenError,
- convertToBlobWithEtag,
} from "./common";
import { HttpUser } from "./user";
@@ -29,7 +21,8 @@ export interface HttpTimelineInfo {
description: string;
owner: HttpUser;
visibility: TimelineVisibility;
- lastModified: Date;
+ color: string;
+ lastModified: string;
members: HttpUser[];
isHighlight: boolean;
isBookmark: boolean;
@@ -45,57 +38,28 @@ export interface HttpTimelinePostRequest {
name: string;
}
-export interface HttpTimelinePostTextContent {
- type: "text";
- text: string;
-}
-
-export interface HttpTimelinePostImageContent {
- type: "image";
+export interface HttpTimelinePostDataDigest {
+ kind: string;
+ eTag: string;
+ lastUpdated: string;
}
-export type HttpTimelinePostContent =
- | HttpTimelinePostTextContent
- | HttpTimelinePostImageContent;
-
export interface HttpTimelinePostInfo {
id: number;
- content: HttpTimelinePostContent;
- time: Date;
- lastUpdated: Date;
+ time: string;
author: HttpUser;
- deleted: false;
-}
-
-export interface HttpTimelineDeletedPostInfo {
- id: number;
- time: Date;
- lastUpdated: Date;
- author?: HttpUser;
- deleted: true;
-}
-
-export type HttpTimelineGenericPostInfo =
- | HttpTimelinePostInfo
- | HttpTimelineDeletedPostInfo;
-
-export interface HttpTimelinePostPostRequestTextContent {
- type: "text";
- text: string;
-}
-
-export interface HttpTimelinePostPostRequestImageContent {
- type: "image";
- data: Blob;
+ dataList: HttpTimelinePostDataDigest;
+ color: string;
+ lastUpdated: string;
}
-export type HttpTimelinePostPostRequestContent =
- | HttpTimelinePostPostRequestTextContent
- | HttpTimelinePostPostRequestImageContent;
-
export interface HttpTimelinePostPostRequest {
- content: HttpTimelinePostPostRequestContent;
- time?: Date;
+ time?: string;
+ color?: string;
+ dataList: {
+ contentType: string;
+ data: string;
+ }[];
}
export interface HttpTimelinePatchRequest {
@@ -105,120 +69,12 @@ export interface HttpTimelinePatchRequest {
description?: string;
}
-export class HttpTimelineNotExistError extends Error {
- constructor(public innerError?: AxiosError) {
- super();
- }
-}
-
-export class HttpTimelinePostNotExistError extends Error {
- constructor(public innerError?: AxiosError) {
- super();
- }
-}
-
export class HttpTimelineNameConflictError extends Error {
constructor(public innerError?: AxiosError) {
super();
}
}
-//-------------------- begin: internal model --------------------
-
-export interface RawHttpTimelineInfo {
- uniqueId: string;
- title: string;
- name: string;
- description: string;
- owner: HttpUser;
- visibility: TimelineVisibility;
- lastModified: string;
- members: HttpUser[];
- isHighlight: boolean;
- isBookmark: boolean;
-}
-
-interface RawTimelinePostTextContent {
- type: "text";
- text: string;
-}
-
-interface RawTimelinePostImageContent {
- type: "image";
- url: string;
-}
-
-type RawTimelinePostContent =
- | RawTimelinePostTextContent
- | RawTimelinePostImageContent;
-
-interface RawTimelinePostInfo {
- id: number;
- content: RawTimelinePostContent;
- time: string;
- lastUpdated: string;
- author: HttpUser;
- deleted: false;
-}
-
-interface RawTimelineDeletedPostInfo {
- id: number;
- time: string;
- lastUpdated: string;
- author: HttpUser;
- deleted: true;
-}
-
-type RawTimelineGenericPostInfo =
- | RawTimelinePostInfo
- | RawTimelineDeletedPostInfo;
-
-interface RawTimelinePostPostRequestTextContent {
- type: "text";
- text: string;
-}
-
-interface RawTimelinePostPostRequestImageContent {
- type: "image";
- data: string;
-}
-
-type RawTimelinePostPostRequestContent =
- | RawTimelinePostPostRequestTextContent
- | RawTimelinePostPostRequestImageContent;
-
-interface RawTimelinePostPostRequest {
- content: RawTimelinePostPostRequestContent;
- time?: string;
-}
-
-//-------------------- end: internal model --------------------
-
-export function processRawTimelineInfo(
- raw: RawHttpTimelineInfo
-): HttpTimelineInfo {
- return {
- ...raw,
- lastModified: new Date(raw.lastModified),
- };
-}
-
-function processRawTimelinePostInfo(
- raw: RawTimelinePostInfo
-): HttpTimelinePostInfo;
-function processRawTimelinePostInfo(
- raw: RawTimelineGenericPostInfo
-): HttpTimelineGenericPostInfo;
-function processRawTimelinePostInfo(
- raw: RawTimelineGenericPostInfo
-): HttpTimelineGenericPostInfo {
- return {
- ...raw,
- time: new Date(raw.time),
- lastUpdated: new Date(raw.lastUpdated),
- };
-}
-
export interface IHttpTimelineClient {
listTimeline(query: HttpTimelineListQuery): Promise<HttpTimelineInfo[]>;
getTimeline(timelineName: string): Promise<HttpTimelineInfo>;
@@ -231,26 +87,6 @@ export interface IHttpTimelineClient {
memberPut(timelineName: string, username: string): Promise<void>;
memberDelete(timelineName: string, username: string): Promise<void>;
listPost(timelineName: string): Promise<HttpTimelinePostInfo[]>;
- listPost(
- timelineName: string,
- query: {
- modifiedSince?: Date;
- includeDeleted?: false;
- }
- ): Promise<HttpTimelinePostInfo[]>;
- listPost(
- timelineName: string,
- query: {
- modifiedSince?: Date;
- includeDeleted: true;
- }
- ): Promise<HttpTimelineGenericPostInfo[]>;
- getPostData(timelineName: string, postId: number): Promise<BlobWithEtag>;
- getPostData(
- timelineName: string,
- postId: number,
- etag: string
- ): Promise<BlobWithEtag | NotModified>;
postPost(
timelineName: string,
req: HttpTimelinePostPostRequest
@@ -261,30 +97,23 @@ export interface IHttpTimelineClient {
export class HttpTimelineClient implements IHttpTimelineClient {
listTimeline(query: HttpTimelineListQuery): Promise<HttpTimelineInfo[]> {
return axios
- .get<RawHttpTimelineInfo[]>(
+ .get<HttpTimelineInfo[]>(
applyQueryParameters(`${apiBaseUrl}/timelines`, query)
)
- .then(extractResponseData)
- .then((list) => list.map(processRawTimelineInfo))
- .catch(convertToNetworkError);
+ .then(extractResponseData);
}
getTimeline(timelineName: string): Promise<HttpTimelineInfo> {
return axios
- .get<RawHttpTimelineInfo>(`${apiBaseUrl}/timelines/${timelineName}`)
- .then(extractResponseData)
- .then(processRawTimelineInfo)
- .catch(convertToIfStatusCodeIs(404, HttpTimelineNotExistError))
- .catch(convertToNetworkError);
+ .get<HttpTimelineInfo>(`${apiBaseUrl}/timelines/${timelineName}`)
+ .then(extractResponseData);
}
postTimeline(req: HttpTimelinePostRequest): Promise<HttpTimelineInfo> {
return axios
- .post<RawHttpTimelineInfo>(`${apiBaseUrl}/timelines`, req)
+ .post<HttpTimelineInfo>(`${apiBaseUrl}/timelines`, req)
.then(extractResponseData)
- .then(processRawTimelineInfo)
- .catch(convertToIfErrorCodeIs(11040101, HttpTimelineNameConflictError))
- .catch(convertToNetworkError);
+ .catch(convertToIfErrorCodeIs(11040101, HttpTimelineNameConflictError));
}
patchTimeline(
@@ -292,134 +121,49 @@ export class HttpTimelineClient implements IHttpTimelineClient {
req: HttpTimelinePatchRequest
): Promise<HttpTimelineInfo> {
return axios
- .patch<RawHttpTimelineInfo>(
- `${apiBaseUrl}/timelines/${timelineName}`,
- req
- )
- .then(extractResponseData)
- .then(processRawTimelineInfo)
- .catch(convertToNetworkError);
+ .patch<HttpTimelineInfo>(`${apiBaseUrl}/timelines/${timelineName}`, req)
+ .then(extractResponseData);
}
deleteTimeline(timelineName: string): Promise<void> {
- return axios
- .delete(`${apiBaseUrl}/timelines/${timelineName}`)
- .catch(convertToNetworkError)
- .then();
+ return axios.delete(`${apiBaseUrl}/timelines/${timelineName}`).then();
}
memberPut(timelineName: string, username: string): Promise<void> {
return axios
.put(`${apiBaseUrl}/timelines/${timelineName}/members/${username}`)
- .catch(convertToNetworkError)
.then();
}
memberDelete(timelineName: string, username: string): Promise<void> {
return axios
.delete(`${apiBaseUrl}/timelines/${timelineName}/members/${username}`)
- .catch(convertToNetworkError)
.then();
}
- listPost(timelineName: string): Promise<HttpTimelinePostInfo[]>;
- listPost(
- timelineName: string,
- query: {
- modifiedSince?: Date;
- includeDeleted?: false;
- }
- ): Promise<HttpTimelinePostInfo[]>;
- listPost(
- timelineName: string,
- query: {
- modifiedSince?: Date;
- includeDeleted: true;
- }
- ): Promise<HttpTimelineGenericPostInfo[]>;
- listPost(
- timelineName: string,
- query?: {
- modifiedSince?: Date;
- includeDeleted?: boolean;
- }
- ): Promise<HttpTimelineGenericPostInfo[]> {
+ listPost(timelineName: string): Promise<HttpTimelinePostInfo[]> {
return axios
- .get<RawTimelineGenericPostInfo[]>(
- applyQueryParameters(
- `${apiBaseUrl}/timelines/${timelineName}/posts`,
- query
- )
+ .get<HttpTimelinePostInfo[]>(
+ `${apiBaseUrl}/timelines/${timelineName}/posts`
)
- .then(extractResponseData)
- .catch(convertToIfStatusCodeIs(404, HttpTimelineNotExistError))
- .catch(convertToForbiddenError)
- .catch(convertToNetworkError)
- .then((rawPosts) =>
- rawPosts.map((raw) => processRawTimelinePostInfo(raw))
- );
- }
-
- getPostData(timelineName: string, postId: number): Promise<BlobWithEtag>;
- getPostData(
- timelineName: string,
- postId: number,
- etag?: string
- ): Promise<BlobWithEtag | NotModified> {
- const headers =
- etag != null
- ? {
- "If-None-Match": etag,
- }
- : undefined;
-
- const url = `${apiBaseUrl}/timelines/${timelineName}/posts/${postId}/data`;
-
- return axios
- .get(url, {
- responseType: "blob",
- headers,
- })
- .then(convertToBlobWithEtag)
- .catch(convertToNotModified)
- .catch(convertToIfStatusCodeIs(404, HttpTimelinePostNotExistError))
- .catch(convertToNetworkError);
+ .then(extractResponseData);
}
- async postPost(
+ postPost(
timelineName: string,
req: HttpTimelinePostPostRequest
): Promise<HttpTimelinePostInfo> {
- let content: RawTimelinePostPostRequestContent;
- if (req.content.type === "image") {
- const base64Data = await base64(req.content.data);
- content = {
- ...req.content,
- data: base64Data,
- } as RawTimelinePostPostRequestImageContent;
- } else {
- content = req.content;
- }
- const rawReq: RawTimelinePostPostRequest = {
- content,
- };
- if (req.time != null) {
- rawReq.time = req.time.toISOString();
- }
- return await axios
- .post<RawTimelinePostInfo>(
+ return axios
+ .post<HttpTimelinePostInfo>(
`${apiBaseUrl}/timelines/${timelineName}/posts`,
- rawReq
+ req
)
- .then(extractResponseData)
- .catch(convertToNetworkError)
- .then((rawPost) => processRawTimelinePostInfo(rawPost));
+ .then(extractResponseData);
}
deletePost(timelineName: string, postId: number): Promise<void> {
return axios
.delete(`${apiBaseUrl}/timelines/${timelineName}/posts/${postId}`)
- .catch(convertToNetworkError)
.then();
}
}
diff --git a/FrontEnd/src/app/http/token.ts b/FrontEnd/src/app/http/token.ts
index c0644515..f8b09d63 100644
--- a/FrontEnd/src/app/http/token.ts
+++ b/FrontEnd/src/app/http/token.ts
@@ -4,7 +4,6 @@ import axios, { AxiosError } from "axios";
import {
apiBaseUrl,
- convertToNetworkError,
convertToIfErrorCodeIs,
extractResponseData,
} from "./common";
@@ -47,15 +46,13 @@ export class HttpTokenClient implements IHttpTokenClient {
.then(extractResponseData)
.catch(
convertToIfErrorCodeIs(11010101, HttpCreateTokenBadCredentialError)
- )
- .catch(convertToNetworkError);
+ );
}
verify(req: HttpVerifyTokenRequest): Promise<HttpVerifyTokenResponse> {
return axios
.post<HttpVerifyTokenResponse>(`${apiBaseUrl}/token/verify`, req)
- .then(extractResponseData)
- .catch(convertToNetworkError);
+ .then(extractResponseData);
}
}
diff --git a/FrontEnd/src/app/http/user.ts b/FrontEnd/src/app/http/user.ts
index 19accc42..c6a567d3 100644
--- a/FrontEnd/src/app/http/user.ts
+++ b/FrontEnd/src/app/http/user.ts
@@ -3,14 +3,9 @@ import { AxiosError } from "axios";
import {
axios,
apiBaseUrl,
- convertToNetworkError,
extractResponseData,
convertToIfStatusCodeIs,
convertToIfErrorCodeIs,
- NotModified,
- BlobWithEtag,
- convertToBlobWithEtag,
- convertToNotModified,
extractEtag,
} from "./common";
@@ -66,11 +61,6 @@ export interface IHttpUserClient {
get(username: string): Promise<HttpUser>;
patch(username: string, req: HttpUserPatchRequest): Promise<HttpUser>;
delete(username: string): Promise<void>;
- getAvatar(username: string): Promise<BlobWithEtag>;
- getAvatar(
- username: string,
- etag: string
- ): Promise<BlobWithEtag | NotModified>;
// return etag
putAvatar(username: string, data: Blob): Promise<string>;
changePassword(req: HttpChangePasswordRequest): Promise<void>;
@@ -90,53 +80,24 @@ export class HttpUserClient implements IHttpUserClient {
list(): Promise<HttpUser[]> {
return axios
.get<HttpUser[]>(`${apiBaseUrl}/users`)
- .then(extractResponseData)
- .catch(convertToNetworkError);
+ .then(extractResponseData);
}
get(username: string): Promise<HttpUser> {
return axios
.get<HttpUser>(`${apiBaseUrl}/users/${username}`)
.then(extractResponseData)
- .catch(convertToIfStatusCodeIs(404, HttpUserNotExistError))
- .catch(convertToNetworkError);
+ .catch(convertToIfStatusCodeIs(404, HttpUserNotExistError));
}
patch(username: string, req: HttpUserPatchRequest): Promise<HttpUser> {
return axios
.patch<HttpUser>(`${apiBaseUrl}/users/${username}`, req)
- .then(extractResponseData)
- .catch(convertToNetworkError);
+ .then(extractResponseData);
}
delete(username: string): Promise<void> {
- return axios
- .delete(`${apiBaseUrl}/users/${username}`)
- .catch(convertToNetworkError)
- .then();
- }
-
- getAvatar(username: string): Promise<BlobWithEtag>;
- getAvatar(
- username: string,
- etag?: string
- ): Promise<BlobWithEtag | NotModified> {
- const headers =
- etag != null
- ? {
- "If-None-Match": etag,
- }
- : undefined;
-
- return axios
- .get(`${apiBaseUrl}/users/${username}/avatar`, {
- responseType: "blob",
- headers,
- })
- .then(convertToBlobWithEtag)
- .catch(convertToNotModified)
- .catch(convertToIfStatusCodeIs(404, HttpUserNotExistError))
- .catch(convertToNetworkError);
+ return axios.delete(`${apiBaseUrl}/users/${username}`).then();
}
putAvatar(username: string, data: Blob): Promise<string> {
@@ -146,7 +107,6 @@ export class HttpUserClient implements IHttpUserClient {
"Content-Type": data.type,
},
})
- .catch(convertToNetworkError)
.then(extractEtag);
}
@@ -156,7 +116,6 @@ export class HttpUserClient implements IHttpUserClient {
.catch(
convertToIfErrorCodeIs(11020201, HttpChangePasswordBadCredentialError)
)
- .catch(convertToNetworkError)
.then();
}
@@ -166,7 +125,6 @@ export class HttpUserClient implements IHttpUserClient {
): Promise<void> {
return axios
.put(`${apiBaseUrl}/users/${username}/permissions/${permission}`)
- .catch(convertToNetworkError)
.then();
}
@@ -176,7 +134,6 @@ export class HttpUserClient implements IHttpUserClient {
): Promise<void> {
return axios
.delete(`${apiBaseUrl}/users/${username}/permissions/${permission}`)
- .catch(convertToNetworkError)
.then();
}
@@ -184,7 +141,6 @@ export class HttpUserClient implements IHttpUserClient {
return axios
.post<HttpUser>(`${apiBaseUrl}/userop/createuser`, req)
.then(extractResponseData)
- .catch(convertToNetworkError)
.then();
}
}
diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json
index 408950b1..9e40cb85 100644
--- a/FrontEnd/src/app/locales/en/translation.json
+++ b/FrontEnd/src/app/locales/en/translation.json
@@ -29,7 +29,6 @@
"relatedTimeline": "Timelines Related To You",
"publicTimeline": "Public Timelines",
"bookmarkTimeline": "Bookmark Timelines",
- "offlinePrompt": "Oh oh, it seems you are offline. Here list some timelines cached locally. You can view them or click <1>here</1> to refresh.",
"message": {
"moveHighlightFail": "Failed to move highlight timeline.",
"deleteHighlightFail": "Failed to delete highlight timeline.",
diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json
index 498a74e4..8d6dbcf3 100644
--- a/FrontEnd/src/app/locales/zh/translation.json
+++ b/FrontEnd/src/app/locales/zh/translation.json
@@ -29,7 +29,6 @@
"relatedTimeline": "关于你的时间线",
"publicTimeline": "公开时间线",
"bookmarkTimeline": "书签时间线",
- "offlinePrompt": "你好像处于离线状态。以下是一些缓存在本地的时间线。你可以查看它们或者<1>点击</1>重新获取在线信息。",
"message": {
"moveHighlightFail": "移动高光时间线失败。",
"deleteHighlightFail": "删除高光时间线失败。",
diff --git a/FrontEnd/src/app/services/DataHub2.ts b/FrontEnd/src/app/services/DataHub2.ts
deleted file mode 100644
index f0fb724b..00000000
--- a/FrontEnd/src/app/services/DataHub2.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-import { Observable } from "rxjs";
-
-export type DataStatus = "syncing" | "synced" | "offline";
-
-export function mergeDataStatus(statusList: DataStatus[]): DataStatus {
- if (statusList.includes("offline")) {
- return "offline";
- } else if (statusList.includes("syncing")) {
- return "syncing";
- } else {
- return "synced";
- }
-}
-
-export type Subscriber<TData> = (data: TData) => void;
-
-export interface DataAndStatus<TData> {
- data: TData | null;
- status: DataStatus;
-}
-
-export class DataLine2<TData> {
- constructor(
- private config: {
- saveData: (data: TData) => Promise<void>;
- getSavedData: () => Promise<TData | null>;
- // return null for offline
- fetchData: (savedData: TData | null) => Promise<TData | null>;
- }
- ) {}
-
- private _current: DataAndStatus<TData> | null = null;
- private _observers: Subscriber<DataAndStatus<TData>>[] = [];
-
- private _syncPromise: Promise<void> | null = null;
-
- get currentData(): DataAndStatus<TData> | null {
- return this._current;
- }
-
- get isDestroyable(): boolean {
- const { _observers, currentData } = this;
- return (
- _observers.length === 0 &&
- (currentData == null || currentData.status !== "syncing")
- );
- }
-
- private next(data: DataAndStatus<TData>): void {
- this._current = data;
- this._observers.forEach((o) => o(data));
- }
-
- subscribe(subsriber: Subscriber<DataAndStatus<TData>>): void {
- void this.sync(); // TODO: Should I sync at this point or let the user sync explicitly.
- this._observers.push(subsriber);
- const { currentData } = this;
- if (currentData != null) {
- subsriber(currentData);
- }
- }
-
- unsubscribe(subsriber: Subscriber<DataAndStatus<TData>>): void {
- const index = this._observers.indexOf(subsriber);
- if (index > -1) this._observers.splice(index, 1);
- }
-
- getObservalble(): Observable<DataAndStatus<TData>> {
- return new Observable<DataAndStatus<TData>>((observer) => {
- const f = (data: DataAndStatus<TData>): void => {
- observer.next(data);
- };
- this.subscribe(f);
-
- return () => {
- this.unsubscribe(f);
- };
- });
- }
-
- private syncWithAction(action: () => Promise<void>): Promise<void> {
- if (this._syncPromise != null) return this._syncPromise;
- this._syncPromise = action().then(() => {
- this._syncPromise = null;
- });
- return this._syncPromise;
- }
-
- sync(): Promise<void> {
- return this.syncWithAction(this.doSync.bind(this));
- }
-
- private async doSync(): Promise<void> {
- const { currentData } = this;
- this.next({ data: currentData?.data ?? null, status: "syncing" });
- const savedData = await this.config.getSavedData();
- if (currentData == null && savedData != null) {
- this.next({ data: savedData, status: "syncing" });
- }
- const data = await this.config.fetchData(savedData);
- if (data == null) {
- this.next({
- data: savedData,
- status: "offline",
- });
- } else {
- await this.config.saveData(data);
- this.next({ data: data, status: "synced" });
- }
- }
-
- save(data: TData): Promise<void> {
- return this.syncWithAction(this.doSave.bind(this, data));
- }
-
- private async doSave(data: TData): Promise<void> {
- await this.config.saveData(data);
- this.next({ data: data, status: "synced" });
- }
-
- getSavedData(): Promise<TData | null> {
- return this.config.getSavedData();
- }
-}
-
-export class DataHub2<TKey, TData> {
- private readonly subscriptionLineMap = new Map<string, DataLine2<TData>>();
-
- private keyToString: (key: TKey) => string;
-
- private cleanTimerId = 0;
-
- // setup is called after creating line and if it returns a function as destroyer, then when the line is destroyed the destroyer will be called.
- constructor(
- private config: {
- saveData: (key: TKey, data: TData) => Promise<void>;
- getSavedData: (key: TKey) => Promise<TData | null>;
- fetchData: (key: TKey, savedData: TData | null) => Promise<TData | null>;
- keyToString?: (key: TKey) => string;
- }
- ) {
- this.keyToString =
- config.keyToString ??
- ((value): string => {
- if (typeof value === "string") return value;
- else
- throw new Error(
- "Default keyToString function only pass string value."
- );
- });
- }
-
- private cleanLines(): void {
- const toDelete: string[] = [];
- for (const [key, line] of this.subscriptionLineMap.entries()) {
- if (line.isDestroyable) {
- toDelete.push(key);
- }
- }
-
- if (toDelete.length === 0) return;
-
- for (const key of toDelete) {
- this.subscriptionLineMap.delete(key);
- }
-
- if (this.subscriptionLineMap.size === 0) {
- window.clearInterval(this.cleanTimerId);
- this.cleanTimerId = 0;
- }
- }
-
- private createLine(key: TKey): DataLine2<TData> {
- const keyString = this.keyToString(key);
- const newLine: DataLine2<TData> = new DataLine2<TData>({
- saveData: (data) => this.config.saveData(key, data),
- getSavedData: () => this.config.getSavedData(key),
- fetchData: (savedData) => this.config.fetchData(key, savedData),
- });
- this.subscriptionLineMap.set(keyString, newLine);
- if (this.subscriptionLineMap.size === 1) {
- this.cleanTimerId = window.setInterval(this.cleanLines.bind(this), 20000);
- }
- return newLine;
- }
-
- getLine(key: TKey): DataLine2<TData> {
- const keyString = this.keyToString(key);
- return this.subscriptionLineMap.get(keyString) ?? this.createLine(key);
- }
-}
diff --git a/FrontEnd/src/app/services/common.ts b/FrontEnd/src/app/services/common.ts
deleted file mode 100644
index 9208737b..00000000
--- a/FrontEnd/src/app/services/common.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import localforage from "localforage";
-
-const dataVersion = 1;
-
-export const dataStorage = localforage.createInstance({
- name: "data",
- description: "Database for offline data.",
- driver: localforage.INDEXEDDB,
-});
-
-void (async () => {
- const currentVersion = await dataStorage.getItem<number | null>("version");
- if (currentVersion !== dataVersion) {
- console.log("Data storage version has changed. Clear all data.");
- await dataStorage.clear();
- await dataStorage.setItem("version", dataVersion);
- }
-})();
-
-export class ForbiddenError extends Error {
- constructor(message?: string) {
- super(message);
- }
-}
diff --git a/FrontEnd/src/app/services/timeline.ts b/FrontEnd/src/app/services/timeline.ts
index 46671ea1..d803521b 100644
--- a/FrontEnd/src/app/services/timeline.ts
+++ b/FrontEnd/src/app/services/timeline.ts
@@ -1,515 +1,7 @@
-import React from "react";
import XRegExp from "xregexp";
-import { Observable, from } from "rxjs";
-
-import { convertError } from "@/utilities/rxjs";
-import {
- TimelineVisibility,
- HttpTimelineInfo,
- HttpTimelinePatchRequest,
- HttpTimelinePostPostRequest,
- HttpTimelinePostPostRequestContent,
- HttpTimelinePostPostRequestTextContent,
- HttpTimelinePostPostRequestImageContent,
- HttpTimelinePostInfo,
- HttpTimelinePostTextContent,
- getHttpTimelineClient,
- HttpTimelineNotExistError,
- HttpTimelineNameConflictError,
-} from "@/http/timeline";
-import { HttpForbiddenError, HttpNetworkError } from "@/http/common";
-
-export { kTimelineVisibilities } from "@/http/timeline";
-
-export type { TimelineVisibility } from "@/http/timeline";
-
-import { dataStorage } from "./common";
-import { userInfoService, AuthUser } from "./user";
-import { DataAndStatus, DataHub2 } from "./DataHub2";
-import { getHttpBookmarkClient } from "@/http/bookmark";
-import { getHttpHighlightClient } from "@/http/highlight";
-
-export type TimelineInfo = HttpTimelineInfo;
-export type TimelineChangePropertyRequest = HttpTimelinePatchRequest;
-export type TimelineCreatePostRequest = HttpTimelinePostPostRequest;
-export type TimelineCreatePostContent = HttpTimelinePostPostRequestContent;
-export type TimelineCreatePostTextContent = HttpTimelinePostPostRequestTextContent;
-export type TimelineCreatePostImageContent = HttpTimelinePostPostRequestImageContent;
-
-export type TimelinePostTextContent = HttpTimelinePostTextContent;
-
-export interface TimelinePostImageContent {
- type: "image";
- data: Blob;
- etag: string;
-}
-
-export type TimelinePostContent =
- | TimelinePostTextContent
- | TimelinePostImageContent;
-
-export type TimelinePostInfo = Omit<HttpTimelinePostInfo, "content"> & {
- content: TimelinePostContent;
-};
-
-export interface TimelinePostsInfo {
- lastUpdated: Date;
- posts: TimelinePostInfo[];
-}
-
-export const timelineVisibilityTooltipTranslationMap: Record<
- TimelineVisibility,
- string
-> = {
- Public: "timeline.visibilityTooltip.public",
- Register: "timeline.visibilityTooltip.register",
- Private: "timeline.visibilityTooltip.private",
-};
-
-export class TimelineNameConflictError extends Error {}
-
-type TimelineData = Omit<HttpTimelineInfo, "owner" | "members"> & {
- owner: string;
- members: string[];
-};
-
-type TimelinePostData = Omit<TimelinePostInfo, "author"> & {
- author: string;
-};
-
-interface TimelinePostsData {
- lastUpdated: Date;
- posts: TimelinePostData[];
-}
-
-export class TimelineService {
- private async clearTimelineData(timelineName: string): Promise<void> {
- const keys = (await dataStorage.keys()).filter((k) =>
- k.startsWith(`timeline.${timelineName}`)
- );
- await Promise.all(keys.map((k) => dataStorage.removeItem(k)));
- }
-
- private generateTimelineDataStorageKey(timelineName: string): string {
- return `timeline.${timelineName}`;
- }
-
- private convertHttpTimelineToData(timeline: HttpTimelineInfo): TimelineData {
- return {
- ...timeline,
- owner: timeline.owner.username,
- members: timeline.members.map((m) => m.username),
- };
- }
-
- readonly timelineHub = new DataHub2<string, HttpTimelineInfo | "notexist">({
- saveData: async (timelineName, data) => {
- if (data === "notexist") return;
-
- // TODO: Avoid save same user.
- void userInfoService.saveUser(data.owner);
- void userInfoService.saveUsers(data.members);
-
- await dataStorage.setItem<TimelineData>(
- this.generateTimelineDataStorageKey(timelineName),
- this.convertHttpTimelineToData(data)
- );
- },
- getSavedData: async (timelineName) => {
- const savedData = await dataStorage.getItem<TimelineData | null>(
- this.generateTimelineDataStorageKey(timelineName)
- );
-
- if (savedData == null) return null;
-
- const owner = await userInfoService.getCachedUser(savedData.owner);
- if (owner == null) return null;
- const members = await userInfoService.getCachedUsers(savedData.members);
- if (members == null) return null;
-
- return { ...savedData, owner, members };
- },
- fetchData: async (timelineName, savedData) => {
- try {
- const timeline = await getHttpTimelineClient().getTimeline(
- timelineName
- );
-
- if (
- savedData != null &&
- savedData !== "notexist" &&
- savedData.uniqueId !== timeline.uniqueId
- ) {
- console.log(
- `Timeline with name ${timelineName} has changed to a new one. Clear old data.`
- );
-
- void this.clearTimelineData(timelineName); // If timeline has changed, clear all old data.
- }
-
- return timeline;
- } catch (e) {
- if (e instanceof HttpTimelineNotExistError) {
- return "notexist";
- } else if (e instanceof HttpNetworkError) {
- return null;
- } else {
- throw e;
- }
- }
- },
- });
-
- syncTimeline(timelineName: string): Promise<void> {
- return this.timelineHub.getLine(timelineName).sync();
- }
-
- createTimeline(timelineName: string): Observable<TimelineInfo> {
- return from(
- getHttpTimelineClient().postTimeline({
- name: timelineName,
- })
- ).pipe(
- convertError(HttpTimelineNameConflictError, TimelineNameConflictError)
- );
- }
-
- changeTimelineProperty(
- timelineName: string,
- req: TimelineChangePropertyRequest
- ): Promise<void> {
- return getHttpTimelineClient()
- .patchTimeline(timelineName, req)
- .then(() => {
- void this.syncTimeline(timelineName);
- });
- }
-
- deleteTimeline(timelineName: string): Observable<unknown> {
- return from(getHttpTimelineClient().deleteTimeline(timelineName));
- }
-
- addMember(timelineName: string, username: string): Promise<void> {
- return getHttpTimelineClient()
- .memberPut(timelineName, username)
- .then(() => {
- void this.syncTimeline(timelineName);
- });
- }
-
- removeMember(timelineName: string, username: string): Promise<void> {
- return getHttpTimelineClient()
- .memberDelete(timelineName, username)
- .then(() => {
- void this.syncTimeline(timelineName);
- });
- }
-
- private generatePostsDataStorageKey(timelineName: string): string {
- return `timeline.${timelineName}.posts`;
- }
-
- readonly postsHub = new DataHub2<
- string,
- TimelinePostsInfo | "notexist" | "forbid"
- >({
- saveData: async (timelineName, data) => {
- if (data === "notexist" || data === "forbid") return;
-
- const savedData: TimelinePostsData = {
- ...data,
- posts: data.posts.map((p) => ({ ...p, author: p.author.username })),
- };
-
- data.posts.forEach((p) => {
- void userInfoService.saveUser(p.author);
- });
-
- await dataStorage.setItem<TimelinePostsData>(
- this.generatePostsDataStorageKey(timelineName),
- savedData
- );
- },
- getSavedData: async (timelineName) => {
- const savedData = await dataStorage.getItem<TimelinePostsData | null>(
- this.generatePostsDataStorageKey(timelineName)
- );
- if (savedData == null) return null;
-
- const authors = await userInfoService.getCachedUsers(
- savedData.posts.map((p) => p.author)
- );
-
- if (authors == null) return null;
-
- return {
- ...savedData,
- posts: savedData.posts.map((p, index) => ({
- ...p,
- author: authors[index],
- })),
- };
- },
- fetchData: async (timelineName, savedData) => {
- const convert = async (
- post: HttpTimelinePostInfo
- ): Promise<TimelinePostInfo> => {
- const { content } = post;
- if (content.type === "text") {
- return { ...post, content };
- } else {
- const data = await getHttpTimelineClient().getPostData(
- timelineName,
- post.id
- );
- return {
- ...post,
- content: {
- type: "image",
- data: data.data,
- etag: data.etag,
- },
- };
- }
- };
-
- const convertList = (
- posts: HttpTimelinePostInfo[]
- ): Promise<TimelinePostInfo[]> =>
- Promise.all(posts.map((p) => convert(p)));
-
- const now = new Date();
-
- try {
- if (
- savedData == null ||
- savedData === "forbid" ||
- savedData === "notexist"
- ) {
- const httpPosts = await getHttpTimelineClient().listPost(
- timelineName
- );
-
- return {
- lastUpdated: now,
- posts: await convertList(httpPosts),
- };
- } else {
- const httpPosts = await getHttpTimelineClient().listPost(
- timelineName,
- {
- modifiedSince: savedData.lastUpdated,
- includeDeleted: true,
- }
- );
-
- const deletedIds = httpPosts
- .filter((p) => p.deleted)
- .map((p) => p.id);
-
- const changed = await convertList(
- httpPosts.filter((p): p is HttpTimelinePostInfo => !p.deleted)
- );
-
- const posts = savedData.posts.filter(
- (p) => !deletedIds.includes(p.id)
- );
-
- for (const changedPost of changed) {
- const savedChangedPostIndex = posts.findIndex(
- (p) => p.id === changedPost.id
- );
- if (savedChangedPostIndex === -1) {
- posts.push(await convert(changedPost));
- } else {
- posts[savedChangedPostIndex] = await convert(changedPost);
- }
- }
-
- return { lastUpdated: now, posts };
- }
- } catch (e) {
- if (e instanceof HttpTimelineNotExistError) {
- return "notexist";
- } else if (e instanceof HttpForbiddenError) {
- return "forbid";
- } else if (e instanceof HttpNetworkError) {
- return null;
- } else {
- throw e;
- }
- }
- },
- });
-
- syncPosts(timelineName: string): Promise<void> {
- return this.postsHub.getLine(timelineName).sync();
- }
-
- createPost(
- timelineName: string,
- request: TimelineCreatePostRequest
- ): Promise<void> {
- return getHttpTimelineClient()
- .postPost(timelineName, request)
- .then(() => {
- void this.syncPosts(timelineName);
- });
- }
-
- deletePost(timelineName: string, postId: number): Promise<void> {
- return getHttpTimelineClient()
- .deletePost(timelineName, postId)
- .then(() => {
- void this.syncPosts(timelineName);
- });
- }
-
- isMemberOf(username: string, timeline: TimelineInfo): boolean {
- return timeline.members.findIndex((m) => m.username == username) >= 0;
- }
-
- hasReadPermission(
- user: AuthUser | null | undefined,
- timeline: TimelineInfo
- ): boolean {
- if (user != null && user.hasAllTimelineAdministrationPermission)
- return true;
-
- const { visibility } = timeline;
- if (visibility === "Public") {
- return true;
- } else if (visibility === "Register") {
- if (user != null) return true;
- } else if (visibility === "Private") {
- if (
- user != null &&
- (user.username === timeline.owner.username ||
- this.isMemberOf(user.username, timeline))
- ) {
- return true;
- }
- }
- return false;
- }
-
- hasPostPermission(
- user: AuthUser | null | undefined,
- timeline: TimelineInfo
- ): boolean {
- if (user != null && user.hasAllTimelineAdministrationPermission)
- return true;
-
- return (
- user != null &&
- (timeline.owner.username === user.username ||
- this.isMemberOf(user.username, timeline))
- );
- }
-
- hasManagePermission(
- user: AuthUser | null | undefined,
- timeline: TimelineInfo
- ): boolean {
- if (user != null && user.hasAllTimelineAdministrationPermission)
- return true;
-
- return user != null && user.username == timeline.owner.username;
- }
-
- hasModifyPostPermission(
- user: AuthUser | null | undefined,
- timeline: TimelineInfo,
- post: TimelinePostInfo
- ): boolean {
- if (user != null && user.hasAllTimelineAdministrationPermission)
- return true;
-
- return (
- user != null &&
- (user.username === timeline.owner.username ||
- user.username === post.author.username)
- );
- }
-
- setHighlight(timelineName: string, highlight: boolean): Promise<void> {
- const client = getHttpHighlightClient();
- const promise = highlight
- ? client.put(timelineName)
- : client.delete(timelineName);
- return promise.then(() => {
- void timelineService.syncTimeline(timelineName);
- });
- }
-
- setBookmark(timelineName: string, bookmark: boolean): Promise<void> {
- const client = getHttpBookmarkClient();
- const promise = bookmark
- ? client.put(timelineName)
- : client.delete(timelineName);
- return promise.then(() => {
- void timelineService.syncTimeline(timelineName);
- });
- }
-}
-
-export const timelineService = new TimelineService();
const timelineNameReg = XRegExp("^[-_\\p{L}]*$", "u");
export function validateTimelineName(name: string): boolean {
return timelineNameReg.test(name);
}
-
-export function useTimeline(
- timelineName: string
-): DataAndStatus<TimelineInfo | "notexist"> {
- const [state, setState] = React.useState<
- DataAndStatus<TimelineInfo | "notexist">
- >({
- status: "syncing",
- data: null,
- });
- React.useEffect(() => {
- const subscription = timelineService.timelineHub
- .getLine(timelineName)
- .getObservalble()
- .subscribe((data) => {
- setState(data);
- });
- return () => {
- subscription.unsubscribe();
- };
- }, [timelineName]);
- return state;
-}
-
-export function usePosts(
- timelineName: string
-): DataAndStatus<TimelinePostsInfo | "notexist" | "forbid"> {
- const [state, setState] = React.useState<
- DataAndStatus<TimelinePostsInfo | "notexist" | "forbid">
- >({ status: "syncing", data: null });
- React.useEffect(() => {
- const subscription = timelineService.postsHub
- .getLine(timelineName)
- .getObservalble()
- .subscribe((data) => {
- setState(data);
- });
- return () => {
- subscription.unsubscribe();
- };
- }, [timelineName]);
- return state;
-}
-
-export async function getAllCachedTimelineNames(): Promise<string[]> {
- const keys = await dataStorage.keys();
- return keys
- .filter(
- (key) =>
- key.startsWith("timeline.") && (key.match(/\./g) ?? []).length === 1
- )
- .map((key) => key.substr("timeline.".length));
-}
diff --git a/FrontEnd/src/app/services/user.ts b/FrontEnd/src/app/services/user.ts
index 611a86ae..4814bf7c 100644
--- a/FrontEnd/src/app/services/user.ts
+++ b/FrontEnd/src/app/services/user.ts
@@ -1,33 +1,23 @@
-import React, { useState, useEffect } from "react";
-import { BehaviorSubject, Observable, from } from "rxjs";
+import { useState, useEffect } from "react";
+import { BehaviorSubject, Observable } from "rxjs";
import { UiLogicError } from "@/common";
-import {
- HttpNetworkError,
- BlobWithEtag,
- NotModified,
- setHttpToken,
-} from "@/http/common";
+import { HttpNetworkError, setHttpToken } from "@/http/common";
import {
getHttpTokenClient,
HttpCreateTokenBadCredentialError,
} from "@/http/token";
-import {
- getHttpUserClient,
- HttpUserNotExistError,
- HttpUser,
- UserPermission,
-} from "@/http/user";
+import { getHttpUserClient, HttpUser, UserPermission } from "@/http/user";
-import { DataHub2 } from "./DataHub2";
-import { dataStorage } from "./common";
import { pushAlert } from "./alert";
-export type User = HttpUser;
+interface IAuthUser extends HttpUser {
+ token: string;
+}
-export class AuthUser implements User {
- constructor(user: User, public 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;
@@ -87,9 +77,17 @@ export class UserService {
console.warn("Already checked user. Can't check twice.");
}
- const savedUser = await dataStorage.getItem<AuthUser | null>(
- USER_STORAGE_KEY
- );
+ 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);
@@ -102,7 +100,7 @@ export class UserService {
try {
const res = await getHttpTokenClient().verify({ token: savedToken });
const user = new AuthUser(res.user, savedToken);
- await dataStorage.setItem<AuthUser>(USER_STORAGE_KEY, user);
+ localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user));
this.userSubject.next(user);
pushAlert({
type: "success",
@@ -120,7 +118,7 @@ export class UserService {
});
return savedUser;
} else {
- await dataStorage.removeItem(USER_STORAGE_KEY);
+ localStorage.removeItem(USER_STORAGE_KEY);
this.userSubject.next(null);
pushAlert({
type: "danger",
@@ -145,7 +143,7 @@ export class UserService {
});
const user = new AuthUser(res.user, res.token);
if (rememberMe) {
- await dataStorage.setItem<AuthUser>(USER_STORAGE_KEY, user);
+ localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user));
}
this.userSubject.next(user);
} catch (e) {
@@ -157,34 +155,29 @@ export class UserService {
}
}
- async logout(): Promise<void> {
+ logout(): Promise<void> {
if (this.currentUser === undefined) {
throw new UiLogicError("Please check user first.");
}
if (this.currentUser === null) {
throw new UiLogicError("No login.");
}
- await dataStorage.removeItem(USER_STORAGE_KEY);
+ localStorage.removeItem(USER_STORAGE_KEY);
this.userSubject.next(null);
+ return Promise.resolve();
}
- changePassword(
- oldPassword: string,
- newPassword: string
- ): Observable<unknown> {
+ changePassword(oldPassword: string, newPassword: string): Promise<void> {
if (this.currentUser == undefined) {
throw new UiLogicError("Not login or checked now, can't log out.");
}
- const $ = from(
- getHttpUserClient().changePassword({
+
+ return getHttpUserClient()
+ .changePassword({
oldPassword,
newPassword,
})
- );
- $.subscribe(() => {
- void this.logout();
- });
- return $;
+ .then(() => this.logout());
}
}
@@ -236,156 +229,3 @@ export function useUserLoggedIn(): AuthUser {
}
return user;
}
-
-export function checkLogin(): AuthUser {
- const user = userService.currentUser;
- if (user == null) {
- throw new UiLogicError("You must login to perform the operation.");
- }
- return user;
-}
-
-export class UserNotExistError extends Error {}
-
-export class UserInfoService {
- saveUser(user: HttpUser): Promise<void> {
- return this.userHub.getLine(user.username).save(user);
- }
-
- saveUsers(users: HttpUser[]): Promise<void> {
- return Promise.all(users.map((user) => this.saveUser(user))).then();
- }
-
- async getCachedUser(username: string): Promise<HttpUser | null> {
- const user = await this.userHub.getLine(username).getSavedData();
- if (user == null || user === "notexist") return null;
- return user;
- }
-
- async getCachedUsers(usernames: string[]): Promise<HttpUser[] | null> {
- const users = await Promise.all(
- usernames.map((username) => this.userHub.getLine(username).getSavedData())
- );
-
- for (const u of users) {
- if (u == null || u === "notexist") {
- return null;
- }
- }
-
- return users as HttpUser[];
- }
-
- private generateUserDataStorageKey(username: string): string {
- return `user.${username}`;
- }
-
- readonly userHub = new DataHub2<string, HttpUser | "notexist">({
- saveData: (username, data) => {
- if (typeof data === "string") return Promise.resolve();
- return dataStorage
- .setItem<HttpUser>(this.generateUserDataStorageKey(username), data)
- .then();
- },
- getSavedData: (username) => {
- return dataStorage.getItem<HttpUser | null>(
- this.generateUserDataStorageKey(username)
- );
- },
- fetchData: async (username) => {
- try {
- return await getHttpUserClient().get(username);
- } catch (e) {
- if (e instanceof HttpUserNotExistError) {
- return "notexist";
- } else if (e instanceof HttpNetworkError) {
- return null;
- }
- throw e;
- }
- },
- });
-
- private generateAvatarDataStorageKey(username: string): string {
- return `user.${username}.avatar`;
- }
-
- readonly avatarHub = new DataHub2<string, BlobWithEtag | "notexist">({
- saveData: async (username, data) => {
- if (typeof data === "string") return;
- await dataStorage.setItem<BlobWithEtag>(
- this.generateAvatarDataStorageKey(username),
- data
- );
- },
- getSavedData: (username) =>
- dataStorage.getItem<BlobWithEtag | null>(
- this.generateAvatarDataStorageKey(username)
- ),
- fetchData: async (username, savedData) => {
- try {
- if (savedData == null || savedData === "notexist") {
- return await getHttpUserClient().getAvatar(username);
- } else {
- const res = await getHttpUserClient().getAvatar(
- username,
- savedData.etag
- );
- if (res instanceof NotModified) {
- return savedData;
- } else {
- return res;
- }
- }
- } catch (e) {
- if (e instanceof HttpUserNotExistError) {
- return "notexist";
- } else if (e instanceof HttpNetworkError) {
- return null;
- } else {
- throw e;
- }
- }
- },
- });
-
- async setAvatar(username: string, blob: Blob): Promise<void> {
- const etag = await getHttpUserClient().putAvatar(username, blob);
- await this.avatarHub.getLine(username).save({ data: blob, etag });
- }
-
- async setNickname(username: string, nickname: string): Promise<void> {
- return getHttpUserClient()
- .patch(username, { nickname })
- .then((user) => this.saveUser(user));
- }
-}
-
-export const userInfoService = new UserInfoService();
-
-export function useAvatar(username?: string): Blob | undefined {
- const [state, setState] = React.useState<Blob | undefined>(undefined);
- React.useEffect(() => {
- if (username == null) {
- setState(undefined);
- return;
- }
-
- const subscription = userInfoService.avatarHub
- .getLine(username)
- .getObservalble()
- .subscribe((data) => {
- if (data.data != null && data.data !== "notexist") {
- setState(data.data.data);
- } else {
- setState(undefined);
- }
- });
-
- return () => {
- subscription.unsubscribe();
- };
- }, [username]);
-
- return state;
-}
diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx
index fbdfd5a3..369eaf1e 100644
--- a/FrontEnd/src/app/views/admin/UserAdmin.tsx
+++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx
@@ -6,7 +6,7 @@ import OperationDialog, {
OperationBoolInputInfo,
} from "../common/OperationDialog";
-import { User, AuthUser } from "@/services/user";
+import { AuthUser } from "@/services/user";
import {
getHttpUserClient,
HttpUser,
@@ -199,7 +199,7 @@ type ContextMenuItem = TModify | TModifyPermission | TDelete;
interface UserItemProps {
on: { [key in ContextMenuItem]: () => void };
- user: User;
+ user: HttpUser;
}
const UserItem: React.FC<UserItemProps> = ({ user, on }) => {
@@ -273,7 +273,7 @@ const UserAdmin: React.FC<UserAdminProps> = (props) => {
}
| { type: TDelete; username: string };
- const [users, setUsers] = useState<User[] | null>(null);
+ const [users, setUsers] = useState<HttpUser[] | null>(null);
const [dialog, setDialog] = useState<DialogInfo>(null);
const [usersVersion, setUsersVersion] = useState<number>(0);
const updateUsers = (): void => {
diff --git a/FrontEnd/src/app/views/home/OfflineBoard.tsx b/FrontEnd/src/app/views/home/OfflineBoard.tsx
deleted file mode 100644
index fc05bd74..00000000
--- a/FrontEnd/src/app/views/home/OfflineBoard.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import React from "react";
-import { Link } from "react-router-dom";
-import { Trans } from "react-i18next";
-
-import { getAllCachedTimelineNames } from "@/services/timeline";
-import UserTimelineLogo from "../common/UserTimelineLogo";
-import TimelineLogo from "../common/TimelineLogo";
-
-export interface OfflineBoardProps {
- onReload: () => void;
-}
-
-const OfflineBoard: React.FC<OfflineBoardProps> = ({ onReload }) => {
- const [timelines, setTimelines] = React.useState<string[]>([]);
-
- React.useEffect(() => {
- let subscribe = true;
- void getAllCachedTimelineNames().then((t) => {
- if (subscribe) setTimelines(t);
- });
- return () => {
- subscribe = false;
- };
- });
-
- return (
- <>
- <Trans i18nKey="home.offlinePrompt">
- 0
- <a
- href="#"
- onClick={(e) => {
- onReload();
- e.preventDefault();
- }}
- >
- 1
- </a>
- 2
- </Trans>
- {timelines.map((timeline) => {
- const isPersonal = timeline.startsWith("@");
- const url = isPersonal
- ? `/users/${timeline.slice(1)}`
- : `/timelines/${timeline}`;
- return (
- <div key={timeline} className="timeline-board-item">
- {isPersonal ? (
- <UserTimelineLogo className="icon" />
- ) : (
- <TimelineLogo className="icon" />
- )}
- <Link to={url}>{timeline}</Link>
- </div>
- );
- })}
- </>
- );
-};
-
-export default OfflineBoard;
diff --git a/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx b/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx
index 5dcba612..b4e25ba1 100644
--- a/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx
+++ b/FrontEnd/src/app/views/home/TimelineCreateDialog.tsx
@@ -1,12 +1,9 @@
import React from "react";
import { useHistory } from "react-router";
-import {
- validateTimelineName,
- timelineService,
- TimelineInfo,
-} from "@/services/timeline";
+import { validateTimelineName } from "@/services/timeline";
import OperationDialog from "../common/OperationDialog";
+import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline";
interface TimelineCreateDialogProps {
open: boolean;
@@ -42,10 +39,10 @@ const TimelineCreateDialog: React.FC<TimelineCreateDialogProps> = (props) => {
return null;
}
}}
- onProcess={([name]): Promise<TimelineInfo> => {
- return timelineService.createTimeline(name).toPromise();
- }}
- onSuccessAndClose={(timeline: TimelineInfo) => {
+ onProcess={([name]): Promise<HttpTimelineInfo> =>
+ getHttpTimelineClient().postTimeline({ name })
+ }
+ onSuccessAndClose={(timeline: HttpTimelineInfo) => {
history.push(`timelines/${timeline.name}`);
}}
failurePrompt={(e) => `${e as string}`}