From aa89b6cce7701a57b0c377d938788b4c940013d6 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 1 Sep 2020 02:32:06 +0800 Subject: ... --- Timeline/ClientApp/src/app/timeline/Timeline.tsx | 88 ------ .../src/app/timeline/TimelineDeleteDialog.tsx | 54 ---- .../src/app/timeline/TimelineInfoCard.tsx | 110 ------- .../ClientApp/src/app/timeline/TimelineItem.tsx | 181 ------------ .../ClientApp/src/app/timeline/TimelineMember.tsx | 218 -------------- .../ClientApp/src/app/timeline/TimelinePage.tsx | 37 --- .../src/app/timeline/TimelinePageTemplate.tsx | 190 ------------ .../src/app/timeline/TimelinePageTemplateUI.tsx | 324 --------------------- .../ClientApp/src/app/timeline/TimelinePageUI.tsx | 21 -- .../src/app/timeline/TimelinePostEdit.tsx | 232 --------------- .../app/timeline/TimelinePropertyChangeDialog.tsx | 71 ----- .../ClientApp/src/app/timeline/timeline-ui.sass | 35 --- Timeline/ClientApp/src/app/timeline/timeline.sass | 131 --------- 13 files changed, 1692 deletions(-) delete mode 100644 Timeline/ClientApp/src/app/timeline/Timeline.tsx delete mode 100644 Timeline/ClientApp/src/app/timeline/TimelineDeleteDialog.tsx delete mode 100644 Timeline/ClientApp/src/app/timeline/TimelineInfoCard.tsx delete mode 100644 Timeline/ClientApp/src/app/timeline/TimelineItem.tsx delete mode 100644 Timeline/ClientApp/src/app/timeline/TimelineMember.tsx delete mode 100644 Timeline/ClientApp/src/app/timeline/TimelinePage.tsx delete mode 100644 Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx delete mode 100644 Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx delete mode 100644 Timeline/ClientApp/src/app/timeline/TimelinePageUI.tsx delete mode 100644 Timeline/ClientApp/src/app/timeline/TimelinePostEdit.tsx delete mode 100644 Timeline/ClientApp/src/app/timeline/TimelinePropertyChangeDialog.tsx delete mode 100644 Timeline/ClientApp/src/app/timeline/timeline-ui.sass delete mode 100644 Timeline/ClientApp/src/app/timeline/timeline.sass (limited to 'Timeline/ClientApp/src/app/timeline') diff --git a/Timeline/ClientApp/src/app/timeline/Timeline.tsx b/Timeline/ClientApp/src/app/timeline/Timeline.tsx deleted file mode 100644 index 780588d1..00000000 --- a/Timeline/ClientApp/src/app/timeline/Timeline.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React from "react"; -import clsx from "clsx"; - -import { TimelinePostInfo } from "../data/timeline"; - -import TimelineItem from "./TimelineItem"; - -export interface TimelinePostInfoEx extends TimelinePostInfo { - deletable: boolean; -} - -export type TimelineDeleteCallback = (index: number, id: number) => void; - -export interface TimelineProps { - className?: string; - posts: TimelinePostInfoEx[]; - onDelete: TimelineDeleteCallback; - onResize?: () => void; - containerRef?: React.Ref; -} - -const Timeline: React.FC = (props) => { - const { posts, onDelete, onResize } = props; - - const [indexShowDeleteButton, setIndexShowDeleteButton] = React.useState< - number - >(-1); - - const onItemClick = React.useCallback(() => { - setIndexShowDeleteButton(-1); - }, []); - - const onToggleDelete = React.useMemo(() => { - return posts.map((post, i) => { - return post.deletable - ? () => { - setIndexShowDeleteButton((oldIndexShowDeleteButton) => { - return oldIndexShowDeleteButton !== i ? i : -1; - }); - } - : undefined; - }); - }, [posts]); - - const onItemDelete = React.useMemo(() => { - return posts.map((post, i) => { - return () => { - onDelete(i, post.id); - }; - }); - }, [posts, onDelete]); - - return ( -
-
- {(() => { - const length = posts.length; - return posts.map((post, i) => { - const toggleMore = onToggleDelete[i]; - - return ( - - ); - }); - })()} -
- ); -}; - -export default Timeline; diff --git a/Timeline/ClientApp/src/app/timeline/TimelineDeleteDialog.tsx b/Timeline/ClientApp/src/app/timeline/TimelineDeleteDialog.tsx deleted file mode 100644 index 5ebbf9df..00000000 --- a/Timeline/ClientApp/src/app/timeline/TimelineDeleteDialog.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from "react"; -import { useHistory } from "react-router"; -import { Trans } from "react-i18next"; - -import OperationDialog from "../common/OperationDialog"; -import { timelineService } from "../data/timeline"; - -interface TimelineDeleteDialog { - open: boolean; - name: string; - close: () => void; -} - -const TimelineDeleteDialog: React.FC = (props) => { - const history = useHistory(); - - const { name } = props; - - return ( - { - return ( - - 0{{ name }}2 - - ); - }} - inputScheme={[ - { - type: "text", - validator: (value) => { - if (value !== name) { - return "timeline.deleteDialog.notMatch"; - } else { - return null; - } - }, - }, - ]} - onProcess={() => { - return timelineService.deleteTimeline(name).toPromise(); - }} - onSuccessAndClose={() => { - history.replace("/"); - }} - /> - ); -}; - -export default TimelineDeleteDialog; diff --git a/Timeline/ClientApp/src/app/timeline/TimelineInfoCard.tsx b/Timeline/ClientApp/src/app/timeline/TimelineInfoCard.tsx deleted file mode 100644 index c11c3376..00000000 --- a/Timeline/ClientApp/src/app/timeline/TimelineInfoCard.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import { - Dropdown, - DropdownToggle, - DropdownMenu, - DropdownItem, - Button, -} from "reactstrap"; -import { useTranslation } from "react-i18next"; -import { fromEvent } from "rxjs"; - -import { useAvatar } from "../data/user"; -import { timelineVisibilityTooltipTranslationMap } from "../data/timeline"; -import BlobImage from "../common/BlobImage"; - -import { TimelineCardComponentProps } from "./TimelinePageTemplateUI"; - -export type OrdinaryTimelineManageItem = "delete"; - -export type TimelineInfoCardProps = TimelineCardComponentProps< - OrdinaryTimelineManageItem ->; - -const TimelineInfoCard: React.FC = (props) => { - const { onHeight, onManage } = props; - - const { t } = useTranslation(); - - const avatar = useAvatar(props.timeline.owner.username); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const containerRef = React.useRef(null!); - - const notifyHeight = React.useCallback((): void => { - if (onHeight) { - onHeight(containerRef.current.getBoundingClientRect().height); - } - }, [onHeight]); - - React.useEffect(() => { - const subscription = fromEvent(window, "resize").subscribe(notifyHeight); - return () => subscription.unsubscribe(); - }); - - const [manageDropdownOpen, setManageDropdownOpen] = React.useState( - false - ); - const toggleManageDropdown = React.useCallback( - (): void => setManageDropdownOpen((old) => !old), - [] - ); - - return ( -
-

- {props.timeline.name} -

-
- - {props.timeline.owner.nickname} - - @{props.timeline.owner.username} - -
-

{props.timeline.description}

- - {t(timelineVisibilityTooltipTranslationMap[props.timeline.visibility])} - -
- {onManage != null ? ( - - - {t("timeline.manage")} - - - onManage("property")}> - {t("timeline.manageItem.property")} - - - {t("timeline.manageItem.member")} - - - onManage("delete")} - > - {t("timeline.manageItem.delete")} - - - - ) : ( - - )} -
-
- ); -}; - -export default TimelineInfoCard; diff --git a/Timeline/ClientApp/src/app/timeline/TimelineItem.tsx b/Timeline/ClientApp/src/app/timeline/TimelineItem.tsx deleted file mode 100644 index 33f0741e..00000000 --- a/Timeline/ClientApp/src/app/timeline/TimelineItem.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import { - Row, - Col, - Modal, - ModalHeader, - ModalBody, - ModalFooter, - Button, -} from "reactstrap"; -import { Link } from "react-router-dom"; -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 BlobImage from "../common/BlobImage"; -import { useAvatar } from "../data/user"; -import { TimelinePostInfo } from "../data/timeline"; - -const TimelinePostDeleteConfirmDialog: React.FC<{ - toggle: () => void; - onConfirm: () => void; -}> = ({ toggle, onConfirm }) => { - const { t } = useTranslation(); - - return ( - - - {t("timeline.post.deleteDialog.title")} - - {t("timeline.post.deleteDialog.prompt")} - - - - - - ); -}; - -export interface TimelineItemProps { - post: TimelinePostInfo; - current?: boolean; - more?: { - isOpen: boolean; - toggle: () => void; - onDelete: () => void; - }; - onClick?: () => void; - onResize?: () => void; - className?: string; - style?: React.CSSProperties; -} - -const TimelineItem: React.FC = (props) => { - const { i18n } = useTranslation(); - - const current = props.current === true; - - const { more, onResize } = props; - - const avatar = useAvatar(props.post.author.username); - - const [deleteDialog, setDeleteDialog] = React.useState(false); - const toggleDeleteDialog = React.useCallback( - () => setDeleteDialog((old) => !old), - [] - ); - - return ( - - -
-
-
-
-
- {current &&
} - - - -
- - - {props.post.time.toLocaleString(i18n.languages)} - - - {props.post.author.nickname} - - -
- {more != null ? ( -
- { - more.toggle(); - e.stopPropagation(); - }} - /> -
- ) : null} -
-
- - - - {(() => { - const { content } = props.post; - if (content.type === "text") { - return content.text; - } else { - return ( - - ); - } - })()} -
- - {more != null && more.isOpen ? ( - <> -
- { - toggleDeleteDialog(); - e.stopPropagation(); - }} - /> -
- {deleteDialog ? ( - { - toggleDeleteDialog(); - more.toggle(); - }} - onConfirm={more.onDelete} - /> - ) : null} - - ) : null} - - ); -}; - -export default TimelineItem; diff --git a/Timeline/ClientApp/src/app/timeline/TimelineMember.tsx b/Timeline/ClientApp/src/app/timeline/TimelineMember.tsx deleted file mode 100644 index f334c6e9..00000000 --- a/Timeline/ClientApp/src/app/timeline/TimelineMember.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import React, { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { - Container, - ListGroup, - ListGroupItem, - Modal, - Row, - Col, - Button, -} from "reactstrap"; - -import { User, useAvatar } from "../data/user"; -import SearchInput from "../common/SearchInput"; -import BlobImage from "../common/BlobImage"; - -const TimelineMemberItem: React.FC<{ - user: User; - owner: boolean; - onRemove?: (username: string) => void; -}> = ({ user, owner, onRemove }) => { - const { t } = useTranslation(); - - const avatar = useAvatar(user.username); - - return ( - - - - - - - {user.nickname} - - {"@" + user.username} - - - {(() => { - if (owner) { - return null; - } - if (onRemove == null) { - return null; - } - return ( - - ); - })()} - - - ); -}; - -export interface TimelineMemberCallbacks { - onCheckUser: (username: string) => Promise; - onAddUser: (user: User) => Promise; - onRemoveUser: (username: string) => void; -} - -export interface TimelineMemberProps { - members: User[]; - edit: TimelineMemberCallbacks | null | undefined; -} - -const TimelineMember: React.FC = (props) => { - const { t } = useTranslation(); - - const [userSearchText, setUserSearchText] = useState(""); - const [userSearchState, setUserSearchState] = useState< - | { - type: "user"; - data: User; - } - | { type: "error"; data: string } - | { type: "loading" } - | { type: "init" } - >({ type: "init" }); - - const userSearchAvatar = useAvatar( - userSearchState.type === "user" ? userSearchState.data.username : undefined - ); - - const members = props.members; - - return ( - - - {members.map((member, index) => ( - - ))} - - {(() => { - const edit = props.edit; - if (edit != null) { - return ( - <> - { - setUserSearchText(v); - }} - loading={userSearchState.type === "loading"} - onButtonClick={() => { - if (userSearchText === "") { - setUserSearchState({ - type: "error", - data: "login.emptyUsername", - }); - return; - } - - setUserSearchState({ type: "loading" }); - edit.onCheckUser(userSearchText).then( - (u) => { - if (u == null) { - setUserSearchState({ - type: "error", - data: "timeline.userNotExist", - }); - } else { - setUserSearchState({ type: "user", data: u }); - } - }, - (e) => { - setUserSearchState({ - type: "error", - data: `${e as string}`, - }); - } - ); - }} - /> - {(() => { - if (userSearchState.type === "user") { - const u = userSearchState.data; - const addable = - members.findIndex((m) => m.username === u.username) === -1; - return ( - <> - {!addable ? ( -

{t("timeline.member.alreadyMember")}

- ) : null} - - - - - - - {u.nickname} - - {"@" + u.username} - - - - - - - ); - } else if (userSearchState.type === "error") { - return ( -

{t(userSearchState.data)}

- ); - } - })()} - - ); - } else { - return null; - } - })()} -
- ); -}; - -export default TimelineMember; - -export interface TimelineMemberDialogProps extends TimelineMemberProps { - open: boolean; - onClose: () => void; -} - -export const TimelineMemberDialog: React.FC = ( - props -) => { - return ( - - - - ); -}; diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePage.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePage.tsx deleted file mode 100644 index 21d52db1..00000000 --- a/Timeline/ClientApp/src/app/timeline/TimelinePage.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; -import { useParams } from "react-router"; - -import TimelinePageTemplate from "../timeline/TimelinePageTemplate"; - -import TimelinePageUI from "./TimelinePageUI"; -import { OrdinaryTimelineManageItem } from "./TimelineInfoCard"; -import TimelineDeleteDialog from "./TimelineDeleteDialog"; - -const TimelinePage: React.FC = (_) => { - const { name } = useParams<{ name: string }>(); - - const [dialog, setDialog] = React.useState( - null - ); - - let dialogElement: React.ReactElement | undefined; - if (dialog === "delete") { - dialogElement = ( - setDialog(null)} name={name} /> - ); - } - - return ( - <> - setDialog(item)} - notFoundI18nKey="timeline.timelineNotExist" - /> - {dialogElement} - - ); -}; - -export default TimelinePage; diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx deleted file mode 100644 index 62470e63..00000000 --- a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { of } from "rxjs"; -import { catchError } from "rxjs/operators"; - -import { ExcludeKey } from "../utilities/type"; -import { pushAlert } from "../common/alert-service"; -import { useUser, userInfoService, UserNotExistError } from "../data/user"; -import { - timelineService, - usePostList, - useTimelineInfo, -} from "../data/timeline"; -import { UiLogicError } from "../common"; - -import { TimelineDeleteCallback } from "./Timeline"; -import { TimelineMemberDialog } from "./TimelineMember"; -import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; -import { TimelinePageTemplateUIProps } from "./TimelinePageTemplateUI"; -import { TimelinePostSendCallback } from "./TimelinePostEdit"; - -export interface TimelinePageTemplateProps { - name: string; - onManage: (item: TManageItem) => void; - UiComponent: React.ComponentType< - ExcludeKey, "CardComponent"> - >; - notFoundI18nKey: string; -} - -export default function TimelinePageTemplate( - props: TimelinePageTemplateProps -): React.ReactElement | null { - const { t } = useTranslation(); - - const { name } = props; - - const service = timelineService; - - const user = useUser(); - - const [dialog, setDialog] = React.useState( - null - ); - - const timelineState = useTimelineInfo(name); - - const timeline = timelineState?.timeline; - - const postListState = usePostList(name); - - const error: string | undefined = (() => { - if (timelineState != null) { - const { type, timeline } = timelineState; - if (type === "offline" && timeline == null) return "Network Error"; - if (type === "synced" && timeline == null) - return t(props.notFoundI18nKey); - } - return undefined; - })(); - - const closeDialog = React.useCallback((): void => { - setDialog(null); - }, []); - - let dialogElement: React.ReactElement | undefined; - - if (dialog === "property") { - if (timeline == null) { - throw new UiLogicError( - "Timeline is null but attempt to open change property dialog." - ); - } - - dialogElement = ( - { - return service.changeTimelineProperty(name, req).toPromise().then(); - }} - /> - ); - } else if (dialog === "member") { - if (timeline == null) { - throw new UiLogicError( - "Timeline is null but attempt to open change property dialog." - ); - } - - dialogElement = ( - { - return userInfoService - .getUserInfo(u) - .pipe( - catchError((e) => { - if (e instanceof UserNotExistError) { - return of(null); - } else { - throw e; - } - }) - ) - .toPromise(); - }, - onAddUser: (u) => { - return service.addMember(name, u.username).toPromise().then(); - }, - onRemoveUser: (u) => { - service.removeMember(name, u); - }, - } - : null - } - /> - ); - } - - const { UiComponent } = props; - - const onDelete: TimelineDeleteCallback = React.useCallback( - (index, id) => { - service.deletePost(name, id).subscribe(null, () => { - pushAlert({ - type: "danger", - message: t("timeline.deletePostFailed"), - }); - }); - }, - [service, name, t] - ); - - const onPost: TimelinePostSendCallback = React.useCallback( - (req) => { - return service.createPost(name, req).toPromise().then(); - }, - [service, name] - ); - - const onManageProp = props.onManage; - - const onManage = React.useCallback( - (item: "property" | TManageItem) => { - if (item === "property") { - setDialog(item); - } else { - onManageProp(item); - } - }, - [onManageProp] - ); - - const onMember = React.useCallback(() => { - setDialog("member"); - }, []); - - return ( - <> - - {dialogElement} - - ); -} diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx deleted file mode 100644 index e6514478..00000000 --- a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplateUI.tsx +++ /dev/null @@ -1,324 +0,0 @@ -import React, { CSSProperties } from "react"; -import { Spinner } from "reactstrap"; -import { useTranslation } from "react-i18next"; -import { fromEvent } from "rxjs"; -import Svg from "react-inlinesvg"; -import clsx from "clsx"; -import arrowsAngleContractIcon from "bootstrap-icons/icons/arrows-angle-contract.svg"; -import arrowsAngleExpandIcon from "bootstrap-icons/icons/arrows-angle-expand.svg"; - -import { getAlertHost } from "../common/alert-service"; -import { useEventEmiiter, UiLogicError } from "../common"; -import { - TimelineInfo, - TimelinePostsWithSyncState, - timelineService, -} from "../data/timeline"; -import { userService } from "../data/user"; -import AppBar from "../common/AppBar"; - -import Timeline, { - TimelinePostInfoEx, - 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 ( -
- {(() => { - switch (state) { - case "syncing": { - return ( - <> - - - {t("timeline.postSyncState.syncing")} - - - ); - } - case "synced": { - return ( - <> - - - {t("timeline.postSyncState.synced")} - - - ); - } - case "offline": { - return ( - <> - - - {t("timeline.postSyncState.offline")} - - - ); - } - default: - throw new UiLogicError("Unknown sync state."); - } - })()} -
- ); -}; - -export interface TimelineCardComponentProps { - timeline: TimelineInfo; - onManage?: (item: TManageItems | "property") => void; - onMember: () => void; - className?: string; - onHeight?: (height: number) => void; -} - -export interface TimelinePageTemplateUIProps { - avatarKey?: string | number; - timeline?: TimelineInfo; - postListState?: TimelinePostsWithSyncState; - CardComponent: React.ComponentType>; - onMember: () => void; - onManage?: (item: TManageItems | "property") => void; - onPost?: TimelinePostSendCallback; - onDelete: TimelineDeleteCallback; - error?: string; -} - -export default function TimelinePageTemplateUI( - props: TimelinePageTemplateUIProps -): React.ReactElement | null { - const { timeline, postListState } = props; - - const { t } = useTranslation(); - - const bottomSpaceRef = React.useRef(null); - - const onPostEditHeightChange = React.useCallback((height: number): void => { - const { current: bottomSpaceDiv } = bottomSpaceRef; - if (bottomSpaceDiv != null) { - bottomSpaceDiv.style.height = `${height}px`; - } - 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 timelineRef = React.useRef(null); - - const [getResizeEvent, triggerResizeEvent] = useEventEmiiter(); - - React.useEffect(() => { - const { current: timelineElement } = timelineRef; - if (timelineElement != null) { - let loadingScrollToBottom = true; - let pinBottom = false; - - const isAtBottom = (): boolean => - window.innerHeight + window.scrollY + 10 >= document.body.scrollHeight; - - const disableLoadingScrollToBottom = (): void => { - loadingScrollToBottom = false; - if (isAtBottom()) pinBottom = true; - }; - - const checkAndScrollToBottom = (): void => { - if (loadingScrollToBottom || pinBottom) { - window.scrollTo(0, document.body.scrollHeight); - } - }; - - const subscriptions = [ - fromEvent(timelineElement, "wheel").subscribe( - disableLoadingScrollToBottom - ), - fromEvent(timelineElement, "pointerdown").subscribe( - disableLoadingScrollToBottom - ), - fromEvent(timelineElement, "keydown").subscribe( - disableLoadingScrollToBottom - ), - fromEvent(window, "scroll").subscribe(() => { - if (loadingScrollToBottom) return; - - if (isAtBottom()) { - pinBottom = true; - } else { - pinBottom = false; - } - }), - fromEvent(window, "resize").subscribe(checkAndScrollToBottom), - getResizeEvent().subscribe(checkAndScrollToBottom), - ]; - - return () => { - subscriptions.forEach((s) => s.unsubscribe()); - }; - } - }, [getResizeEvent, triggerResizeEvent, timeline, postListState]); - - const [cardHeight, setCardHeight] = React.useState(0); - - const genCardCollapseLocalStorageKey = (uniqueId: string): string => - `timeline.${uniqueId}.cardCollapse`; - - const cardCollapseLocalStorageKey = - timeline != null ? genCardCollapseLocalStorageKey(timeline.uniqueId) : null; - - const [infoCardCollapse, setInfoCardCollapse] = React.useState(true); - React.useEffect(() => { - if (cardCollapseLocalStorageKey != null) { - const savedCollapse = - window.localStorage.getItem(cardCollapseLocalStorageKey) === "true"; - setInfoCardCollapse(savedCollapse); - } - }, [cardCollapseLocalStorageKey]); - - let body: React.ReactElement; - - if (props.error != null) { - body =

{t(props.error)}

; - } else { - if (timeline != null) { - let timelineBody: React.ReactElement; - if (postListState != null) { - if (postListState.type === "notexist") { - throw new UiLogicError( - "Timeline is not null but post list state is notexist." - ); - } - if (postListState.type === "forbid") { - timelineBody = ( -

{t("timeline.messageCantSee")}

- ); - } else { - const posts: TimelinePostInfoEx[] = postListState.posts.map( - (post) => ({ - ...post, - deletable: timelineService.hasModifyPostPermission( - userService.currentUser, - timeline, - post - ), - }) - ); - - const topHeight: string = infoCardCollapse - ? "calc(68px + 1.5em)" - : `${cardHeight + 60}px`; - - const syncState: TimelinePostSyncState = postListState.syncing - ? "syncing" - : postListState.type === "synced" - ? "synced" - : "offline"; - - timelineBody = ( -
- - -
- ); - if (props.onPost != null) { - timelineBody = ( - <> - {timelineBody} -
- - - ); - } - } - } else { - timelineBody = ( -
- -
- ); - } - const { CardComponent } = props; - - body = ( - <> -
- { - const newState = !infoCardCollapse; - setInfoCardCollapse(newState); - window.localStorage.setItem( - genCardCollapseLocalStorageKey(timeline.uniqueId), - newState.toString() - ); - }} - className="float-right m-1 info-card-collapse-button text-primary icon-button" - /> - -
- {timelineBody} - - ); - } else { - body = ( -
- -
- ); - } - } - - return ( - <> - -
-
- {body} -
- - ); -} diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageUI.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageUI.tsx deleted file mode 100644 index 63751eeb..00000000 --- a/Timeline/ClientApp/src/app/timeline/TimelinePageUI.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; - -import { ExcludeKey } from "../utilities/type"; - -import TimelinePageTemplateUI, { - TimelinePageTemplateUIProps, -} from "./TimelinePageTemplateUI"; -import TimelineInfoCard, { - OrdinaryTimelineManageItem, -} from "./TimelineInfoCard"; - -export type TimelinePageUIProps = ExcludeKey< - TimelinePageTemplateUIProps, - "CardComponent" ->; - -const TimelinePageUI: React.FC = (props) => { - return ; -}; - -export default TimelinePageUI; diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePostEdit.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePostEdit.tsx deleted file mode 100644 index b30dc8d3..00000000 --- a/Timeline/ClientApp/src/app/timeline/TimelinePostEdit.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import React from "react"; -import { Button, Spinner, Row, Col } from "reactstrap"; -import { useTranslation } from "react-i18next"; -import Svg from "react-inlinesvg"; -import textIcon from "bootstrap-icons/icons/card-text.svg"; -import imageIcon from "bootstrap-icons/icons/image.svg"; - -import { pushAlert } from "../common/alert-service"; -import { TimelineCreatePostRequest } from "../data/timeline"; -import FileInput from "../common/FileInput"; -import { UiLogicError } from "../common"; - -interface TimelinePostEditImageProps { - onSelect: (blob: Blob | null) => void; -} - -const TimelinePostEditImage: React.FC = (props) => { - const { onSelect } = props; - const { t } = useTranslation(); - - const [file, setFile] = React.useState(null); - const [fileUrl, setFileUrl] = React.useState(null); - const [error, setError] = React.useState(null); - - React.useEffect(() => { - if (file != null) { - const url = URL.createObjectURL(file); - setFileUrl(url); - return () => { - URL.revokeObjectURL(url); - }; - } - }, [file]); - - const onInputChange: React.ChangeEventHandler = React.useCallback( - (e) => { - const files = e.target.files; - if (files == null || files.length === 0) { - setFile(null); - setFileUrl(null); - } else { - setFile(files[0]); - } - onSelect(null); - setError(null); - }, - [onSelect] - ); - - const onImgLoad = React.useCallback(() => { - onSelect(file); - }, [onSelect, file]); - - const onImgError = React.useCallback(() => { - setError("loadImageError"); - }, []); - - return ( - <> - - {fileUrl && error == null && ( - - )} - {error != null &&
{t(error)}
} - - ); -}; - -export type TimelinePostSendCallback = ( - content: TimelineCreatePostRequest -) => Promise; - -export interface TimelinePostEditProps { - className?: string; - onPost: TimelinePostSendCallback; - onHeightChange?: (height: number) => void; - timelineUniqueId: string; -} - -const TimelinePostEdit: React.FC = (props) => { - const { onPost } = props; - - const { t } = useTranslation(); - - const [state, setState] = React.useState<"input" | "process">("input"); - const [kind, setKind] = React.useState<"text" | "image">("text"); - const [text, setText] = React.useState(""); - const [imageBlob, setImageBlob] = React.useState(null); - - const draftLocalStorageKey = `timeline.${props.timelineUniqueId}.postDraft`; - - React.useEffect(() => { - setText(window.localStorage.getItem(draftLocalStorageKey) ?? ""); - }, [draftLocalStorageKey]); - - const canSend = kind === "text" || (kind === "image" && imageBlob != null); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const containerRef = React.useRef(null!); - - const notifyHeightChange = (): void => { - if (props.onHeightChange) { - props.onHeightChange(containerRef.current.clientHeight); - } - }; - - React.useEffect(() => { - if (props.onHeightChange) { - props.onHeightChange(containerRef.current.clientHeight); - } - return () => { - if (props.onHeightChange) { - props.onHeightChange(0); - } - }; - }); - - const toggleKind = React.useCallback(() => { - setKind((oldKind) => (oldKind === "text" ? "image" : "text")); - setImageBlob(null); - }, []); - - const onSend = React.useCallback(() => { - setState("process"); - - const req: TimelineCreatePostRequest = (() => { - switch (kind) { - case "text": - return { - content: { - type: "text", - text: text, - }, - } as TimelineCreatePostRequest; - case "image": - if (imageBlob == null) { - throw new UiLogicError( - "Content type is image but image blob is null." - ); - } - return { - content: { - type: "image", - data: imageBlob, - }, - } as TimelineCreatePostRequest; - default: - throw new UiLogicError("Unknown content type."); - } - })(); - - onPost(req).then( - (_) => { - if (kind === "text") { - setText(""); - window.localStorage.removeItem(draftLocalStorageKey); - } - setState("input"); - setKind("text"); - }, - (_) => { - pushAlert({ - type: "danger", - message: t("timeline.sendPostFailed"), - }); - setState("input"); - } - ); - }, [onPost, kind, text, imageBlob, t, draftLocalStorageKey]); - - const onImageSelect = React.useCallback((blob: Blob | null) => { - setImageBlob(blob); - }, []); - - return ( -
- - - {kind === "text" ? ( -