aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/app
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2021-02-13 16:18:50 +0800
committercrupest <crupest@outlook.com>2021-02-13 16:18:50 +0800
commit17272858aaf09eac5a3550b23e97f8d339847bd9 (patch)
tree2317d17f5f515d5c647c1bfcd32413b3529bc0ba /FrontEnd/src/app
parenta7cc64ca2b30b47c57cae2115e10f34f361c90b9 (diff)
downloadtimeline-17272858aaf09eac5a3550b23e97f8d339847bd9.tar.gz
timeline-17272858aaf09eac5a3550b23e97f8d339847bd9.tar.bz2
timeline-17272858aaf09eac5a3550b23e97f8d339847bd9.zip
...
Diffstat (limited to 'FrontEnd/src/app')
-rw-r--r--FrontEnd/src/app/http/timeline.ts2
-rw-r--r--FrontEnd/src/app/services/timeline.ts10
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx19
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelineMember.tsx9
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx157
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx18
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx109
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);