aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2020-09-03 21:10:58 +0800
committercrupest <crupest@outlook.com>2020-09-03 21:10:58 +0800
commit01446b3c8306112cd965eeaaa40a0ac573cc374e (patch)
tree92516444e0955624dc0bc1d9109eff46d977052d
parentd8a2ca7d0ad9afdd01c654bea29b2a6c2b92ff2c (diff)
downloadtimeline-01446b3c8306112cd965eeaaa40a0ac573cc374e.tar.gz
timeline-01446b3c8306112cd965eeaaa40a0ac573cc374e.tar.bz2
timeline-01446b3c8306112cd965eeaaa40a0ac573cc374e.zip
...
-rw-r--r--Timeline/ClientApp/src/app/index.sass4
-rw-r--r--Timeline/ClientApp/src/app/views/timeline-common/CollapseButton.tsx23
-rw-r--r--Timeline/ClientApp/src/app/views/timeline-common/SyncStatusBadge.tsx58
-rw-r--r--Timeline/ClientApp/src/app/views/timeline-common/Timeline.tsx5
-rw-r--r--Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx58
-rw-r--r--Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx128
-rw-r--r--Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass23
-rw-r--r--Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx108
-rw-r--r--Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx13
9 files changed, 226 insertions, 194 deletions
diff --git a/Timeline/ClientApp/src/app/index.sass b/Timeline/ClientApp/src/app/index.sass
index 42a89da5..3322e503 100644
--- a/Timeline/ClientApp/src/app/index.sass
+++ b/Timeline/ClientApp/src/app/index.sass
@@ -12,10 +12,6 @@
body
margin: 0
-#app
- display: flex
- flex-direction: column
-
small
line-height: 1.2
diff --git a/Timeline/ClientApp/src/app/views/timeline-common/CollapseButton.tsx b/Timeline/ClientApp/src/app/views/timeline-common/CollapseButton.tsx
new file mode 100644
index 00000000..3c52150f
--- /dev/null
+++ b/Timeline/ClientApp/src/app/views/timeline-common/CollapseButton.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+import clsx from "clsx";
+import Svg from "react-inlinesvg";
+import arrowsAngleContractIcon from "bootstrap-icons/icons/arrows-angle-contract.svg";
+import arrowsAngleExpandIcon from "bootstrap-icons/icons/arrows-angle-expand.svg";
+
+const CollapseButton: React.FC<{
+ collapse: boolean;
+ onClick: () => void;
+ className?: string;
+ style?: React.CSSProperties;
+}> = ({ collapse, onClick, className, style }) => {
+ return (
+ <Svg
+ src={collapse ? arrowsAngleExpandIcon : arrowsAngleContractIcon}
+ onClick={onClick}
+ className={clsx("text-primary icon-button", className)}
+ style={style}
+ />
+ );
+};
+
+export default CollapseButton;
diff --git a/Timeline/ClientApp/src/app/views/timeline-common/SyncStatusBadge.tsx b/Timeline/ClientApp/src/app/views/timeline-common/SyncStatusBadge.tsx
new file mode 100644
index 00000000..e67cfb43
--- /dev/null
+++ b/Timeline/ClientApp/src/app/views/timeline-common/SyncStatusBadge.tsx
@@ -0,0 +1,58 @@
+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/Timeline/ClientApp/src/app/views/timeline-common/Timeline.tsx b/Timeline/ClientApp/src/app/views/timeline-common/Timeline.tsx
index 1ad62a51..1cb15d8e 100644
--- a/Timeline/ClientApp/src/app/views/timeline-common/Timeline.tsx
+++ b/Timeline/ClientApp/src/app/views/timeline-common/Timeline.tsx
@@ -51,10 +51,7 @@ const Timeline: React.FC<TimelineProps> = (props) => {
}, [posts, onDelete]);
return (
- <div
- ref={props.containerRef}
- className={clsx("container-fluid timeline", props.className)}
- >
+ <div ref={props.containerRef} className={clsx("timeline", props.className)}>
<div className="timeline-enter-animation-mask" />
{(() => {
const length = posts.length;
diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx
index ce371015..09d74d3c 100644
--- a/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx
+++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelineItem.tsx
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
import Svg from "react-inlinesvg";
import chevronDownIcon from "bootstrap-icons/icons/chevron-down.svg";
import trashIcon from "bootstrap-icons/icons/trash.svg";
-import { Row, Col, Modal, Button } from "react-bootstrap";
+import { Modal, Button } from "react-bootstrap";
import { useAvatar } from "@/services/user";
import { TimelinePostInfo } from "@/services/timeline";
@@ -74,51 +74,45 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => {
);
return (
- <Row
+ <div
className={clsx(
- "position-relative flex-nowrap",
+ "timeline-item position-relative",
current && "current",
props.className
)}
onClick={props.onClick}
style={props.style}
>
- <Col className="timeline-line-area">
+ <div className="timeline-line-area">
<div className="timeline-line-segment start"></div>
<div className="timeline-line-node-container">
<div className="timeline-line-node"></div>
</div>
<div className="timeline-line-segment end"></div>
{current && <div className="timeline-line-segment current-end" />}
- </Col>
- <Col className="timeline-pt-start">
- <Row className="flex-nowrap">
- <div className="col-auto flex-shrink-1 px-0">
- <Row className="ml-n3 mr-0 align-items-center">
- <span className="ml-3 text-primary white-space-no-wrap">
- {props.post.time.toLocaleString(i18n.languages)}
- </span>
- <small className="text-dark ml-3">
- {props.post.author.nickname}
- </small>
- </Row>
- </div>
+ </div>
+ <div className="timeline-content-area">
+ <div>
+ <span className="mr-2">
+ <span className="text-primary white-space-no-wrap mr-2">
+ {props.post.time.toLocaleString(i18n.languages)}
+ </span>
+ <small className="text-dark">{props.post.author.nickname}</small>
+ </span>
{more != null ? (
- <div className="col-auto px-2 d-flex justify-content-center align-items-center">
- <Svg
- src={chevronDownIcon}
- className="text-info icon-button"
- onClick={(e: Event) => {
- more.toggle();
- e.stopPropagation();
- }}
- />
- </div>
+ <Svg
+ src={chevronDownIcon}
+ className="text-info icon-button"
+ onClick={(e: Event) => {
+ more.toggle();
+ e.stopPropagation();
+ }}
+ />
) : null}
- </Row>
- <div className="row d-block timeline-content">
+ </div>
+ <div className="timeline-content">
<Link
- className="float-right float-sm-left mx-2"
+ className="float-left m-2"
to={"/users/" + props.post.author.username}
>
<BlobImage
@@ -142,7 +136,7 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => {
}
})()}
</div>
- </Col>
+ </div>
{more != null && more.isOpen ? (
<>
<div
@@ -169,7 +163,7 @@ const TimelineItem: React.FC<TimelineItemProps> = (props) => {
) : null}
</>
) : null}
- </Row>
+ </div>
);
};
diff --git a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx
index 4296a5ce..c2d4aeaa 100644
--- a/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx
+++ b/Timeline/ClientApp/src/app/views/timeline-common/TimelinePageTemplateUI.tsx
@@ -1,11 +1,7 @@
-import React, { CSSProperties } from "react";
-import clsx from "clsx";
+import React from "react";
import { useTranslation } from "react-i18next";
import { fromEvent } from "rxjs";
-import Svg from "react-inlinesvg";
-import { Spinner, Collapse } from "react-bootstrap";
-import arrowsAngleContractIcon from "bootstrap-icons/icons/arrows-angle-contract.svg";
-import arrowsAngleExpandIcon from "bootstrap-icons/icons/arrows-angle-expand.svg";
+import { Spinner } from "react-bootstrap";
import { getAlertHost } from "@/services/alert";
import { useEventEmiiter, UiLogicError } from "@/common";
@@ -21,63 +17,16 @@ import Timeline, {
TimelineDeleteCallback,
} from "./Timeline";
import TimelinePostEdit, { TimelinePostSendCallback } from "./TimelinePostEdit";
-
-type TimelinePostSyncState = "syncing" | "synced" | "offline";
-
-const TimelinePostSyncStateBadge: React.FC<{
- state: TimelinePostSyncState;
- style?: CSSProperties;
- className?: string;
-}> = ({ state, style, className }) => {
- const { t } = useTranslation();
-
- return (
- <div style={style} className={clsx("timeline-sync-state-badge", className)}>
- {(() => {
- switch (state) {
- 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>
- );
-};
+import { TimelineSyncStatus } from "./SyncStatusBadge";
export interface TimelineCardComponentProps<TManageItems> {
timeline: TimelineInfo;
onManage?: (item: TManageItems | "property") => void;
onMember: () => void;
className?: string;
+ collapse: boolean;
+ syncStatus: TimelineSyncStatus;
+ toggleCollapse: () => void;
}
export interface TimelinePageTemplateUIProps<TManageItems> {
@@ -216,22 +165,13 @@ export default function TimelinePageTemplateUI<TManageItems>(
})
);
- const syncState: TimelinePostSyncState = postListState.syncing
- ? "syncing"
- : postListState.type === "synced"
- ? "synced"
- : "offline";
-
timelineBody = (
- <div>
- <TimelinePostSyncStateBadge state={syncState} />
- <Timeline
- containerRef={timelineRef}
- posts={posts}
- onDelete={props.onDelete}
- onResize={triggerResizeEvent}
- />
- </div>
+ <Timeline
+ containerRef={timelineRef}
+ posts={posts}
+ onDelete={props.onDelete}
+ onResize={triggerResizeEvent}
+ />
);
if (props.onPost != null) {
timelineBody = (
@@ -255,37 +195,35 @@ export default function TimelinePageTemplateUI<TManageItems>(
</div>
);
}
+
const { CardComponent } = props;
+ const syncStatus: TimelineSyncStatus =
+ postListState == null || postListState.syncing
+ ? "syncing"
+ : postListState.type === "synced"
+ ? "synced"
+ : "offline";
body = (
<>
- <div className="info-card-container">
- <Svg
- src={
- infoCardCollapse
- ? arrowsAngleExpandIcon
- : arrowsAngleContractIcon
- }
- onClick={() => {
- const newState = !infoCardCollapse;
- setInfoCardCollapse(newState);
+ <CardComponent
+ timeline={timeline}
+ onManage={props.onManage}
+ onMember={props.onMember}
+ className="timeline-info-card"
+ syncStatus={syncStatus}
+ collapse={infoCardCollapse}
+ toggleCollapse={() => {
+ const newState = !infoCardCollapse;
+ setInfoCardCollapse(newState);
+ if (timeline != null) {
window.localStorage.setItem(
genCardCollapseLocalStorageKey(timeline.uniqueId),
newState.toString()
);
- }}
- className="float-right m-1 info-card-collapse-button text-primary icon-button"
- />
- <Collapse in={!infoCardCollapse}>
- <CardComponent
- timeline={timeline}
- onManage={props.onManage}
- onMember={props.onMember}
- className="info-card-content"
- />
- </Collapse>
- </div>
-
+ }
+ }}
+ />
{timelineBody}
</>
);
diff --git a/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass b/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass
index a7b9af7b..ad024c78 100644
--- a/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass
+++ b/Timeline/ClientApp/src/app/views/timeline-common/timeline-common.sass
@@ -1,11 +1,12 @@
@use 'sass:color'
.timeline
- display: flex
- flex-direction: column
z-index: 0
position: relative
+ &-item
+ display: flex
+
@keyframes timeline-enter-animation-mask-animation
to
height: 0
@@ -96,8 +97,9 @@ $timeline-line-color-current: #36c2e6
&-node
animation-name: timeline-line-node-current
-.timeline-pt-start
+.timeline-content-area
padding-top: 18px
+ flex-grow: 1
.timeline-item-delete-button
position: absolute
@@ -123,10 +125,6 @@ $timeline-line-color-current: #36c2e6
transition: height 0.5s
.timeline-sync-state-badge
- position: fixed
- top: 0
- right: 0
- z-index: 1
font-size: 0.8em
padding: 3px 8px
border-radius: 5px
@@ -140,7 +138,14 @@ $timeline-line-color-current: #36c2e6
vertical-align: middle
margin-right: 0.6em
-.info-card-container
+.timeline-info-card
position: sticky
- top: 56px
z-index: 1
+ top: 56px
+ margin: 0.5em
+
+ @include media-breakpoint-down(sm)
+ margin-bottom: 0
+
+ @include media-breakpoint-up(sm)
+ float: right
diff --git a/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx b/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx
index 9f989148..764910aa 100644
--- a/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx
+++ b/Timeline/ClientApp/src/app/views/timeline/TimelineInfoCard.tsx
@@ -8,6 +8,8 @@ import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline";
import BlobImage from "../common/BlobImage";
import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI";
+import CollapseButton from "../timeline-common/CollapseButton";
+import SyncStatusBadge from "../timeline-common/SyncStatusBadge";
export type OrdinaryTimelineManageItem = "delete";
@@ -16,55 +18,75 @@ export type TimelineInfoCardProps = TimelineCardComponentProps<
>;
const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => {
- const { onMember, onManage } = props;
+ const {
+ timeline,
+ onMember,
+ onManage,
+ collapse,
+ syncStatus,
+ toggleCollapse,
+ } = props;
const { t } = useTranslation();
- const avatar = useAvatar(props.timeline.owner.username);
+ const avatar = useAvatar(timeline?.owner?.username);
return (
- <div className={clsx("rounded border p-2 bg-light", props.className)}>
- <h3 className="text-primary mx-3 d-inline-block align-middle">
- {props.timeline.name}
- </h3>
- <div className="d-inline-block align-middle">
- <BlobImage blob={avatar} className="avatar small rounded-circle" />
- {props.timeline.owner.nickname}
- <small className="ml-3 text-secondary">
- @{props.timeline.owner.username}
- </small>
+ <div
+ className={clsx(
+ "rounded border p-2 bg-light",
+ props.className,
+ collapse && "align-self-end"
+ )}
+ >
+ <div className="float-right d-flex align-items-center">
+ <SyncStatusBadge status={syncStatus} className="mr-2" />
+ <CollapseButton collapse={collapse} onClick={toggleCollapse} />
</div>
- <p className="mb-0">{props.timeline.description}</p>
- <small className="mt-1 d-block">
- {t(timelineVisibilityTooltipTranslationMap[props.timeline.visibility])}
- </small>
- <div className="text-right mt-2">
- {onManage != null ? (
- <Dropdown>
- <Dropdown.Toggle variant="outline-primary">
- {t("timeline.manage")}
- </Dropdown.Toggle>
- <Dropdown.Menu>
- <Dropdown.Item onClick={() => onManage("property")}>
- {t("timeline.manageItem.property")}
- </Dropdown.Item>
- <Dropdown.Item onClick={onMember}>
- {t("timeline.manageItem.member")}
- </Dropdown.Item>
- <Dropdown.Divider />
- <Dropdown.Item
- className="text-danger"
- onClick={() => onManage("delete")}
- >
- {t("timeline.manageItem.delete")}
- </Dropdown.Item>
- </Dropdown.Menu>
- </Dropdown>
- ) : (
- <Button variant="outline-primary" onClick={onMember}>
- {t("timeline.memberButton")}
- </Button>
- )}
+
+ <div style={{ display: collapse ? "none" : "block" }}>
+ <h3 className="text-primary mx-3 d-inline-block align-middle">
+ {timeline.name}
+ </h3>
+ <div className="d-inline-block align-middle">
+ <BlobImage blob={avatar} className="avatar small rounded-circle" />
+ {timeline.owner.nickname}
+ <small className="ml-3 text-secondary">
+ @{timeline.owner.username}
+ </small>
+ </div>
+ <p className="mb-0">{timeline.description}</p>
+ <small className="mt-1 d-block">
+ {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])}
+ </small>
+ <div className="text-right mt-2">
+ {onManage != null ? (
+ <Dropdown>
+ <Dropdown.Toggle variant="outline-primary">
+ {t("timeline.manage")}
+ </Dropdown.Toggle>
+ <Dropdown.Menu>
+ <Dropdown.Item onClick={() => onManage("property")}>
+ {t("timeline.manageItem.property")}
+ </Dropdown.Item>
+ <Dropdown.Item onClick={onMember}>
+ {t("timeline.manageItem.member")}
+ </Dropdown.Item>
+ <Dropdown.Divider />
+ <Dropdown.Item
+ className="text-danger"
+ onClick={() => onManage("delete")}
+ >
+ {t("timeline.manageItem.delete")}
+ </Dropdown.Item>
+ </Dropdown.Menu>
+ </Dropdown>
+ ) : (
+ <Button variant="outline-primary" onClick={onMember}>
+ {t("timeline.memberButton")}
+ </Button>
+ )}
+ </div>
</div>
</div>
);
diff --git a/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx b/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx
index cec81421..251e53b4 100644
--- a/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx
+++ b/Timeline/ClientApp/src/app/views/user/UserInfoCard.tsx
@@ -1,7 +1,6 @@
import React from "react";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
-import { fromEvent } from "rxjs";
import { Dropdown, Button } from "react-bootstrap";
import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline";
@@ -17,10 +16,10 @@ export type UserInfoCardProps = TimelineCardComponentProps<
>;
const UserInfoCard: React.FC<UserInfoCardProps> = (props) => {
- const { onManage } = props;
+ const { onManage, timeline } = props;
const { t } = useTranslation();
- const avatar = useAvatar(props.timeline.owner.username);
+ const avatar = useAvatar(timeline?.owner?.username);
return (
<div className={clsx("rounded border bg-light p-2", props.className)}>
@@ -29,14 +28,14 @@ const UserInfoCard: React.FC<UserInfoCardProps> = (props) => {
className="avatar large mr-2 rounded-circle float-left"
/>
<div>
- {props.timeline.owner.nickname}
+ {timeline.owner.nickname}
<small className="ml-3 text-secondary">
- @{props.timeline.owner.username}
+ @{timeline.owner.username}
</small>
</div>
- <p className="mb-0">{props.timeline.description}</p>
+ <p className="mb-0">{timeline.description}</p>
<small className="mt-1 d-block">
- {t(timelineVisibilityTooltipTranslationMap[props.timeline.visibility])}
+ {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])}
</small>
<div className="text-right mt-2">
{onManage != null ? (