diff options
author | crupest <crupest@outlook.com> | 2021-02-13 15:31:49 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2021-02-13 15:31:49 +0800 |
commit | 790fc48e013ecd424d73e45072607927a3a43a70 (patch) | |
tree | c39fd481a36a624a9cd444777f1a91b79b9c884d /FrontEnd/src/app/views | |
parent | 8d4db22c80a5992915abdf6d2b7d8047b93265ff (diff) | |
download | timeline-790fc48e013ecd424d73e45072607927a3a43a70.tar.gz timeline-790fc48e013ecd424d73e45072607927a3a43a70.tar.bz2 timeline-790fc48e013ecd424d73e45072607927a3a43a70.zip |
...
Diffstat (limited to 'FrontEnd/src/app/views')
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/SyncStatusBadge.tsx | 58 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/Timeline.tsx | 70 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx | 6 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelineDateLabel.tsx (renamed from FrontEnd/src/app/views/timeline-common/TimelineDateItem.tsx) | 4 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelineMember.tsx | 24 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx | 73 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx (renamed from FrontEnd/src/app/views/timeline-common/TimelineItem.tsx) | 67 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx | 18 |
8 files changed, 126 insertions, 194 deletions
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 ( - <div style={style} className={clsx("timeline-sync-state-badge", className)}> - {(() => { - switch (status) { - case "syncing": { - return ( - <> - <span className="timeline-sync-state-badge-pin bg-warning" /> - <span className="text-warning"> - {t("timeline.postSyncState.syncing")} - </span> - </> - ); - } - case "synced": { - return ( - <> - <span className="timeline-sync-state-badge-pin bg-success" /> - <span className="text-success"> - {t("timeline.postSyncState.synced")} - </span> - </> - ); - } - case "offline": { - return ( - <> - <span className="timeline-sync-state-badge-pin bg-danger" /> - <span className="text-danger"> - {t("timeline.postSyncState.offline")} - </span> - </> - ); - } - default: - throw new UiLogicError("Unknown sync state."); - } - })()} - </div> - ); -}; - -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<TimelineProps> = (props) => { } }; -export interface TimelinePostListViewProps { - className?: string; - style?: React.CSSProperties; - posts: HttpTimelinePostInfo[]; -} - -export const TimelinePostListView: React.FC<TimelinePostListViewProps> = ( - 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 ( - <div style={style} className={clsx("timeline", className)}> - {groupedPosts.map((group) => { - return ( - <> - <TimelineDateItem date={group.date} /> - {group.posts.map((post) => { - return ( - <TimelineItem - key={post.id} - post={post} - current={posts.length - 1 === post.index} - /> - ); - })} - </> - ); - })} - </div> - ); -}; - 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/TimelineDateLabel.tsx index bcc1530f..ae1b7386 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelineDateItem.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelineDateLabel.tsx @@ -5,7 +5,7 @@ export interface TimelineDateItemProps { date: Date; } -const TimelineDateItem: React.FC<TimelineDateItemProps> = ({ date }) => { +const TimelineDateLabel: React.FC<TimelineDateItemProps> = ({ date }) => { return ( <div className="timeline-date-item"> <TimelineLine center={null} /> @@ -16,4 +16,4 @@ const TimelineDateItem: React.FC<TimelineDateItemProps> = ({ date }) => { ); }; -export default TimelineDateItem; +export default TimelineDateLabel; 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<TimelineMemberProps> = (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<TimelinePostListViewProps> = (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 ( + <div style={style} className={clsx("timeline", className)}> + {groupedPosts.map((group) => { + return ( + <> + <TimelineDateLabel date={group.date} /> + {group.posts.map((post) => { + return ( + <TimelinePostView + key={post.id} + post={post} + current={posts.length - 1 === post.index} + /> + ); + })} + </> + ); + })} + </div> + ); +}; + +export default TimelinePostListView; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx index a5b6d04a..a2ae72cf 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelineItem.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx @@ -2,46 +2,43 @@ import React from "react"; import clsx from "clsx"; import { Link } from "react-router-dom"; -import { TimelinePostInfo } from "@/services/timeline"; +import { HttpTimelinePostInfo } from "@/http/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; +export interface TimelinePostViewProps { + post: HttpTimelinePostInfo; current?: boolean; - more?: { - isOpen: boolean; - toggle: () => void; - onDelete: () => void; - }; - onClick?: () => void; className?: string; style?: React.CSSProperties; } -const TimelineItem: React.FC<TimelineItemProps> = (props) => { +const TimelinePostView: React.FC<TimelinePostViewProps> = (props) => { + const { post, className, style } = props; const current = props.current === true; - const { post, more } = props; - + const [ + operationMaskVisible, + setOperationMaskVisible, + ] = React.useState<boolean>(false); const [deleteDialog, setDeleteDialog] = React.useState<boolean>(false); + // TODO: Load content. + return ( <div - className={clsx("timeline-item", current && "current", props.className)} - onClick={props.onClick} - style={props.style} + className={clsx("timeline-item", current && "current", className)} + style={style} > <TimelineLine center="node" current={current} /> <div className="timeline-item-card"> - {more != null ? ( + {post.editable ? ( <i className="bi-chevron-down text-info icon-button float-right" onClick={(e) => { - more.toggle(); + setOperationMaskVisible(true); e.stopPropagation(); }} /> @@ -57,30 +54,18 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => { </Link> <small className="text-dark mr-2">{post.author.nickname}</small> <small className="text-secondary white-space-no-wrap"> - {post.time.toLocaleTimeString()} + {new Date(post.time).toLocaleTimeString()} </small> </span> </span> </div> - <div className="timeline-content"> - {(() => { - const { content } = post; - if (content.type === "text") { - return content.text; - } else { - return ( - <BlobImage - blob={content.data} - className="timeline-content-image" - /> - ); - } - })()} - </div> - {more != null && more.isOpen ? ( + <div className="timeline-content">{/** TODO: Load content. */}</div> + {operationMaskVisible ? ( <div className="position-absolute position-lt w-100 h-100 mask d-flex justify-content-center align-items-center" - onClick={more.toggle} + onClick={() => { + setOperationMaskVisible(false); + }} > <i className="bi-trash text-danger icon-button large" @@ -92,17 +77,19 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => { </div> ) : null} </div> - {deleteDialog && more != null ? ( + {deleteDialog ? ( <TimelinePostDeleteConfirmDialog onClose={() => { setDeleteDialog(false); - more.toggle(); + setOperationMaskVisible(false); + }} + onConfirm={() => { + // TODO: Implement this! }} - onConfirm={more.onDelete} /> ) : null} </div> ); }; -export default TimelineItem; +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<void>; + timeline: HttpTimelineInfo; } const labelMap: { [key in TimelineVisibility]: string } = { @@ -54,7 +56,7 @@ const TimelinePropertyChangeDialog: React.FC<TimelinePropertyChangeDialogProps> 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<TimelinePropertyChangeDialogProps> if (newDescription !== timeline.description) { req.description = newDescription; } - return props.onProcess(req); + return getHttpTimelineClient().patchTimeline(timeline.name, req); }} /> ); |