diff options
Diffstat (limited to 'FrontEnd/src/app')
-rw-r--r-- | FrontEnd/src/app/http/bookmark.ts | 34 | ||||
-rw-r--r-- | FrontEnd/src/app/http/common.ts | 69 | ||||
-rw-r--r-- | FrontEnd/src/app/http/highlight.ts | 34 | ||||
-rw-r--r-- | FrontEnd/src/app/http/search.ts | 22 | ||||
-rw-r--r-- | FrontEnd/src/app/http/timeline.ts | 324 | ||||
-rw-r--r-- | FrontEnd/src/app/http/token.ts | 7 | ||||
-rw-r--r-- | FrontEnd/src/app/http/user.ts | 52 | ||||
-rw-r--r-- | FrontEnd/src/app/locales/en/translation.json | 1 | ||||
-rw-r--r-- | FrontEnd/src/app/locales/zh/translation.json | 1 | ||||
-rw-r--r-- | FrontEnd/src/app/services/DataHub2.ts | 191 | ||||
-rw-r--r-- | FrontEnd/src/app/services/common.ts | 24 | ||||
-rw-r--r-- | FrontEnd/src/app/services/timeline.ts | 508 | ||||
-rw-r--r-- | FrontEnd/src/app/services/user.ts | 222 | ||||
-rw-r--r-- | FrontEnd/src/app/views/admin/UserAdmin.tsx | 6 | ||||
-rw-r--r-- | FrontEnd/src/app/views/home/OfflineBoard.tsx | 61 | ||||
-rw-r--r-- | FrontEnd/src/app/views/home/TimelineCreateDialog.tsx | 15 |
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}`} |