From 790fc48e013ecd424d73e45072607927a3a43a70 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 13 Feb 2021 15:31:49 +0800 Subject: ... --- FrontEnd/src/app/http/timeline.ts | 2 + .../app/views/timeline-common/SyncStatusBadge.tsx | 58 ----------- .../src/app/views/timeline-common/Timeline.tsx | 70 +------------ .../views/timeline-common/TimelineCardTemplate.tsx | 6 -- .../app/views/timeline-common/TimelineDateItem.tsx | 19 ---- .../views/timeline-common/TimelineDateLabel.tsx | 19 ++++ .../src/app/views/timeline-common/TimelineItem.tsx | 108 --------------------- .../app/views/timeline-common/TimelineMember.tsx | 24 ++--- .../views/timeline-common/TimelinePostListView.tsx | 73 ++++++++++++++ .../app/views/timeline-common/TimelinePostView.tsx | 95 ++++++++++++++++++ .../TimelinePropertyChangeDialog.tsx | 18 ++-- 11 files changed, 213 insertions(+), 279 deletions(-) delete mode 100644 FrontEnd/src/app/views/timeline-common/SyncStatusBadge.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelineDateItem.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/TimelineDateLabel.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelineItem.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx create mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx diff --git a/FrontEnd/src/app/http/timeline.ts b/FrontEnd/src/app/http/timeline.ts index a84a40ef..bfb17a42 100644 --- a/FrontEnd/src/app/http/timeline.ts +++ b/FrontEnd/src/app/http/timeline.ts @@ -51,6 +51,8 @@ export interface HttpTimelinePostInfo { dataList: HttpTimelinePostDataDigest; color: string; lastUpdated: string; + timelineName: string; + editable: boolean; } export interface HttpTimelinePostPostRequest { diff --git a/FrontEnd/src/app/views/timeline-common/SyncStatusBadge.tsx b/FrontEnd/src/app/views/timeline-common/SyncStatusBadge.tsx deleted file mode 100644 index e67cfb43..00000000 --- a/FrontEnd/src/app/views/timeline-common/SyncStatusBadge.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import { useTranslation } from "react-i18next"; - -import { UiLogicError } from "@/common"; - -export type TimelineSyncStatus = "syncing" | "synced" | "offline"; - -const SyncStatusBadge: React.FC<{ - status: TimelineSyncStatus; - style?: React.CSSProperties; - className?: string; -}> = ({ status, style, className }) => { - const { t } = useTranslation(); - - return ( -
- {(() => { - switch (status) { - case "syncing": { - return ( - <> - - - {t("timeline.postSyncState.syncing")} - - - ); - } - case "synced": { - return ( - <> - - - {t("timeline.postSyncState.synced")} - - - ); - } - case "offline": { - return ( - <> - - - {t("timeline.postSyncState.offline")} - - - ); - } - default: - throw new UiLogicError("Unknown sync state."); - } - })()} -
- ); -}; - -export default SyncStatusBadge; diff --git a/FrontEnd/src/app/views/timeline-common/Timeline.tsx b/FrontEnd/src/app/views/timeline-common/Timeline.tsx index 7aa645f5..d970af84 100644 --- a/FrontEnd/src/app/views/timeline-common/Timeline.tsx +++ b/FrontEnd/src/app/views/timeline-common/Timeline.tsx @@ -1,5 +1,4 @@ import React from "react"; -import clsx from "clsx"; import { HttpForbiddenError, @@ -8,16 +7,7 @@ import { } from "@/http/common"; import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; -import TimelineItem from "./TimelineItem"; -import TimelineDateItem from "./TimelineDateItem"; - -function dateEqual(left: Date, right: Date): boolean { - return ( - left.getDate() == right.getDate() && - left.getMonth() == right.getMonth() && - left.getFullYear() == right.getFullYear() - ); -} +import TimelinePostListView from "./TimelinePostListView"; export interface TimelineProps { className?: string; @@ -101,62 +91,4 @@ const Timeline: React.FC = (props) => { } }; -export interface TimelinePostListViewProps { - className?: string; - style?: React.CSSProperties; - posts: HttpTimelinePostInfo[]; -} - -export const TimelinePostListView: React.FC = ( - props -) => { - const { className, style, posts } = props; - - const groupedPosts = React.useMemo< - { date: Date; posts: (HttpTimelinePostInfo & { index: number })[] }[] - >(() => { - const result: { - date: Date; - posts: (HttpTimelinePostInfo & { index: number })[]; - }[] = []; - let index = 0; - for (const post of posts) { - const time = new Date(post.time); - if (result.length === 0) { - result.push({ date: time, posts: [{ ...post, index }] }); - } else { - const lastGroup = result[result.length - 1]; - if (dateEqual(lastGroup.date, time)) { - lastGroup.posts.push({ ...post, index }); - } else { - result.push({ date: time, posts: [{ ...post, index }] }); - } - } - index++; - } - return result; - }, [posts]); - - return ( -
- {groupedPosts.map((group) => { - return ( - <> - - {group.posts.map((post) => { - return ( - - ); - })} - - ); - })} -
- ); -}; - export default Timeline; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx index b9f296c5..53312758 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx @@ -3,13 +3,7 @@ import clsx from "clsx"; import { useTranslation } from "react-i18next"; import { Dropdown, Button } from "react-bootstrap"; -import { - timelineService, - timelineVisibilityTooltipTranslationMap, -} from "@/services/timeline"; - import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; -import SyncStatusBadge from "../timeline-common/SyncStatusBadge"; import CollapseButton from "../timeline-common/CollapseButton"; import { useUser } from "@/services/user"; import { pushAlert } from "@/services/alert"; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineDateItem.tsx b/FrontEnd/src/app/views/timeline-common/TimelineDateItem.tsx deleted file mode 100644 index bcc1530f..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelineDateItem.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; -import TimelineLine from "./TimelineLine"; - -export interface TimelineDateItemProps { - date: Date; -} - -const TimelineDateItem: React.FC = ({ date }) => { - return ( -
- -
- {date.toLocaleDateString()} -
-
- ); -}; - -export default TimelineDateItem; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineDateLabel.tsx b/FrontEnd/src/app/views/timeline-common/TimelineDateLabel.tsx new file mode 100644 index 00000000..ae1b7386 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelineDateLabel.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import TimelineLine from "./TimelineLine"; + +export interface TimelineDateItemProps { + date: Date; +} + +const TimelineDateLabel: React.FC = ({ date }) => { + return ( +
+ +
+ {date.toLocaleDateString()} +
+
+ ); +}; + +export default TimelineDateLabel; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx b/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx deleted file mode 100644 index a5b6d04a..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import { Link } from "react-router-dom"; - -import { TimelinePostInfo } from "@/services/timeline"; - -import BlobImage from "../common/BlobImage"; -import UserAvatar from "../common/user/UserAvatar"; -import TimelineLine from "./TimelineLine"; -import TimelinePostDeleteConfirmDialog from "./TimelinePostDeleteConfirmDialog"; - -export interface TimelineItemProps { - post: TimelinePostInfo; - current?: boolean; - more?: { - isOpen: boolean; - toggle: () => void; - onDelete: () => void; - }; - onClick?: () => void; - className?: string; - style?: React.CSSProperties; -} - -const TimelineItem: React.FC = (props) => { - const current = props.current === true; - - const { post, more } = props; - - const [deleteDialog, setDeleteDialog] = React.useState(false); - - return ( -
- -
- {more != null ? ( - { - more.toggle(); - e.stopPropagation(); - }} - /> - ) : null} -
- - - - - - {post.author.nickname} - - {post.time.toLocaleTimeString()} - - - -
-
- {(() => { - const { content } = post; - if (content.type === "text") { - return content.text; - } else { - return ( - - ); - } - })()} -
- {more != null && more.isOpen ? ( -
- { - setDeleteDialog(true); - e.stopPropagation(); - }} - /> -
- ) : null} -
- {deleteDialog && more != null ? ( - { - setDeleteDialog(false); - more.toggle(); - }} - onConfirm={more.onDelete} - /> - ) : null} -
- ); -}; - -export default TimelineItem; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx index 9660b2aa..b5f8c0a2 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx @@ -2,17 +2,17 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Container, ListGroup, Modal, Row, Col, Button } from "react-bootstrap"; -import { getHttpSearchClient } from "@/http/search"; +import { convertI18nText, I18nText } from "@/common"; -import { User } from "@/services/user"; -import { TimelineInfo, timelineService } from "@/services/timeline"; +import { HttpUser } from "@/http/user"; +import { getHttpSearchClient } from "@/http/search"; import SearchInput from "../common/SearchInput"; import UserAvatar from "../common/user/UserAvatar"; -import { convertI18nText, I18nText } from "@/common"; +import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; const TimelineMemberItem: React.FC<{ - user: User; + user: HttpUser; add?: boolean; onAction?: (username: string) => void; }> = ({ user, add, onAction }) => { @@ -46,7 +46,7 @@ const TimelineMemberItem: React.FC<{ ); }; -const TimelineMemberUserSearch: React.FC<{ timeline: TimelineInfo }> = ({ +const TimelineMemberUserSearch: React.FC<{ timeline: HttpTimelineInfo }> = ({ timeline, }) => { const { t } = useTranslation(); @@ -55,7 +55,7 @@ const TimelineMemberUserSearch: React.FC<{ timeline: TimelineInfo }> = ({ const [userSearchState, setUserSearchState] = useState< | { type: "users"; - data: User[]; + data: HttpUser[]; } | { type: "error"; data: I18nText } | { type: "loading" } @@ -115,8 +115,8 @@ const TimelineMemberUserSearch: React.FC<{ timeline: TimelineInfo }> = ({ user={user} add onAction={() => { - void timelineService - .addMember(timeline.name, user.username) + void getHttpTimelineClient() + .memberPut(timeline.name, user.username) .then(() => { setUserSearchText(""); setUserSearchState({ type: "init" }); @@ -139,8 +139,10 @@ const TimelineMemberUserSearch: React.FC<{ timeline: TimelineInfo }> = ({ ); }; +// TODO: Trigger resync. + export interface TimelineMemberProps { - timeline: TimelineInfo; + timeline: HttpTimelineInfo; editable: boolean; } @@ -158,7 +160,7 @@ const TimelineMember: React.FC = (props) => { onAction={ editable && index !== 0 ? () => { - void timelineService.removeMember( + void getHttpTimelineClient().memberDelete( timeline.name, member.username ); diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx new file mode 100644 index 00000000..5acc1c21 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import clsx from "clsx"; + +import { HttpTimelinePostInfo } from "@/http/timeline"; + +import TimelinePostView from "./TimelinePostView"; +import TimelineDateLabel from "./TimelineDateLabel"; + +function dateEqual(left: Date, right: Date): boolean { + return ( + left.getDate() == right.getDate() && + left.getMonth() == right.getMonth() && + left.getFullYear() == right.getFullYear() + ); +} + +export interface TimelinePostListViewProps { + className?: string; + style?: React.CSSProperties; + posts: HttpTimelinePostInfo[]; +} + +const TimelinePostListView: React.FC = (props) => { + const { className, style, posts } = props; + + const groupedPosts = React.useMemo< + { date: Date; posts: (HttpTimelinePostInfo & { index: number })[] }[] + >(() => { + const result: { + date: Date; + posts: (HttpTimelinePostInfo & { index: number })[]; + }[] = []; + let index = 0; + for (const post of posts) { + const time = new Date(post.time); + if (result.length === 0) { + result.push({ date: time, posts: [{ ...post, index }] }); + } else { + const lastGroup = result[result.length - 1]; + if (dateEqual(lastGroup.date, time)) { + lastGroup.posts.push({ ...post, index }); + } else { + result.push({ date: time, posts: [{ ...post, index }] }); + } + } + index++; + } + return result; + }, [posts]); + + return ( +
+ {groupedPosts.map((group) => { + return ( + <> + + {group.posts.map((post) => { + return ( + + ); + })} + + ); + })} +
+ ); +}; + +export default TimelinePostListView; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx new file mode 100644 index 00000000..a2ae72cf --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import clsx from "clsx"; +import { Link } from "react-router-dom"; + +import { HttpTimelinePostInfo } from "@/http/timeline"; + +import UserAvatar from "../common/user/UserAvatar"; +import TimelineLine from "./TimelineLine"; +import TimelinePostDeleteConfirmDialog from "./TimelinePostDeleteConfirmDialog"; + +export interface TimelinePostViewProps { + post: HttpTimelinePostInfo; + current?: boolean; + className?: string; + style?: React.CSSProperties; +} + +const TimelinePostView: React.FC = (props) => { + const { post, className, style } = props; + const current = props.current === true; + + const [ + operationMaskVisible, + setOperationMaskVisible, + ] = React.useState(false); + const [deleteDialog, setDeleteDialog] = React.useState(false); + + // TODO: Load content. + + return ( +
+ +
+ {post.editable ? ( + { + setOperationMaskVisible(true); + e.stopPropagation(); + }} + /> + ) : null} +
+ + + + + + {post.author.nickname} + + {new Date(post.time).toLocaleTimeString()} + + + +
+
{/** TODO: Load content. */}
+ {operationMaskVisible ? ( +
{ + setOperationMaskVisible(false); + }} + > + { + setDeleteDialog(true); + e.stopPropagation(); + }} + /> +
+ ) : null} +
+ {deleteDialog ? ( + { + setDeleteDialog(false); + setOperationMaskVisible(false); + }} + onConfirm={() => { + // TODO: Implement this! + }} + /> + ) : null} +
+ ); +}; + +export default TimelinePostView; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx index ab3285f5..b99ec267 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx @@ -1,19 +1,21 @@ import React from "react"; import { - TimelineVisibility, + getHttpTimelineClient, + HttpTimelineInfo, + HttpTimelinePatchRequest, kTimelineVisibilities, - TimelineChangePropertyRequest, - TimelineInfo, -} from "@/services/timeline"; + TimelineVisibility, +} from "@/http/timeline"; import OperationDialog from "../common/OperationDialog"; +// TODO: Trigger resync. + export interface TimelinePropertyChangeDialogProps { open: boolean; close: () => void; - timeline: TimelineInfo; - onProcess: (request: TimelineChangePropertyRequest) => Promise; + timeline: HttpTimelineInfo; } const labelMap: { [key in TimelineVisibility]: string } = { @@ -54,7 +56,7 @@ const TimelinePropertyChangeDialog: React.FC open={props.open} close={props.close} onProcess={([newTitle, newVisibility, newDescription]) => { - const req: TimelineChangePropertyRequest = {}; + const req: HttpTimelinePatchRequest = {}; if (newTitle !== timeline.title) { req.title = newTitle; } @@ -64,7 +66,7 @@ const TimelinePropertyChangeDialog: React.FC if (newDescription !== timeline.description) { req.description = newDescription; } - return props.onProcess(req); + return getHttpTimelineClient().patchTimeline(timeline.name, req); }} /> ); -- cgit v1.2.3