From 47587812b809fee2a95c76266d9d0e42fc4ac1ca Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 15 Jun 2021 14:14:28 +0800 Subject: ... --- .../app/views/timeline-common/CollapseButton.tsx | 23 -- .../timeline-common/ConnectionStatusBadge.tsx | 39 --- .../app/views/timeline-common/MarkdownPostEdit.tsx | 205 --------------- .../timeline-common/PostPropertyChangeDialog.tsx | 36 --- .../src/app/views/timeline-common/Timeline.tsx | 143 ---------- .../views/timeline-common/TimelineDateLabel.tsx | 19 -- .../src/app/views/timeline-common/TimelineLine.tsx | 51 ---- .../app/views/timeline-common/TimelineLoading.tsx | 18 -- .../app/views/timeline-common/TimelineMember.tsx | 195 -------------- .../timeline-common/TimelinePageCardTemplate.tsx | 158 ----------- .../views/timeline-common/TimelinePageTemplate.tsx | 190 -------------- .../timeline-common/TimelinePagedPostListView.tsx | 43 --- .../timeline-common/TimelinePostContentView.tsx | 197 -------------- .../TimelinePostDeleteConfirmDialog.tsx | 37 --- .../app/views/timeline-common/TimelinePostEdit.tsx | 291 --------------------- .../views/timeline-common/TimelinePostListView.tsx | 79 ------ .../app/views/timeline-common/TimelinePostView.tsx | 151 ----------- .../TimelinePropertyChangeDialog.tsx | 87 ------ .../src/app/views/timeline-common/TimelineTop.tsx | 27 -- .../app/views/timeline-common/timeline-common.sass | 259 ------------------ 20 files changed, 2248 deletions(-) delete mode 100644 FrontEnd/src/app/views/timeline-common/CollapseButton.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/ConnectionStatusBadge.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/PostPropertyChangeDialog.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/Timeline.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelineDateLabel.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelineLine.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelineLoading.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelineMember.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePageCardTemplate.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePagedPostListView.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/TimelineTop.tsx delete mode 100644 FrontEnd/src/app/views/timeline-common/timeline-common.sass (limited to 'FrontEnd/src/app/views/timeline-common') diff --git a/FrontEnd/src/app/views/timeline-common/CollapseButton.tsx b/FrontEnd/src/app/views/timeline-common/CollapseButton.tsx deleted file mode 100644 index 12a3b710..00000000 --- a/FrontEnd/src/app/views/timeline-common/CollapseButton.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import classnames from "classnames"; - -const CollapseButton: React.FC<{ - collapse: boolean; - onClick: () => void; - className?: string; - style?: React.CSSProperties; -}> = ({ collapse, onClick, className, style }) => { - return ( - - ); -}; - -export default CollapseButton; diff --git a/FrontEnd/src/app/views/timeline-common/ConnectionStatusBadge.tsx b/FrontEnd/src/app/views/timeline-common/ConnectionStatusBadge.tsx deleted file mode 100644 index df43d8d2..00000000 --- a/FrontEnd/src/app/views/timeline-common/ConnectionStatusBadge.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react"; -import classnames from "classnames"; -import { HubConnectionState } from "@microsoft/signalr"; -import { useTranslation } from "react-i18next"; - -export interface ConnectionStatusBadgeProps { - status: HubConnectionState; - className?: string; - style?: React.CSSProperties; -} - -const classNameMap: Record = { - Connected: "success", - Connecting: "warning", - Disconnected: "danger", - Disconnecting: "warning", - Reconnecting: "warning", -}; - -const ConnectionStatusBadge: React.FC = (props) => { - const { status, className, style } = props; - - const { t } = useTranslation(); - - return ( -
- {t(`connectionState.${status}`)} -
- ); -}; - -export default ConnectionStatusBadge; diff --git a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx deleted file mode 100644 index 685e17be..00000000 --- a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import React from "react"; -import classnames from "classnames"; -import { Form, Spinner } from "react-bootstrap"; -import { useTranslation } from "react-i18next"; -import { Prompt } from "react-router"; - -import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; - -import FlatButton from "../common/FlatButton"; -import TabPages from "../common/TabPages"; -import TimelinePostBuilder from "@/services/TimelinePostBuilder"; -import ConfirmDialog from "../common/ConfirmDialog"; - -export interface MarkdownPostEditProps { - timeline: string; - onPosted: (post: HttpTimelinePostInfo) => void; - onPostError: () => void; - onClose: () => void; - className?: string; - style?: React.CSSProperties; -} - -const MarkdownPostEdit: React.FC = ({ - timeline: timelineName, - onPosted, - onClose, - onPostError, - className, - style, -}) => { - const { t } = useTranslation(); - - const [canLeave, setCanLeave] = React.useState(true); - - const [process, setProcess] = React.useState(false); - - const [showLeaveConfirmDialog, setShowLeaveConfirmDialog] = - React.useState(false); - - const [text, _setText] = React.useState(""); - const [images, _setImages] = React.useState<{ file: File; url: string }[]>( - [] - ); - const [previewHtml, _setPreviewHtml] = React.useState(""); - - const _builder = React.useRef(null); - - const getBuilder = (): TimelinePostBuilder => { - if (_builder.current == null) { - const builder = new TimelinePostBuilder(() => { - setCanLeave(builder.isEmpty); - _setText(builder.text); - _setImages(builder.images); - _setPreviewHtml(builder.renderHtml()); - }); - _builder.current = builder; - } - return _builder.current; - }; - - const canSend = text.length > 0; - - React.useEffect(() => { - return () => { - getBuilder().dispose(); - }; - }, []); - - React.useEffect(() => { - window.onbeforeunload = (): unknown => { - if (!canLeave) { - return t("timeline.confirmLeave"); - } - }; - - return () => { - window.onbeforeunload = null; - }; - }, [canLeave, t]); - - const send = async (): Promise => { - setProcess(true); - try { - const dataList = await getBuilder().build(); - const post = await getHttpTimelineClient().postPost(timelineName, { - dataList, - }); - onPosted(post); - onClose(); - } catch (e) { - setProcess(false); - onPostError(); - } - }; - - return ( - <> - - - ) : ( - <> - { - if (canLeave) { - onClose(); - } else { - setShowLeaveConfirmDialog(true); - } - }} - > - {t("operationDialog.cancel")} - - - {t("timeline.send")} - - - ) - } - pages={[ - { - id: "text", - tabText: "edit", - page: ( - { - getBuilder().setMarkdownText(event.currentTarget.value); - }} - /> - ), - }, - { - id: "images", - tabText: "image", - page: ( -
- {images.map((image, index) => ( -
- - { - getBuilder().deleteImage(index); - }} - /> -
- ))} - ) => { - const { files } = event.currentTarget; - if (files != null && files.length !== 0) { - getBuilder().appendImage(files[0]); - } - }} - disabled={process} - /> -
- ), - }, - { - id: "preview", - tabText: "preview", - page: ( -
- ), - }, - ]} - /> - {showLeaveConfirmDialog && ( - setShowLeaveConfirmDialog(false)} - onConfirm={onClose} - title="timeline.dropDraft" - body="timeline.confirmLeave" - /> - )} - - ); -}; - -export default MarkdownPostEdit; diff --git a/FrontEnd/src/app/views/timeline-common/PostPropertyChangeDialog.tsx b/FrontEnd/src/app/views/timeline-common/PostPropertyChangeDialog.tsx deleted file mode 100644 index 001e52d7..00000000 --- a/FrontEnd/src/app/views/timeline-common/PostPropertyChangeDialog.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; - -import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; - -import OperationDialog from "../common/OperationDialog"; - -function PostPropertyChangeDialog(props: { - onClose: () => void; - post: HttpTimelinePostInfo; - onSuccess: (post: HttpTimelinePostInfo) => void; -}): React.ReactElement | null { - const { onClose, post, onSuccess } = props; - - return ( - { - return getHttpTimelineClient().patchPost(post.timelineName, post.id, { - time: time === "" ? undefined : new Date(time).toISOString(), - }); - }} - onSuccessAndClose={onSuccess} - /> - ); -} - -export default PostPropertyChangeDialog; diff --git a/FrontEnd/src/app/views/timeline-common/Timeline.tsx b/FrontEnd/src/app/views/timeline-common/Timeline.tsx deleted file mode 100644 index 589382b0..00000000 --- a/FrontEnd/src/app/views/timeline-common/Timeline.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import React from "react"; -import { HubConnectionState } from "@microsoft/signalr"; - -import { - HttpForbiddenError, - HttpNetworkError, - HttpNotFoundError, -} from "@/http/common"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; - -import { getTimelinePostUpdate$ } from "@/services/timeline"; - -import TimelinePagedPostListView from "./TimelinePagedPostListView"; -import TimelineTop from "./TimelineTop"; -import TimelineLoading from "./TimelineLoading"; - -export interface TimelineProps { - className?: string; - style?: React.CSSProperties; - timelineName?: string; - reloadKey: number; - onReload: () => void; - onConnectionStateChanged?: (state: HubConnectionState) => void; -} - -const Timeline: React.FC = (props) => { - const { timelineName, className, style, reloadKey } = props; - - const [state, setState] = React.useState< - "loading" | "loaded" | "offline" | "notexist" | "forbid" | "error" - >("loading"); - const [posts, setPosts] = React.useState([]); - - React.useEffect(() => { - setState("loading"); - setPosts([]); - }, [timelineName]); - - const onReload = React.useRef<() => void>(props.onReload); - - React.useEffect(() => { - onReload.current = props.onReload; - }, [props.onReload]); - - const onConnectionStateChanged = React.useRef< - ((state: HubConnectionState) => void) | null - >(null); - - React.useEffect(() => { - onConnectionStateChanged.current = props.onConnectionStateChanged ?? null; - }, [props.onConnectionStateChanged]); - - React.useEffect(() => { - if (timelineName != null && state === "loaded") { - const timelinePostUpdate$ = getTimelinePostUpdate$(timelineName); - const subscription = timelinePostUpdate$.subscribe( - ({ update, state }) => { - if (update) { - onReload.current(); - } - onConnectionStateChanged.current?.(state); - } - ); - return () => { - subscription.unsubscribe(); - }; - } - }, [timelineName, state]); - - React.useEffect(() => { - if (timelineName != null) { - let subscribe = true; - - void getHttpTimelineClient() - .listPost(timelineName) - .then( - (data) => { - if (subscribe) { - setState("loaded"); - setPosts(data); - } - }, - (error) => { - if (error instanceof HttpNetworkError) { - setState("offline"); - } else if (error instanceof HttpForbiddenError) { - setState("forbid"); - } else if (error instanceof HttpNotFoundError) { - setState("notexist"); - } else { - console.error(error); - setState("error"); - } - } - ); - - return () => { - subscribe = false; - }; - } - }, [timelineName, reloadKey]); - - switch (state) { - case "loading": - return ; - case "offline": - return ( -
- Offline. -
- ); - case "notexist": - return ( -
- Not exist. -
- ); - case "forbid": - return ( -
- Forbid. -
- ); - case "error": - return ( -
- Error. -
- ); - default: - return ( - <> - - - - ); - } -}; - -export default Timeline; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineDateLabel.tsx b/FrontEnd/src/app/views/timeline-common/TimelineDateLabel.tsx deleted file mode 100644 index 80968ee2..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelineDateLabel.tsx +++ /dev/null @@ -1,19 +0,0 @@ -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/TimelineLine.tsx b/FrontEnd/src/app/views/timeline-common/TimelineLine.tsx deleted file mode 100644 index 0a828b32..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelineLine.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from "react"; -import classnames from "classnames"; - -export interface TimelineLineProps { - current?: boolean; - startSegmentLength?: string | number; - center: "node" | "loading" | "none"; - className?: string; - style?: React.CSSProperties; -} - -const TimelineLine: React.FC = ({ - startSegmentLength, - center, - current, - className, - style, -}) => { - return ( -
-
- {center !== "none" ? ( -
-
- {center === "loading" ? ( - - - - ) : null} -
- ) : null} - {center !== "loading" ?
: null} - {current &&
} -
- ); -}; - -export default TimelineLine; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineLoading.tsx b/FrontEnd/src/app/views/timeline-common/TimelineLoading.tsx deleted file mode 100644 index fc42f4b4..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelineLoading.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; - -import TimelineTop from "./TimelineTop"; - -const TimelineLoading: React.FC = () => { - return ( - - ); -}; - -export default TimelineLoading; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx deleted file mode 100644 index 299d6a53..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import React, { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Container, ListGroup, Modal, Row, Col, Button } from "react-bootstrap"; - -import { convertI18nText, I18nText } from "@/common"; - -import { HttpUser } from "@/http/user"; -import { getHttpSearchClient } from "@/http/search"; - -import SearchInput from "../common/SearchInput"; -import UserAvatar from "../common/user/UserAvatar"; -import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; - -const TimelineMemberItem: React.FC<{ - user: HttpUser; - add?: boolean; - onAction?: (username: string) => void; -}> = ({ user, add, onAction }) => { - const { t } = useTranslation(); - - return ( - - - - - - - {user.nickname} - - {"@" + user.username} - - - {onAction ? ( - - - - ) : null} - - - ); -}; - -const TimelineMemberUserSearch: React.FC<{ - timeline: HttpTimelineInfo; - onChange: () => void; -}> = ({ timeline, onChange }) => { - const { t } = useTranslation(); - - const [userSearchText, setUserSearchText] = useState(""); - const [userSearchState, setUserSearchState] = useState< - | { - type: "users"; - data: HttpUser[]; - } - | { type: "error"; data: I18nText } - | { type: "loading" } - | { type: "init" } - >({ type: "init" }); - - return ( - <> - { - setUserSearchText(v); - }} - loading={userSearchState.type === "loading"} - onButtonClick={() => { - if (userSearchText === "") { - setUserSearchState({ - type: "error", - data: "login.emptyUsername", - }); - return; - } - setUserSearchState({ type: "loading" }); - getHttpSearchClient() - .searchUsers(userSearchText) - .then( - (users) => { - users = users.filter( - (user) => - timeline.members.findIndex( - (m) => m.username === user.username - ) === -1 && timeline.owner.username !== user.username - ); - setUserSearchState({ type: "users", data: users }); - }, - (e) => { - setUserSearchState({ - type: "error", - data: { type: "custom", value: String(e) }, - }); - } - ); - }} - /> - {(() => { - if (userSearchState.type === "users") { - const users = userSearchState.data; - if (users.length === 0) { - return
{t("timeline.member.noUserAvailableToAdd")}
; - } else { - return ( - - {users.map((user) => ( - { - void getHttpTimelineClient() - .memberPut(timeline.name, user.username) - .then(() => { - setUserSearchText(""); - setUserSearchState({ type: "init" }); - onChange(); - }); - }} - /> - ))} - - ); - } - } else if (userSearchState.type === "error") { - return ( -
- {convertI18nText(userSearchState.data, t)} -
- ); - } - })()} - - ); -}; - -export interface TimelineMemberProps { - timeline: HttpTimelineInfo; - onChange: () => void; -} - -const TimelineMember: React.FC = (props) => { - const { timeline, onChange } = props; - const members = [timeline.owner, ...timeline.members]; - - return ( - - - {members.map((member, index) => ( - { - void getHttpTimelineClient() - .memberDelete(timeline.name, member.username) - .then(onChange); - } - : undefined - } - /> - ))} - - {timeline.manageable ? ( - - ) : null} - - ); -}; - -export default TimelineMember; - -export interface TimelineMemberDialogProps extends TimelineMemberProps { - open: boolean; - onClose: () => void; -} - -export const TimelineMemberDialog: React.FC = ( - props -) => { - return ( - - - - ); -}; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageCardTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageCardTemplate.tsx deleted file mode 100644 index 623d643f..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageCardTemplate.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React from "react"; -import classnames from "classnames"; -import { useTranslation } from "react-i18next"; - -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 { useIsSmallScreen } from "@/utilities/mediaQuery"; - -import { TimelinePageCardProps } from "./TimelinePageTemplate"; - -import CollapseButton from "./CollapseButton"; -import { TimelineMemberDialog } from "./TimelineMember"; -import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; -import ConnectionStatusBadge from "./ConnectionStatusBadge"; -import { MenuItems, PopupMenu } from "../common/Menu"; -import FullPage from "../common/FullPage"; - -export interface TimelineCardTemplateProps extends TimelinePageCardProps { - infoArea: React.ReactElement; - manageItems?: MenuItems; - dialog: string | "property" | "member" | null; - setDialog: (dialog: "property" | "member" | null) => void; -} - -const TimelinePageCardTemplate: React.FC = ({ - timeline, - collapse, - toggleCollapse, - infoArea, - manageItems, - connectionStatus, - onReload, - className, - dialog, - setDialog, -}) => { - const { t } = useTranslation(); - - const isSmallScreen = useIsSmallScreen(); - - const user = useUser(); - - const content = ( - <> - {infoArea} -

{timeline.description}

- - {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} - -
- { - getHttpHighlightClient() - [timeline.isHighlight ? "delete" : "put"](timeline.name) - .then(onReload, () => { - pushAlert({ - message: timeline.isHighlight - ? "timeline.removeHighlightFail" - : "timeline.addHighlightFail", - type: "danger", - }); - }); - } - : undefined - } - /> - {user != null ? ( - { - getHttpBookmarkClient() - [timeline.isBookmark ? "delete" : "put"](timeline.name) - .then(onReload, () => { - pushAlert({ - message: timeline.isBookmark - ? "timeline.removeBookmarkFail" - : "timeline.addBookmarkFail", - type: "danger", - }); - }); - }} - /> - ) : null} - setDialog("member")} - /> - {manageItems != null ? ( - - - - ) : null} -
- - ); - - return ( - <> -
-
- - -
- {isSmallScreen ? ( - - {content} - - ) : ( -
{content}
- )} -
- {(() => { - if (dialog === "member") { - return ( - setDialog(null)} - open - onChange={onReload} - /> - ); - } else if (dialog === "property") { - return ( - setDialog(null)} - open - onChange={onReload} - /> - ); - } - })()} - - ); -}; - -export default TimelinePageCardTemplate; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx deleted file mode 100644 index 658ce502..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { Container } from "react-bootstrap"; -import { HubConnectionState } from "@microsoft/signalr"; - -import { HttpNetworkError, HttpNotFoundError } from "@/http/common"; -import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; - -import { getAlertHost } from "@/services/alert"; - -import Timeline from "./Timeline"; -import TimelinePostEdit from "./TimelinePostEdit"; - -import useReverseScrollPositionRemember from "@/utilities/useReverseScrollPositionRemember"; -import { generatePalette, setPalette } from "@/palette"; - -export interface TimelinePageCardProps { - timeline: HttpTimelineInfo; - collapse: boolean; - toggleCollapse: () => void; - connectionStatus: HubConnectionState; - className?: string; - onReload: () => void; -} - -export interface TimelinePageTemplateProps { - timelineName: string; - notFoundI18nKey: string; - reloadKey: number; - onReload: () => void; - CardComponent: React.ComponentType; -} - -const TimelinePageTemplate: React.FC = (props) => { - const { timelineName, reloadKey, onReload, CardComponent } = props; - - const { t } = useTranslation(); - - const [state, setState] = React.useState< - "loading" | "done" | "offline" | "notexist" | "error" - >("loading"); - const [timeline, setTimeline] = React.useState(null); - - const [connectionStatus, setConnectionStatus] = - React.useState(HubConnectionState.Connecting); - - useReverseScrollPositionRemember(); - - React.useEffect(() => { - setState("loading"); - setTimeline(null); - }, [timelineName]); - - React.useEffect(() => { - let subscribe = true; - void getHttpTimelineClient() - .getTimeline(timelineName) - .then( - (data) => { - if (subscribe) { - setState("done"); - setTimeline(data); - } - }, - (error) => { - if (subscribe) { - if (error instanceof HttpNetworkError) { - setState("offline"); - } else if (error instanceof HttpNotFoundError) { - setState("notexist"); - } else { - console.error(error); - setState("error"); - } - setTimeline(null); - } - } - ); - return () => { - subscribe = false; - }; - }, [timelineName, reloadKey]); - - React.useEffect(() => { - if (timeline != null && timeline.color != null) { - return setPalette(generatePalette({ primary: timeline.color })); - } - }, [timeline]); - - const [bottomSpaceHeight, setBottomSpaceHeight] = React.useState(0); - - const [timelineReloadKey, setTimelineReloadKey] = React.useState(0); - - const reloadTimeline = (): void => { - setTimelineReloadKey((old) => old + 1); - }; - - const onPostEditHeightChange = React.useCallback((height: number): void => { - setBottomSpaceHeight(height); - if (height === 0) { - const alertHost = getAlertHost(); - if (alertHost != null) { - alertHost.style.removeProperty("margin-bottom"); - } - } else { - const alertHost = getAlertHost(); - if (alertHost != null) { - alertHost.style.marginBottom = `${height}px`; - } - } - }, []); - - const cardCollapseLocalStorageKey = `timeline.${timelineName}.cardCollapse`; - - const [cardCollapse, setCardCollapse] = React.useState(true); - - React.useEffect(() => { - const savedCollapse = window.localStorage.getItem( - cardCollapseLocalStorageKey - ); - setCardCollapse(savedCollapse == null ? true : savedCollapse === "true"); - }, [cardCollapseLocalStorageKey]); - - const toggleCardCollapse = (): void => { - const newState = !cardCollapse; - setCardCollapse(newState); - window.localStorage.setItem( - cardCollapseLocalStorageKey, - newState.toString() - ); - }; - - return ( - <> - {timeline != null ? ( - - ) : null} - - {(() => { - if (state === "offline") { - // TODO: i18n - return

Offline!

; - } else if (state === "notexist") { - return

{t(props.notFoundI18nKey)}

; - } else if (state === "error") { - // TODO: i18n - return

Error!

; - } else { - return ( - - ); - } - })()} -
- {timeline != null && timeline.postable ? ( - <> -
- - - ) : null} - - ); -}; - -export default TimelinePageTemplate; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePagedPostListView.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePagedPostListView.tsx deleted file mode 100644 index 37f02a82..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelinePagedPostListView.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from "react"; - -import { HttpTimelinePostInfo } from "@/http/timeline"; - -import useScrollToTop from "@/utilities/useScrollToTop"; - -import TimelinePostListView from "./TimelinePostListView"; - -export interface TimelinePagedPostListViewProps { - className?: string; - style?: React.CSSProperties; - posts: HttpTimelinePostInfo[]; - onReload: () => void; -} - -const TimelinePagedPostListView: React.FC = ( - props -) => { - const { className, style, posts, onReload } = props; - - const [lastViewCount, setLastViewCount] = React.useState(10); - - const viewingPosts = React.useMemo(() => { - return lastViewCount >= posts.length - ? posts.slice() - : posts.slice(-lastViewCount); - }, [posts, lastViewCount]); - - useScrollToTop(() => { - setLastViewCount(lastViewCount + 10); - }, lastViewCount < posts.length); - - return ( - - ); -}; - -export default TimelinePagedPostListView; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx deleted file mode 100644 index 607b72c9..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import React from "react"; -import classnames from "classnames"; -import { Remarkable } from "remarkable"; - -import { UiLogicError } from "@/common"; - -import { HttpNetworkError } from "@/http/common"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; - -import { useUser } from "@/services/user"; - -import Skeleton from "../common/Skeleton"; -import LoadFailReload from "../common/LoadFailReload"; - -const TextView: React.FC = (props) => { - const { post, className, style } = props; - - const [text, setText] = React.useState(null); - const [error, setError] = React.useState<"offline" | "error" | null>(null); - - const [reloadKey, setReloadKey] = React.useState(0); - - React.useEffect(() => { - let subscribe = true; - - setText(null); - setError(null); - - void getHttpTimelineClient() - .getPostDataAsString(post.timelineName, post.id) - .then( - (data) => { - if (subscribe) setText(data); - }, - (error) => { - if (subscribe) { - if (error instanceof HttpNetworkError) { - setError("offline"); - } else { - setError("error"); - } - } - } - ); - - return () => { - subscribe = false; - }; - }, [post.timelineName, post.id, reloadKey]); - - if (error != null) { - return ( - setReloadKey(reloadKey + 1)} - /> - ); - } else if (text == null) { - return ; - } else { - return ( -
- {text} -
- ); - } -}; - -const ImageView: React.FC = (props) => { - const { post, className, style } = props; - - useUser(); - - return ( - - ); -}; - -const MarkdownView: React.FC = (props) => { - const { post, className, style } = props; - - const _remarkable = React.useRef(); - - const getRemarkable = (): Remarkable => { - if (_remarkable.current) { - return _remarkable.current; - } else { - _remarkable.current = new Remarkable(); - return _remarkable.current; - } - }; - - const [markdown, setMarkdown] = React.useState(null); - const [error, setError] = React.useState<"offline" | "error" | null>(null); - - const [reloadKey, setReloadKey] = React.useState(0); - - React.useEffect(() => { - let subscribe = true; - - setMarkdown(null); - setError(null); - - void getHttpTimelineClient() - .getPostDataAsString(post.timelineName, post.id) - .then( - (data) => { - if (subscribe) setMarkdown(data); - }, - (error) => { - if (subscribe) { - if (error instanceof HttpNetworkError) { - setError("offline"); - } else { - setError("error"); - } - } - } - ); - - return () => { - subscribe = false; - }; - }, [post.timelineName, post.id, reloadKey]); - - const markdownHtml = React.useMemo(() => { - if (markdown == null) return null; - return getRemarkable().render(markdown); - }, [markdown]); - - if (error != null) { - return ( - setReloadKey(reloadKey + 1)} - /> - ); - } else if (markdown == null) { - return ; - } else { - if (markdownHtml == null) { - throw new UiLogicError("Markdown is not null but markdown html is."); - } - return ( -
- ); - } -}; - -export interface TimelinePostContentViewProps { - post: HttpTimelinePostInfo; - className?: string; - style?: React.CSSProperties; -} - -const viewMap: Record> = { - "text/plain": TextView, - "text/markdown": MarkdownView, - "image/png": ImageView, - "image/jpeg": ImageView, - "image/gif": ImageView, - "image/webp": ImageView, -}; - -const TimelinePostContentView: React.FC = ( - props -) => { - const { post, className, style } = props; - - const type = post.dataList[0].kind; - - if (type in viewMap) { - const View = viewMap[type]; - return ; - } else { - // TODO: i18n - console.error("Unknown post type", post); - return
Error, unknown post type!
; - } -}; - -export default TimelinePostContentView; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx deleted file mode 100644 index b2c7a470..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostDeleteConfirmDialog.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; -import { Modal, Button } from "react-bootstrap"; -import { useTranslation } from "react-i18next"; - -const TimelinePostDeleteConfirmDialog: React.FC<{ - onClose: () => void; - onConfirm: () => void; -}> = ({ onClose, onConfirm }) => { - const { t } = useTranslation(); - - return ( - - - - {t("timeline.post.deleteDialog.title")} - - - {t("timeline.post.deleteDialog.prompt")} - - - - - - ); -}; - -export default TimelinePostDeleteConfirmDialog; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx deleted file mode 100644 index 5f3f0345..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx +++ /dev/null @@ -1,291 +0,0 @@ -import React from "react"; -import classnames from "classnames"; -import { useTranslation } from "react-i18next"; -import { Row, Col, Form } from "react-bootstrap"; - -import { UiLogicError } from "@/common"; - -import { - getHttpTimelineClient, - HttpTimelineInfo, - HttpTimelinePostInfo, - HttpTimelinePostPostRequestData, -} from "@/http/timeline"; - -import { pushAlert } from "@/services/alert"; -import { base64 } from "@/http/common"; - -import BlobImage from "../common/BlobImage"; -import LoadingButton from "../common/LoadingButton"; -import { PopupMenu } from "../common/Menu"; -import MarkdownPostEdit from "./MarkdownPostEdit"; - -interface TimelinePostEditTextProps { - text: string; - disabled: boolean; - onChange: (text: string) => void; - className?: string; - style?: React.CSSProperties; -} - -const TimelinePostEditText: React.FC = (props) => { - const { text, disabled, onChange, className, style } = props; - - return ( - { - onChange(event.target.value); - }} - className={className} - style={style} - /> - ); -}; - -interface TimelinePostEditImageProps { - onSelect: (file: File | null) => void; - disabled: boolean; -} - -const TimelinePostEditImage: React.FC = (props) => { - const { onSelect, disabled } = props; - - const { t } = useTranslation(); - - const [file, setFile] = React.useState(null); - const [error, setError] = React.useState(false); - - const onInputChange: React.ChangeEventHandler = (e) => { - setError(false); - const files = e.target.files; - if (files == null || files.length === 0) { - setFile(null); - onSelect(null); - } else { - setFile(files[0]); - } - }; - - React.useEffect(() => { - return () => { - onSelect(null); - }; - }, [onSelect]); - - return ( - <> - - {file != null && !error && ( - onSelect(file)} - onError={() => { - onSelect(null); - setError(true); - }} - /> - )} - {error ?
{t("loadImageError")}
: null} - - ); -}; - -type PostKind = "text" | "markdown" | "image"; - -const postKindIconClassNameMap: Record = { - text: "bi-fonts", - markdown: "bi-markdown", - image: "bi-image", -}; - -export interface TimelinePostEditProps { - className?: string; - timeline: HttpTimelineInfo; - onPosted: (newPost: HttpTimelinePostInfo) => void; - onHeightChange?: (height: number) => void; -} - -const TimelinePostEdit: React.FC = (props) => { - const { timeline, onHeightChange, className, onPosted } = props; - - const { t } = useTranslation(); - - const [process, setProcess] = React.useState(false); - - const [kind, setKind] = React.useState>("text"); - const [showMarkdown, setShowMarkdown] = React.useState(false); - - const [text, setText] = React.useState(""); - const [image, setImage] = React.useState(null); - - const draftTextLocalStorageKey = `timeline.${timeline.name}.postDraft.text`; - - React.useEffect(() => { - setText(window.localStorage.getItem(draftTextLocalStorageKey) ?? ""); - }, [draftTextLocalStorageKey]); - - const canSend = - (kind === "text" && text.length !== 0) || - (kind === "image" && image != null); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const containerRef = React.useRef(null!); - - const notifyHeightChange = (): void => { - if (onHeightChange) { - onHeightChange(containerRef.current.clientHeight); - } - }; - - React.useEffect(() => { - notifyHeightChange(); - return () => { - if (onHeightChange) { - onHeightChange(0); - } - }; - }); - - const onPostError = (): void => { - pushAlert({ - type: "danger", - message: "timeline.sendPostFailed", - }); - }; - - const onSend = async (): Promise => { - setProcess(true); - - let requestData: HttpTimelinePostPostRequestData; - switch (kind) { - case "text": - requestData = { - contentType: "text/plain", - data: await base64(text), - }; - break; - case "image": - if (image == null) { - throw new UiLogicError( - "Content type is image but image blob is null." - ); - } - requestData = { - contentType: image.type, - data: await base64(image), - }; - break; - default: - throw new UiLogicError("Unknown content type."); - } - - getHttpTimelineClient() - .postPost(timeline.name, { - dataList: [requestData], - }) - .then( - (data) => { - if (kind === "text") { - setText(""); - window.localStorage.removeItem(draftTextLocalStorageKey); - } - setProcess(false); - setKind("text"); - onPosted(data); - }, - (_) => { - setProcess(false); - onPostError(); - } - ); - }; - - return ( -
- {showMarkdown ? ( - setShowMarkdown(false)} - timeline={timeline.name} - onPosted={onPosted} - onPostError={onPostError} - /> - ) : ( - - - {(() => { - if (kind === "text") { - return ( - { - setText(t); - window.localStorage.setItem(draftTextLocalStorageKey, t); - }} - /> - ); - } else if (kind === "image") { - return ( - - ); - } - })()} - - -
- ({ - type: "button", - text: `timeline.post.type.${kind}`, - iconClassName: postKindIconClassNameMap[kind], - onClick: () => { - if (kind === "markdown") { - setShowMarkdown(true); - } else { - setKind(kind); - } - }, - }))} - > - - -
- - {t("timeline.send")} - - -
- )} -
- ); -}; - -export default TimelinePostEdit; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx deleted file mode 100644 index ba204b72..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostListView.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { Fragment } from "react"; -import classnames from "classnames"; - -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[]; - onReload: () => void; -} - -const TimelinePostListView: React.FC = (props) => { - const { className, style, posts, onReload } = 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 deleted file mode 100644 index f7b81478..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostView.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React from "react"; -import classnames from "classnames"; -import { Link } from "react-router-dom"; -import { useTranslation } from "react-i18next"; - -import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; - -import { pushAlert } from "@/services/alert"; - -import UserAvatar from "../common/user/UserAvatar"; -import TimelineLine from "./TimelineLine"; -import TimelinePostContentView from "./TimelinePostContentView"; -import TimelinePostDeleteConfirmDialog from "./TimelinePostDeleteConfirmDialog"; -import PostPropertyChangeDialog from "./PostPropertyChangeDialog"; - -export interface TimelinePostViewProps { - post: HttpTimelinePostInfo; - current?: boolean; - className?: string; - style?: React.CSSProperties; - cardStyle?: React.CSSProperties; - onChanged: (post: HttpTimelinePostInfo) => void; - onDeleted: () => void; -} - -const TimelinePostView: React.FC = (props) => { - const { post, className, style, cardStyle, onChanged, onDeleted } = props; - const current = props.current === true; - - const { t } = useTranslation(); - - const [operationMaskVisible, setOperationMaskVisible] = - React.useState(false); - const [dialog, setDialog] = React.useState< - "delete" | "changeproperty" | null - >(null); - - const cardRef = React.useRef(null); - React.useEffect(() => { - const cardIntersectionObserver = new IntersectionObserver(([e]) => { - if (e.intersectionRatio > 0) { - if (cardRef.current != null) { - cardRef.current.style.animationName = "timeline-post-enter"; - } - } - }); - if (cardRef.current) { - cardIntersectionObserver.observe(cardRef.current); - } - - return () => { - cardIntersectionObserver.disconnect(); - }; - }, []); - - return ( -
- -
- {post.editable ? ( - { - setOperationMaskVisible(true); - e.stopPropagation(); - }} - /> - ) : null} -
- - - - - - {post.author.nickname} - - {new Date(post.time).toLocaleTimeString()} - - - -
-
- -
- {operationMaskVisible ? ( -
{ - setOperationMaskVisible(false); - }} - > - { - setDialog("changeproperty"); - e.stopPropagation(); - }} - > - {t("changeProperty")} - - { - setDialog("delete"); - e.stopPropagation(); - }} - > - {t("delete")} - -
- ) : null} -
- {dialog === "delete" ? ( - { - setDialog(null); - setOperationMaskVisible(false); - }} - onConfirm={() => { - void getHttpTimelineClient() - .deletePost(post.timelineName, post.id) - .then(onDeleted, () => { - pushAlert({ - type: "danger", - message: "timeline.deletePostFailed", - }); - }); - }} - /> - ) : dialog === "changeproperty" ? ( - { - setDialog(null); - setOperationMaskVisible(false); - }} - post={post} - onSuccess={onChanged} - /> - ) : null} -
- ); -}; - -export default TimelinePostView; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx deleted file mode 100644 index 70f72025..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelinePropertyChangeDialog.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React from "react"; - -import { - getHttpTimelineClient, - HttpTimelineInfo, - HttpTimelinePatchRequest, - kTimelineVisibilities, - TimelineVisibility, -} from "@/http/timeline"; - -import OperationDialog from "../common/OperationDialog"; - -export interface TimelinePropertyChangeDialogProps { - open: boolean; - close: () => void; - timeline: HttpTimelineInfo; - onChange: () => void; -} - -const labelMap: { [key in TimelineVisibility]: string } = { - Private: "timeline.visibility.private", - Public: "timeline.visibility.public", - Register: "timeline.visibility.register", -}; - -const TimelinePropertyChangeDialog: React.FC = - (props) => { - const { timeline, onChange } = props; - - return ( - ({ - label: labelMap[v], - value: v, - })), - initValue: timeline.visibility, - }, - { - type: "text", - label: "timeline.dialogChangeProperty.description", - initValue: timeline.description, - }, - { - type: "color", - label: "timeline.dialogChangeProperty.color", - initValue: timeline.color ?? null, - canBeNull: true, - }, - ] as const - } - open={props.open} - close={props.close} - onProcess={([newTitle, newVisibility, newDescription, newColor]) => { - const req: HttpTimelinePatchRequest = {}; - if (newTitle !== timeline.title) { - req.title = newTitle; - } - if (newVisibility !== timeline.visibility) { - req.visibility = newVisibility as TimelineVisibility; - } - if (newDescription !== timeline.description) { - req.description = newDescription; - } - const nc = newColor ?? ""; - if (nc !== timeline.color) { - req.color = nc; - } - return getHttpTimelineClient() - .patchTimeline(timeline.name, req) - .then(onChange); - }} - /> - ); - }; - -export default TimelinePropertyChangeDialog; diff --git a/FrontEnd/src/app/views/timeline-common/TimelineTop.tsx b/FrontEnd/src/app/views/timeline-common/TimelineTop.tsx deleted file mode 100644 index dabbdf1e..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelineTop.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react"; -import classnames from "classnames"; - -import TimelineLine, { TimelineLineProps } from "./TimelineLine"; - -export interface TimelineTopProps { - height?: number | string; - lineProps?: TimelineLineProps; - className?: string; - style?: React.CSSProperties; -} - -const TimelineTop: React.FC = (props) => { - const { height, style, className } = props; - const lineProps = props.lineProps ?? { center: "none" }; - - return ( -
- -
- ); -}; - -export default TimelineTop; diff --git a/FrontEnd/src/app/views/timeline-common/timeline-common.sass b/FrontEnd/src/app/views/timeline-common/timeline-common.sass deleted file mode 100644 index 4400fead..00000000 --- a/FrontEnd/src/app/views/timeline-common/timeline-common.sass +++ /dev/null @@ -1,259 +0,0 @@ -@use 'sass:color' - -.timeline - z-index: 0 - position: relative - width: 100% - overflow-wrap: break-word - animation: 1s timeline-enter - -$timeline-line-width: 7px -$timeline-line-node-radius: 18px -$timeline-line-color: var(--tl-primary-color) -$timeline-line-color-current: var(--tl-primary-enhance-color) - -@keyframes timeline-line-node-noncurrent - to - box-shadow: 0 0 20px 3px var(--tl-primary-lighter-color) - -@keyframes timeline-line-node-current - to - box-shadow: 0 0 20px 3px var(--tl-primary-enhance-lighter-color) - -@keyframes timeline-line-node-loading - to - box-shadow: 0 0 20px 3px var(--tl-primary-lighter-color) - -@keyframes timeline-line-node-loading-edge - from - transform: rotate(0turn) - to - transform: rotate(1turn) - -@keyframes timeline-enter - from - transform: translate(0, -100vh) - -@keyframes timeline-top-loading-enter - from - transform: translate(0, -100%) - -@keyframes timeline-post-enter - from - transform: translate(0, -100%) - opacity: 0 - - to - opacity: 1 - -.timeline-top-loading-enter - animation: 1s timeline-top-loading-enter - -.timeline-line - display: flex - flex-direction: column - align-items: center - width: 30px - - position: absolute - z-index: 1 - left: 2em - top: 0 - bottom: 0 - - transition: left 0.5s - - @include media-breakpoint-down(sm) - left: 1em - - .segment - width: $timeline-line-width - background: $timeline-line-color - - &.start - height: 1.8em - flex: 0 0 auto - - &.end - flex: 1 1 auto - - &.current-end - height: 2em - flex: 0 0 auto - background: linear-gradient($timeline-line-color-current, white) - - .node-container - flex: 0 0 auto - position: relative - width: $timeline-line-node-radius - height: $timeline-line-node-radius - - .node - width: $timeline-line-node-radius + 2 - height: $timeline-line-node-radius + 2 - position: absolute - background: $timeline-line-color - left: -1px - top: -1px - border-radius: 50% - box-sizing: border-box - z-index: 1 - animation: 1s infinite alternate - animation-name: timeline-line-node-noncurrent - - .node-loading-edge - color: $timeline-line-color - width: $timeline-line-node-radius + 20 - height: $timeline-line-node-radius + 20 - position: absolute - left: -10px - top: -10px - box-sizing: border-box - z-index: 2 - animation: 1.5s linear infinite timeline-line-node-loading-edge - - &.current - .segment - &.start - background: linear-gradient($timeline-line-color, $timeline-line-color-current) - &.end - background: $timeline-line-color-current - .node - background: $timeline-line-color-current - animation-name: timeline-line-node-current - - &.loading - .node - background: $timeline-line-color - animation-name: timeline-line-node-loading - -.timeline-item.current - padding-bottom: 2.5em - -.timeline-top - position: relative - text-align: right - -.timeline-item - position: relative - padding: 0.5em - -.timeline-item-card - @extend .cru-card - position: relative - padding: 0.3em 0.5em 1em 4em - transition: background 0.5s, padding-left 0.5s - animation: 0.6s forwards - opacity: 0 - - @include media-breakpoint-down(sm) - padding-left: 3em - -.timeline-item-header - display: flex - align-items: center - @extend .my-2 - -.timeline-avatar - border-radius: 50% - width: 2em - height: 2em - -.timeline-item-delete-button - position: absolute - right: 0 - bottom: 0 - -.timeline-content - white-space: pre-line - -.timeline-content-image - max-width: 80% - max-height: 200px - -.timeline-date-item - position: relative - padding: 0.3em 0 0.3em 4em - -.timeline-date-item-badge - display: inline-block - padding: 0.1em 0.4em - border-radius: 0.4em - background: #7c7c7c - color: white - font-size: 0.8em - -.timeline-post-edit-image - max-width: 100px - max-height: 100px - -.mask - background: change-color($color: white, $alpha: 0.8) - z-index: 100 - -.timeline-sync-state-badge - font-size: 0.8em - padding: 3px 8px - border-radius: 5px - background: #e8fbff - -.timeline-sync-state-badge-pin - display: inline-block - width: 0.4em - height: 0.4em - border-radius: 50% - vertical-align: middle - margin-right: 0.6em - -.timeline-template-card - position: fixed - top: 56px - right: 0 - margin: 0.5em - -.timeline-markdown-post-edit-page - overflow: scroll - max-height: 300px - -.timeline-markdown-post-edit-image-container - position: relative - text-align: center - margin-bottom: 1em - -.timeline-markdown-post-edit-image - max-width: 100% - max-height: 200px - -.timeline-markdown-post-edit-image-delete-button - position: absolute - right: 10px - top: 2px - -.connection-status-badge - font-size: 0.8em - border-radius: 5px - padding: 0.1em 1em - background-color: rgb(234 242 255) - - &::before - width: 10px - height: 10px - border-radius: 50% - display: inline-block - content: '' - margin-right: 0.6em - - &.success - color: #006100 - &::before - background-color: #006100 - - &.warning - color: #e4a700 - &::before - background-color: #e4a700 - - &.danger - color: #fd1616 - &::before - background-color: #fd1616 -- cgit v1.2.3