aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/app/views/timeline-common
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/app/views/timeline-common')
-rw-r--r--FrontEnd/src/app/views/timeline-common/SyncStatusBadge.tsx58
-rw-r--r--FrontEnd/src/app/views/timeline-common/Timeline.tsx70
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx6
-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.tsx24
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx73
-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.tsx18
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);
}}
/>
);