diff options
author | crupest <crupest@outlook.com> | 2021-02-13 16:18:50 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2021-02-13 16:18:50 +0800 |
commit | 8211994b7175f2ad04663abc0c2605eb96143841 (patch) | |
tree | 0dd22de5cbcbbc3faaba02da4e1f6539dfa2f3ef /FrontEnd/src | |
parent | c9737a9c5221e48e2de9113fad80a8653edc0752 (diff) | |
download | timeline-8211994b7175f2ad04663abc0c2605eb96143841.tar.gz timeline-8211994b7175f2ad04663abc0c2605eb96143841.tar.bz2 timeline-8211994b7175f2ad04663abc0c2605eb96143841.zip |
...
Diffstat (limited to 'FrontEnd/src')
7 files changed, 152 insertions, 172 deletions
diff --git a/FrontEnd/src/app/http/timeline.ts b/FrontEnd/src/app/http/timeline.ts index bfb17a42..375a2325 100644 --- a/FrontEnd/src/app/http/timeline.ts +++ b/FrontEnd/src/app/http/timeline.ts @@ -26,6 +26,8 @@ export interface HttpTimelineInfo { members: HttpUser[]; isHighlight: boolean; isBookmark: boolean; + manageable: boolean; + postable: boolean; } export interface HttpTimelineListQuery { diff --git a/FrontEnd/src/app/services/timeline.ts b/FrontEnd/src/app/services/timeline.ts index d803521b..a24ec8eb 100644 --- a/FrontEnd/src/app/services/timeline.ts +++ b/FrontEnd/src/app/services/timeline.ts @@ -1,3 +1,4 @@ +import { TimelineVisibility } from "@/http/timeline"; import XRegExp from "xregexp"; const timelineNameReg = XRegExp("^[-_\\p{L}]*$", "u"); @@ -5,3 +6,12 @@ const timelineNameReg = XRegExp("^[-_\\p{L}]*$", "u"); export function validateTimelineName(name: string): boolean { return timelineNameReg.test(name); } + +export const timelineVisibilityTooltipTranslationMap: Record< + TimelineVisibility, + string +> = { + Public: "timeline.visibilityTooltip.public", + Register: "timeline.visibilityTooltip.register", + Private: "timeline.visibilityTooltip.private", +}; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx index 53312758..d6eaa16c 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx @@ -3,10 +3,15 @@ import clsx from "clsx"; import { useTranslation } from "react-i18next"; import { Dropdown, Button } from "react-bootstrap"; -import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; -import CollapseButton from "../timeline-common/CollapseButton"; +import { getHttpHighlightClient } from "@/http/highlight"; +import { getHttpBookmarkClient } from "@/http/bookmark"; + import { useUser } from "@/services/user"; import { pushAlert } from "@/services/alert"; +import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; + +import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; +import CollapseButton from "../timeline-common/CollapseButton"; export interface TimelineCardTemplateProps extends Omit<TimelineCardComponentProps<"">, "operations"> { @@ -33,7 +38,6 @@ function TimelineCardTemplate({ infoArea, manageArea, toggleCollapse, - syncStatus, className, }: TimelineCardTemplateProps): React.ReactElement | null { const { t } = useTranslation(); @@ -43,7 +47,6 @@ function TimelineCardTemplate({ return ( <div className={clsx("cru-card p-2 clearfix", className)}> <div className="float-right d-flex align-items-center"> - <SyncStatusBadge status={syncStatus} className="mr-2" /> <CollapseButton collapse={collapse} onClick={toggleCollapse} /> </div> <div style={{ display: collapse ? "none" : "block" }}> @@ -61,8 +64,8 @@ function TimelineCardTemplate({ onClick={ user != null && user.hasHighlightTimelineAdministrationPermission ? () => { - timelineService - .setHighlight(timeline.name, !timeline.isHighlight) + getHttpHighlightClient() + [timeline.isHighlight ? "delete" : "put"](timeline.name) .catch(() => { pushAlert({ message: { @@ -85,8 +88,8 @@ function TimelineCardTemplate({ "icon-button text-yellow mr-3" )} onClick={() => { - timelineService - .setBookmark(timeline.name, !timeline.isBookmark) + getHttpBookmarkClient() + [timeline.isBookmark ? "delete" : "put"](timeline.name) .catch(() => { pushAlert({ message: { diff --git a/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx index b5f8c0a2..dd8c7389 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx @@ -143,11 +143,10 @@ const TimelineMemberUserSearch: React.FC<{ timeline: HttpTimelineInfo }> = ({ export interface TimelineMemberProps { timeline: HttpTimelineInfo; - editable: boolean; } const TimelineMember: React.FC<TimelineMemberProps> = (props) => { - const { timeline, editable } = props; + const { timeline } = props; const members = [timeline.owner, ...timeline.members]; return ( @@ -158,7 +157,7 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => { key={member.username} user={member} onAction={ - editable && index !== 0 + timeline.manageable && index !== 0 ? () => { void getHttpTimelineClient().memberDelete( timeline.name, @@ -170,7 +169,9 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => { /> ))} </ListGroup> - {editable ? <TimelineMemberUserSearch timeline={timeline} /> : null} + {timeline.manageable ? ( + <TimelineMemberUserSearch timeline={timeline} /> + ) : null} </Container> ); }; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx index 9b76635e..caab1768 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx @@ -1,21 +1,13 @@ import React from "react"; import { UiLogicError } from "@/common"; -import { useUser } from "@/services/user"; -import { - TimelinePostInfo, - timelineService, - usePosts, - useTimeline, -} from "@/services/timeline"; -import { mergeDataStatus } from "@/services/DataHub2"; + +import { HttpNetworkError, HttpNotFoundError } from "@/http/common"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; import { TimelineMemberDialog } from "./TimelineMember"; import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; -import { - TimelinePageTemplateUIOperations, - TimelinePageTemplateUIProps, -} from "./TimelinePageTemplateUI"; +import { TimelinePageTemplateUIProps } from "./TimelinePageTemplateUI"; export interface TimelinePageTemplateProps<TManageItem> { name: string; @@ -31,95 +23,58 @@ export default function TimelinePageTemplate<TManageItem>( ): React.ReactElement | null { const { name } = props; - const service = timelineService; - - const user = useUser(); - const [dialog, setDialog] = React.useState<null | "property" | "member">( null ); - const [scrollBottomKey, setScrollBottomKey] = React.useState<number>(0); - - React.useEffect(() => { - if (scrollBottomKey > 0) { - window.scrollTo(0, document.body.scrollHeight); - } - }, [scrollBottomKey]); + // TODO: Auto scroll. + // const [scrollBottomKey, _setScrollBottomKey] = React.useState<number>(0); - const timelineAndStatus = useTimeline(name); - const postsAndState = usePosts(name); + // React.useEffect(() => { + // if (scrollBottomKey > 0) { + // window.scrollTo(0, document.body.scrollHeight); + // } + // }, [scrollBottomKey]); - const [ - scrollToBottomNextSyncKey, - setScrollToBottomNextSyncKey, - ] = React.useState<number>(0); - - const scrollToBottomNextSync = (): void => { - setScrollToBottomNextSyncKey((old) => old + 1); - }; + const [timeline, setTimeline] = React.useState< + HttpTimelineInfo | "loading" | "offline" | "notexist" | "error" + >("loading"); React.useEffect(() => { + setTimeline("loading"); + let subscribe = true; - void timelineService.syncPosts(name).then(() => { - if (subscribe) { - setScrollBottomKey((old) => old + 1); - } - }); + void getHttpTimelineClient() + .getTimeline(name) + .then( + (data) => { + if (subscribe) { + setTimeline(data); + } + }, + (error) => { + if (subscribe) { + if (error instanceof HttpNetworkError) { + setTimeline("offline"); + } else if (error instanceof HttpNotFoundError) { + setTimeline("notexist"); + } else { + console.error(error); + setTimeline("error"); + } + } + } + ); return () => { subscribe = false; }; - }, [name, scrollToBottomNextSyncKey]); - - const uiTimelineProp = ((): TimelinePageTemplateUIProps<TManageItem>["timeline"] => { - const { status, data: timeline } = timelineAndStatus; - if (timeline == null) { - if (status === "offline") { - return "offline"; - } else { - return undefined; - } - } else if (timeline === "notexist") { - return "notexist"; - } else { - const operations: TimelinePageTemplateUIOperations<TManageItem> = { - onPost: service.hasPostPermission(user, timeline) - ? (req) => - service.createPost(name, req).then(() => scrollToBottomNextSync()) - : undefined, - onManage: service.hasManagePermission(user, timeline) - ? (item) => { - if (item === "property") { - setDialog(item); - } else { - props.onManage(item); - } - } - : undefined, - onMember: () => setDialog("member"), - }; - - const posts = ((): TimelinePostInfo[] | "forbid" | undefined => { - const { data: postsInfo } = postsAndState; - if (postsInfo === "forbid") { - return "forbid"; - } else if (postsInfo == null || postsInfo === "notexist") { - return undefined; - } else { - return postsInfo.posts; - } - })(); - - return { ...timeline, operations, posts }; - } - })(); + }, [name]); - const timeline = timelineAndStatus?.data; let dialogElement: React.ReactElement | undefined; const closeDialog = (): void => setDialog(null); if (dialog === "property") { - if (timeline == null || timeline === "notexist") { + if (typeof timeline !== "object") { throw new UiLogicError( "Timeline is null but attempt to open change property dialog." ); @@ -130,23 +85,17 @@ export default function TimelinePageTemplate<TManageItem>( open close={closeDialog} timeline={timeline} - onProcess={(req) => service.changeTimelineProperty(name, req)} /> ); } else if (dialog === "member") { - if (timeline == null || timeline === "notexist") { + if (typeof timeline !== "object") { throw new UiLogicError( "Timeline is null but attempt to open change property dialog." ); } dialogElement = ( - <TimelineMemberDialog - open - onClose={closeDialog} - timeline={timeline} - editable={service.hasManagePermission(user, timeline)} - /> + <TimelineMemberDialog open onClose={closeDialog} timeline={timeline} /> ); } @@ -155,11 +104,25 @@ export default function TimelinePageTemplate<TManageItem>( return ( <> <UiComponent - timeline={uiTimelineProp} - syncStatus={mergeDataStatus([ - timelineAndStatus.status, - postsAndState.status, - ])} + timeline={ + typeof timeline === "object" + ? { + ...timeline, + operations: { + onManage: timeline.manageable + ? (item) => { + if (item === "property") { + setDialog(item); + } else { + props.onManage(item); + } + } + : undefined, + onMember: () => setDialog("member"), + }, + } + : timeline + } notExistMessageI18nKey={props.notFoundI18nKey} /> {dialogElement} diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx index b0d3fe97..48263486 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx @@ -7,7 +7,7 @@ import { getAlertHost } from "@/services/alert"; import { HttpTimelineInfo } from "@/http/timeline"; import Timeline from "./Timeline"; -import TimelinePostEdit, { TimelinePostSendCallback } from "./TimelinePostEdit"; +import TimelinePostEdit from "./TimelinePostEdit"; export interface TimelineCardComponentProps<TManageItems> { timeline: HttpTimelineInfo; @@ -25,16 +25,17 @@ export interface TimelinePageTemplateUIOperations<TManageItems> { onMember: () => void; onBookmark?: () => void; onHighlight?: () => void; - onPost?: TimelinePostSendCallback; } export interface TimelinePageTemplateUIProps<TManageItems> { - timeline?: + timeline: | (HttpTimelineInfo & { operations: TimelinePageTemplateUIOperations<TManageItems>; }) | "notexist" - | "offline"; + | "offline" + | "loading" + | "error"; notExistMessageI18nKey: string; CardComponent: React.ComponentType<TimelineCardComponentProps<TManageItems>>; } @@ -90,7 +91,7 @@ export default function TimelinePageTemplateUI<TManageItems>( let body: React.ReactElement; - if (timeline == null) { + if (timeline == "loading") { body = ( <div className="full-viewport-center-child"> <Spinner variant="primary" animation="grow" /> @@ -101,6 +102,9 @@ export default function TimelinePageTemplateUI<TManageItems>( body = <p className="text-danger">Offline!</p>; } else if (timeline === "notexist") { body = <p className="text-danger">{t(props.notExistMessageI18nKey)}</p>; + } else if (timeline === "error") { + // TODO: i18n + body = <p className="text-danger">Error!</p>; } else { const { operations } = timeline; body = ( @@ -120,7 +124,7 @@ export default function TimelinePageTemplateUI<TManageItems>( > <Timeline timelineName={timeline.name} /> </div> - {operations.onPost != null ? ( + {timeline.postable ? ( <> <div style={{ height: bottomSpaceHeight }} @@ -128,7 +132,7 @@ export default function TimelinePageTemplateUI<TManageItems>( /> <TimelinePostEdit className="fixed-bottom" - onPost={operations.onPost} + timeline={timeline} onHeightChange={onPostEditHeightChange} timelineUniqueId={timeline.uniqueId} /> diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx index 207bf6af..488b627c 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx @@ -6,7 +6,7 @@ import { Button, Spinner, Row, Col, Form } from "react-bootstrap"; import { UiLogicError } from "@/common"; import { pushAlert } from "@/services/alert"; -import { TimelineCreatePostRequest } from "@/services/timeline"; +import { HttpTimelineInfo } from "@/http/timeline"; interface TimelinePostEditImageProps { onSelect: (blob: Blob | null) => void; @@ -74,26 +74,20 @@ const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => { ); }; -export type TimelinePostSendCallback = ( - content: TimelineCreatePostRequest -) => Promise<void>; - export interface TimelinePostEditProps { className?: string; - onPost: TimelinePostSendCallback; + timeline: HttpTimelineInfo; onHeightChange?: (height: number) => void; - timelineUniqueId: string; } const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { - const { onPost } = props; - const { t } = useTranslation(); + const { timeline } = props; + const [state, setState] = React.useState<"input" | "process">("input"); const [kind, setKind] = React.useState<"text" | "image">("text"); const [text, setText] = React.useState<string>(""); - const [imageBlob, setImageBlob] = React.useState<Blob | null>(null); const draftLocalStorageKey = `timeline.${props.timelineUniqueId}.postDraft`; @@ -124,57 +118,60 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { }); const toggleKind = React.useCallback(() => { - setKind((oldKind) => (oldKind === "text" ? "image" : "text")); - setImageBlob(null); + // TODO: Implement this. + // setKind((oldKind) => (oldKind === "text" ? "image" : "text")); + // setImageBlob(null); }, []); const onSend = React.useCallback(() => { setState("process"); - const req: TimelineCreatePostRequest = (() => { - switch (kind) { - case "text": - return { - content: { - type: "text", - text: text, - }, - } as TimelineCreatePostRequest; - case "image": - if (imageBlob == null) { - throw new UiLogicError( - "Content type is image but image blob is null." - ); - } - return { - content: { - type: "image", - data: imageBlob, - }, - } as TimelineCreatePostRequest; - default: - throw new UiLogicError("Unknown content type."); - } - })(); - - onPost(req).then( - (_) => { - if (kind === "text") { - setText(""); - window.localStorage.removeItem(draftLocalStorageKey); - } - setState("input"); - setKind("text"); - }, - (_) => { - pushAlert({ - type: "danger", - message: t("timeline.sendPostFailed"), - }); - setState("input"); - } - ); - }, [onPost, kind, text, imageBlob, t, draftLocalStorageKey]); + // TODO: Implement this. + + // const req: TimelineCreatePostRequest = (() => { + // switch (kind) { + // case "text": + // return { + // content: { + // type: "text", + // text: text, + // }, + // } as TimelineCreatePostRequest; + // case "image": + // if (imageBlob == null) { + // throw new UiLogicError( + // "Content type is image but image blob is null." + // ); + // } + // return { + // content: { + // type: "image", + // data: imageBlob, + // }, + // } as TimelineCreatePostRequest; + // default: + // throw new UiLogicError("Unknown content type."); + // } + // })(); + + // onPost(req).then( + // (_) => { + // if (kind === "text") { + // setText(""); + // window.localStorage.removeItem(draftLocalStorageKey); + // } + // setState("input"); + // setKind("text"); + // }, + // (_) => { + // pushAlert({ + // type: "danger", + // message: t("timeline.sendPostFailed"), + // }); + // setState("input"); + // } + // ); + }, []); const onImageSelect = React.useCallback((blob: Blob | null) => { setImageBlob(blob); |