diff options
Diffstat (limited to 'FrontEnd/src/app/services/timeline.ts')
-rw-r--r-- | FrontEnd/src/app/services/timeline.ts | 508 |
1 files changed, 0 insertions, 508 deletions
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)); -} |