From b78d21a524f7a11ad29b4bd230f23825f80c3ed7 Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 26 Jul 2020 15:02:55 +0800 Subject: Merge front end repo --- Timeline/ClientApp/src/app/data/timeline.ts | 265 ++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 Timeline/ClientApp/src/app/data/timeline.ts (limited to 'Timeline/ClientApp/src/app/data/timeline.ts') diff --git a/Timeline/ClientApp/src/app/data/timeline.ts b/Timeline/ClientApp/src/app/data/timeline.ts new file mode 100644 index 00000000..dde204be --- /dev/null +++ b/Timeline/ClientApp/src/app/data/timeline.ts @@ -0,0 +1,265 @@ +import React from 'react'; +import XRegExp from 'xregexp'; +import { Observable, from } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { UserAuthInfo, checkLogin, userService } from './user'; + +import { BlobWithUrl } from './common'; +import { SubscriptionHub, ISubscriptionHub } from './SubscriptionHub'; + +export { kTimelineVisibilities } from '../http/timeline'; + +export type { TimelineVisibility } from '../http/timeline'; + +import { + TimelineVisibility, + HttpTimelineInfo, + HttpTimelinePatchRequest, + HttpTimelinePostPostRequest, + HttpTimelinePostPostRequestContent, + HttpTimelinePostPostRequestTextContent, + HttpTimelinePostPostRequestImageContent, + HttpTimelinePostInfo, + HttpTimelinePostContent, + HttpTimelinePostTextContent, + HttpTimelinePostImageContent, + getHttpTimelineClient, + HttpTimelineNotExistError, + HttpTimelineNameConflictError, +} from '../http/timeline'; +import { convertError } from '../utilities/rxjs'; + +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 interface TimelinePostInfo extends HttpTimelinePostInfo { + timelineName: string; +} + +export type TimelinePostContent = HttpTimelinePostContent; +export type TimelinePostTextContent = HttpTimelinePostTextContent; +export type TimelinePostImageContent = HttpTimelinePostImageContent; + +export const timelineVisibilityTooltipTranslationMap: Record< + TimelineVisibility, + string +> = { + Public: 'timeline.visibilityTooltip.public', + Register: 'timeline.visibilityTooltip.register', + Private: 'timeline.visibilityTooltip.private', +}; + +export class TimelineNotExistError extends Error {} +export class TimelineNameConflictError extends Error {} + +export interface PostKey { + timelineName: string; + postId: number; +} + +export class TimelineService { + getTimeline(timelineName: string): Observable { + return from(getHttpTimelineClient().getTimeline(timelineName)).pipe( + convertError(HttpTimelineNotExistError, TimelineNotExistError) + ); + } + + createTimeline(timelineName: string): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient().postTimeline( + { + name: timelineName, + }, + user.token + ) + ).pipe( + convertError(HttpTimelineNameConflictError, TimelineNameConflictError) + ); + } + + changeTimelineProperty( + timelineName: string, + req: TimelineChangePropertyRequest + ): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient().patchTimeline(timelineName, req, user.token) + ); + } + + deleteTimeline(timelineName: string): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient().deleteTimeline(timelineName, user.token) + ); + } + + addMember(timelineName: string, username: string): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient().memberPut(timelineName, username, user.token) + ); + } + + removeMember(timelineName: string, username: string): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient().memberDelete(timelineName, username, user.token) + ); + } + + getPosts(timelineName: string): Observable { + const token = userService.currentUser?.token; + return from(getHttpTimelineClient().listPost(timelineName, token)).pipe( + map((posts) => { + return posts.map((post) => ({ + ...post, + timelineName, + })); + }) + ); + } + + private _postDataSubscriptionHub = new SubscriptionHub( + (key) => `${key.timelineName}/${key.postId}`, + async (key) => { + const blob = ( + await getHttpTimelineClient().getPostData( + key.timelineName, + key.postId, + userService.currentUser?.token + ) + ).data; + const url = URL.createObjectURL(blob); + return { + blob, + url, + }; + }, + (_key, data) => { + URL.revokeObjectURL(data.url); + } + ); + + get postDataHub(): ISubscriptionHub { + return this._postDataSubscriptionHub; + } + + createPost( + timelineName: string, + request: TimelineCreatePostRequest + ): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient().postPost(timelineName, request, user.token) + ).pipe(map((post) => ({ ...post, timelineName }))); + } + + deletePost(timelineName: string, postId: number): Observable { + const user = checkLogin(); + return from( + getHttpTimelineClient().deletePost(timelineName, postId, user.token) + ); + } + + isMemberOf(username: string, timeline: TimelineInfo): boolean { + return timeline.members.findIndex((m) => m.username == username) >= 0; + } + + hasReadPermission( + user: UserAuthInfo | null | undefined, + timeline: TimelineInfo + ): boolean { + if (user != null && user.administrator) 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 && this.isMemberOf(user.username, timeline)) { + return true; + } + } + return false; + } + + hasPostPermission( + user: UserAuthInfo | null | undefined, + timeline: TimelineInfo + ): boolean { + if (user != null && user.administrator) return true; + + return ( + user != null && + (timeline.owner.username === user.username || + this.isMemberOf(user.username, timeline)) + ); + } + + hasManagePermission( + user: UserAuthInfo | null | undefined, + timeline: TimelineInfo + ): boolean { + if (user != null && user.administrator) return true; + + return user != null && user.username == timeline.owner.username; + } + + hasModifyPostPermission( + user: UserAuthInfo | null | undefined, + timeline: TimelineInfo, + post: TimelinePostInfo + ): boolean { + if (user != null && user.administrator) return true; + + return ( + user != null && + (user.username === timeline.owner.username || + user.username === post.author.username) + ); + } +} + +export const timelineService = new TimelineService(); + +const timelineNameReg = XRegExp('^[-_\\p{L}]*$', 'u'); + +export function validateTimelineName(name: string): boolean { + return timelineNameReg.test(name); +} + +export function usePostDataUrl( + enable: boolean, + timelineName: string, + postId: number +): string | undefined { + const [url, setUrl] = React.useState(undefined); + React.useEffect(() => { + if (!enable) { + setUrl(undefined); + return; + } + + const subscription = timelineService.postDataHub.subscribe( + { + timelineName, + postId, + }, + ({ url }) => { + setUrl(url); + } + ); + return () => { + subscription.unsubscribe(); + }; + }, [timelineName, postId, enable]); + return url; +} -- cgit v1.2.3