diff options
-rw-r--r-- | FrontEnd/src/components/dialog/ConfirmDialog.tsx | 14 | ||||
-rw-r--r-- | FrontEnd/src/components/dialog/Dialog.tsx | 18 | ||||
-rw-r--r-- | FrontEnd/src/components/dialog/DialogProvider.tsx | 95 | ||||
-rw-r--r-- | FrontEnd/src/components/dialog/OperationDialog.tsx | 45 | ||||
-rw-r--r-- | FrontEnd/src/components/dialog/index.ts | 65 | ||||
-rw-r--r-- | FrontEnd/src/components/dialog/index.tsx | 12 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx | 32 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx | 11 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/ChangePasswordDialog.tsx | 11 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/index.tsx | 130 | ||||
-rw-r--r-- | FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx | 33 | ||||
-rw-r--r-- | FrontEnd/src/pages/timeline/TimelineCard.tsx | 28 | ||||
-rw-r--r-- | FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx | 4 | ||||
-rw-r--r-- | FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx | 2 | ||||
-rw-r--r-- | FrontEnd/src/pages/timeline/TimelinePostView.tsx | 48 | ||||
-rw-r--r-- | FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx | 4 |
16 files changed, 285 insertions, 267 deletions
diff --git a/FrontEnd/src/components/dialog/ConfirmDialog.tsx b/FrontEnd/src/components/dialog/ConfirmDialog.tsx index 1d997305..a7b3917f 100644 --- a/FrontEnd/src/components/dialog/ConfirmDialog.tsx +++ b/FrontEnd/src/components/dialog/ConfirmDialog.tsx @@ -1,18 +1,16 @@ import { useC, Text, ThemeColor } from "../common"; + import Dialog from "./Dialog"; import DialogContainer from "./DialogContainer"; +import { useCloseDialog } from "./DialogProvider"; export default function ConfirmDialog({ - open, - onClose, onConfirm, title, body, color, bodyColor, }: { - open: boolean; - onClose: () => void; onConfirm: () => void; title: Text; body: Text; @@ -21,8 +19,10 @@ export default function ConfirmDialog({ }) { const c = useC(); + const closeDialog = useCloseDialog(); + return ( - <Dialog onClose={onClose} open={open}> + <Dialog> <DialogContainer title={title} titleColor={color ?? "danger"} @@ -34,7 +34,7 @@ export default function ConfirmDialog({ text: "operationDialog.cancel", color: "secondary", outline: true, - onClick: onClose, + onClick: closeDialog, }, }, { @@ -45,7 +45,7 @@ export default function ConfirmDialog({ color: "danger", onClick: () => { onConfirm(); - onClose(); + closeDialog(); }, }, }, diff --git a/FrontEnd/src/components/dialog/Dialog.tsx b/FrontEnd/src/components/dialog/Dialog.tsx index 2ff7bea8..b1d66704 100644 --- a/FrontEnd/src/components/dialog/Dialog.tsx +++ b/FrontEnd/src/components/dialog/Dialog.tsx @@ -5,6 +5,8 @@ import classNames from "classnames"; import { ThemeColor } from "../common"; +import { useCloseDialog } from "./DialogProvider"; + import "./Dialog.css"; const optionalPortalElement = document.getElementById("portal"); @@ -14,22 +16,20 @@ if (optionalPortalElement == null) { const portalElement = optionalPortalElement; interface DialogProps { - open: boolean; - onClose: () => void; color?: ThemeColor; children?: ReactNode; disableCloseOnClickOnOverlay?: boolean; } export default function Dialog({ - open, - onClose, color, children, disableCloseOnClickOnOverlay, }: DialogProps) { color = color ?? "primary"; + const closeDialog = useCloseDialog(); + const nodeRef = useRef(null); return ReactDOM.createPortal( @@ -37,7 +37,7 @@ export default function Dialog({ nodeRef={nodeRef} mountOnEnter unmountOnExit - in={open} + in timeout={300} classNames="cru-dialog" > @@ -47,13 +47,7 @@ export default function Dialog({ > <div className="cru-dialog-background" - onClick={ - disableCloseOnClickOnOverlay - ? undefined - : () => { - onClose(); - } - } + onClick={disableCloseOnClickOnOverlay ? undefined : closeDialog} /> <div className="cru-dialog-container">{children}</div> </div> diff --git a/FrontEnd/src/components/dialog/DialogProvider.tsx b/FrontEnd/src/components/dialog/DialogProvider.tsx new file mode 100644 index 00000000..bb85e4cf --- /dev/null +++ b/FrontEnd/src/components/dialog/DialogProvider.tsx @@ -0,0 +1,95 @@ +import { useState, useContext, createContext, ReactNode } from "react"; + +import { UiLogicError } from "../common"; + +type DialogMap<D extends string> = { + [K in D]: ReactNode; +}; + +interface DialogController<D extends string> { + currentDialog: D | null; + currentDialogReactNode: ReactNode; + canSwitchDialog: boolean; + switchDialog: (newDialog: D | null) => void; + setCanSwitchDialog: (enable: boolean) => void; + closeDialog: () => void; + forceSwitchDialog: (newDialog: D | null) => void; + forceCloseDialog: () => void; +} + +export function useDialog<D extends string>( + dialogs: DialogMap<D>, + options?: { + initDialog?: D | null; + onClose?: { + [K in D]?: () => void; + }; + }, +): { + controller: DialogController<D>; + switchDialog: (newDialog: D | null) => void; + forceSwitchDialog: (newDialog: D | null) => void; + createDialogSwitch: (newDialog: D | null) => () => void; +} { + const [canSwitchDialog, setCanSwitchDialog] = useState<boolean>(true); + const [dialog, setDialog] = useState<D | null>(options?.initDialog ?? null); + + const forceSwitchDialog = (newDialog: D | null) => { + if (dialog != null) { + options?.onClose?.[dialog]?.(); + } + setDialog(newDialog); + setCanSwitchDialog(true); + }; + + const switchDialog = (newDialog: D | null) => { + if (canSwitchDialog) { + forceSwitchDialog(newDialog); + } + }; + + const controller: DialogController<D> = { + currentDialog: dialog, + currentDialogReactNode: dialog == null ? null : dialogs[dialog], + canSwitchDialog, + switchDialog, + setCanSwitchDialog, + closeDialog: () => switchDialog(null), + forceSwitchDialog, + forceCloseDialog: () => forceSwitchDialog(null), + }; + + return { + controller, + switchDialog, + forceSwitchDialog, + createDialogSwitch: (newDialog: D | null) => () => switchDialog(newDialog), + }; +} + +const DialogControllerContext = createContext<DialogController<string> | null>( + null, +); + +export function useDialogController(): DialogController<string> { + const controller = useContext(DialogControllerContext); + if (controller == null) throw new UiLogicError("not in dialog provider"); + return controller; +} + +export function useCloseDialog(): () => void { + const controller = useDialogController(); + return controller.closeDialog; +} + +export function DialogProvider<D extends string>({ + controller, +}: { + controller: DialogController<D>; +}) { + return ( + <DialogControllerContext.Provider value={controller as never}> + {controller.currentDialogReactNode} + </DialogControllerContext.Provider> + ); +} diff --git a/FrontEnd/src/components/dialog/OperationDialog.tsx b/FrontEnd/src/components/dialog/OperationDialog.tsx index 96766825..902d60c6 100644 --- a/FrontEnd/src/components/dialog/OperationDialog.tsx +++ b/FrontEnd/src/components/dialog/OperationDialog.tsx @@ -11,6 +11,7 @@ import { import { ButtonRow } from "../button"; import Dialog from "./Dialog"; import DialogContainer from "./DialogContainer"; +import { useDialogController } from "./DialogProvider"; import "./OperationDialog.css"; @@ -35,9 +36,6 @@ function OperationDialogPrompt(props: OperationDialogPromptProps) { } export interface OperationDialogProps<TData> { - open: boolean; - onClose: () => void; - color?: ThemeColor; inputColor?: ThemeColor; title: Text; @@ -56,8 +54,6 @@ export interface OperationDialogProps<TData> { function OperationDialog<TData>(props: OperationDialogProps<TData>) { const { - open, - onClose, color, inputColor, title, @@ -96,6 +92,8 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) { data: unknown; }; + const dialogController = useDialogController(); + const [step, setStep] = useState<Step>({ type: "input" }); const { inputGroupProps, hasErrorAndDirty, setAllDisabled, confirm } = @@ -105,7 +103,7 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) { function close() { if (step.type !== "process") { - onClose(); + dialogController.closeDialog(); if (step.type === "success" && onSuccessAndClose) { onSuccessAndClose?.(step.data); } @@ -118,21 +116,26 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) { const result = confirm(); if (result.type === "ok") { setStep({ type: "process" }); + dialogController.setCanSwitchDialog(false); setAllDisabled(true); - onProcess(result.values).then( - (d) => { - setStep({ - type: "success", - data: d, - }); - }, - (e: unknown) => { - setStep({ - type: "failure", - data: e, - }); - }, - ); + onProcess(result.values) + .then( + (d) => { + setStep({ + type: "success", + data: d, + }); + }, + (e: unknown) => { + setStep({ + type: "failure", + data: e, + }); + }, + ) + .finally(() => { + dialogController.setCanSwitchDialog(true); + }); } } @@ -214,7 +217,7 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) { } return ( - <Dialog open={open} onClose={close}> + <Dialog> <DialogContainer title={title} titleColor={color} buttons={buttons}> {body} </DialogContainer> diff --git a/FrontEnd/src/components/dialog/index.ts b/FrontEnd/src/components/dialog/index.ts deleted file mode 100644 index 17db8fd0..00000000 --- a/FrontEnd/src/components/dialog/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { useState } from "react"; - -export { default as Dialog } from "./Dialog"; -export { default as FullPageDialog } from "./FullPageDialog"; -export { default as OperationDialog } from "./OperationDialog"; -export { default as ConfirmDialog } from "./ConfirmDialog"; -export { default as DialogContainer } from "./DialogContainer"; - -type DialogMap<D extends string, V> = { - [K in D]: V; -}; - -type DialogKeyMap<D extends string> = DialogMap<D, number>; - -type DialogPropsMap<D extends string> = DialogMap< - D, - { key: number | string; open: boolean; onClose: () => void } ->; - -export function useDialog<D extends string>( - dialogs: D[], - options?: { - initDialog?: D | null; - onClose?: { - [K in D]?: () => void; - }; - }, -): { - dialog: D | null; - switchDialog: (newDialog: D | null) => void; - dialogPropsMap: DialogPropsMap<D>; - createDialogSwitch: (newDialog: D | null) => () => void; -} { - const [dialog, setDialog] = useState<D | null>(options?.initDialog ?? null); - - const [dialogKeys, setDialogKeys] = useState<DialogKeyMap<D>>( - () => Object.fromEntries(dialogs.map((d) => [d, 0])) as DialogKeyMap<D>, - ); - - const switchDialog = (newDialog: D | null) => { - if (dialog !== null) { - setDialogKeys({ ...dialogKeys, [dialog]: dialogKeys[dialog] + 1 }); - } - setDialog(newDialog); - }; - - return { - dialog, - switchDialog, - dialogPropsMap: Object.fromEntries( - dialogs.map((d) => [ - d, - { - key: `${d}-${dialogKeys[d]}`, - open: dialog === d, - onClose: () => { - switchDialog(null); - options?.onClose?.[d]?.(); - }, - }, - ]), - ) as DialogPropsMap<D>, - createDialogSwitch: (newDialog: D | null) => () => switchDialog(newDialog), - }; -} diff --git a/FrontEnd/src/components/dialog/index.tsx b/FrontEnd/src/components/dialog/index.tsx new file mode 100644 index 00000000..9ca06de2 --- /dev/null +++ b/FrontEnd/src/components/dialog/index.tsx @@ -0,0 +1,12 @@ +export { default as Dialog } from "./Dialog"; +export { default as FullPageDialog } from "./FullPageDialog"; +export { default as OperationDialog } from "./OperationDialog"; +export { default as ConfirmDialog } from "./ConfirmDialog"; +export { default as DialogContainer } from "./DialogContainer"; + +export { + useDialog, + useDialogController, + useCloseDialog, + DialogProvider, +} from "./DialogProvider"; diff --git a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx index 2fcfef2c..96ae971b 100644 --- a/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeAvatarDialog.tsx @@ -9,23 +9,21 @@ import { getHttpUserClient } from "~src/http/user"; import { ImageCropper, useImageCrop } from "~src/components/ImageCropper"; import BlobImage from "~src/components/BlobImage"; import { ButtonRowV2 } from "~src/components/button"; -import { Dialog, DialogContainer } from "~src/components/dialog"; +import { + Dialog, + DialogContainer, + useDialogController, +} from "~src/components/dialog"; import "./ChangeAvatarDialog.css"; -interface ChangeAvatarDialogProps { - open: boolean; - onClose: () => void; -} - -export default function ChangeAvatarDialog({ - open, - onClose, -}: ChangeAvatarDialogProps) { +export default function ChangeAvatarDialog() { const c = useC(); const user = useUser(); + const controller = useDialogController(); + type State = | "select" | "crop" @@ -49,11 +47,7 @@ export default function ChangeAvatarDialog({ "settings.dialogChangeAvatar.prompt.select", ); - const close = (): void => { - if (state !== "uploading") { - onClose(); - } - }; + const close = controller.closeDialog; const onSelectFile = (e: ChangeEvent<HTMLInputElement>): void => { const files = e.target.files; @@ -96,6 +90,7 @@ export default function ChangeAvatarDialog({ } setState("uploading"); + controller.setCanSwitchDialog(false); getHttpUserClient() .putAvatar(user.username, resultBlob) .then( @@ -106,7 +101,10 @@ export default function ChangeAvatarDialog({ setState("error"); setMessage("operationDialog.error"); }, - ); + ) + .finally(() => { + controller.setCanSwitchDialog(true); + }); }; const cancelButton = { @@ -181,7 +179,7 @@ export default function ChangeAvatarDialog({ }; return ( - <Dialog open={open} onClose={close}> + <Dialog> <DialogContainer title="settings.dialogChangeAvatar.title" titleColor="primary" diff --git a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx index 843659ef..bd1eaa51 100644 --- a/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangeNicknameDialog.tsx @@ -3,19 +3,11 @@ import { useUserLoggedIn } from "~src/services/user"; import OperationDialog from "~src/components/dialog/OperationDialog"; -export interface ChangeNicknameDialogProps { - open: boolean; - onClose: () => void; -} - -export default function ChangeNicknameDialog(props: ChangeNicknameDialogProps) { - const { open, onClose } = props; - +export default function ChangeNicknameDialog() { const user = useUserLoggedIn(); return ( <OperationDialog - open={open} title="settings.dialogChangeNickname.title" inputs={[ { @@ -29,7 +21,6 @@ export default function ChangeNicknameDialog(props: ChangeNicknameDialogProps) { nickname: newNickname, }); }} - onClose={onClose} /> ); } diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx index 946b9fbe..c3111ac8 100644 --- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx +++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx @@ -5,22 +5,13 @@ import { userService } from "~src/services/user"; import { OperationDialog } from "~src/components/dialog"; -interface ChangePasswordDialogProps { - open: boolean; - onClose: () => void; -} - -export function ChangePasswordDialog(props: ChangePasswordDialogProps) { - const { open, onClose } = props; - +export function ChangePasswordDialog() { const navigate = useNavigate(); const [redirect, setRedirect] = useState<boolean>(false); return ( <OperationDialog - open={open} - onClose={onClose} title="settings.dialogChangePassword.title" color="danger" inputPrompt="settings.dialogChangePassword.prompt" diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx index 918a77b5..4d2c28c7 100644 --- a/FrontEnd/src/pages/setting/index.tsx +++ b/FrontEnd/src/pages/setting/index.tsx @@ -14,7 +14,11 @@ import { pushAlert } from "~src/services/alert"; import { useC, Text } from "~src/common"; -import { useDialog, ConfirmDialog } from "~src/components/dialog"; +import { + useDialog, + DialogProvider, + ConfirmDialog, +} from "~src/components/dialog"; import Card from "~src/components/Card"; import Spinner from "~src/components/Spinner"; import Page from "~src/components/Page"; @@ -140,7 +144,22 @@ function RegisterCodeSettingItem() { // undefined: loading const [registerCode, setRegisterCode] = useState<undefined | null | string>(); - const [dialogOpen, setDialogOpen] = useState(false); + const { controller, createDialogSwitch } = useDialog({ + confirm: ( + <ConfirmDialog + title="settings.renewRegisterCode" + body="settings.renewRegisterCodeDesc" + onConfirm={() => { + if (user == null) throw new Error(); + void getHttpUserClient() + .renewRegisterCode(user.username) + .then(() => { + setRegisterCode(undefined); + }); + }} + /> + ), + }); useEffect(() => { setRegisterCode(undefined); @@ -157,49 +176,34 @@ function RegisterCodeSettingItem() { }, [user, registerCode]); return ( - <> - <SettingItemContainer - title="settings.myRegisterCode" - description="settings.myRegisterCodeDesc" - className="register-code-setting-item" - onClick={() => setDialogOpen(true)} - > - {registerCode === undefined ? ( - <Spinner /> - ) : registerCode === null ? ( - <span>Noop</span> - ) : ( - <code - className="register-code" - onClick={(event) => { - void navigator.clipboard.writeText(registerCode).then(() => { - pushAlert({ - type: "create", - message: "settings.myRegisterCodeCopied", - }); + <SettingItemContainer + title="settings.myRegisterCode" + description="settings.myRegisterCodeDesc" + className="register-code-setting-item" + onClick={createDialogSwitch("confirm")} + > + {registerCode === undefined ? ( + <Spinner /> + ) : registerCode === null ? ( + <span>Noop</span> + ) : ( + <code + className="register-code" + onClick={(event) => { + void navigator.clipboard.writeText(registerCode).then(() => { + pushAlert({ + type: "create", + message: "settings.myRegisterCodeCopied", }); - event.stopPropagation(); - }} - > - {registerCode} - </code> - )} - </SettingItemContainer> - <ConfirmDialog - title="settings.renewRegisterCode" - body="settings.renewRegisterCodeDesc" - onClose={() => setDialogOpen(false)} - open={dialogOpen} - onConfirm={() => { - if (user == null) throw new Error(); - void getHttpUserClient() - .renewRegisterCode(user.username) - .then(() => { - setRegisterCode(undefined); }); - }} - />{" "} - </> + event.stopPropagation(); + }} + > + {registerCode} + </code> + )} + <DialogProvider controller={controller} /> + </SettingItemContainer> ); } @@ -240,12 +244,22 @@ export default function SettingPage() { const user = useUser(); const navigate = useNavigate(); - const { dialogPropsMap, createDialogSwitch } = useDialog([ - "change-password", - "change-avatar", - "change-nickname", - "logout", - ]); + const { controller, createDialogSwitch } = useDialog({ + "change-password": <ChangeNicknameDialog />, + "change-avatar": <ChangeAvatarDialog />, + "change-nickname": <ChangePasswordDialog />, + logout: ( + <ConfirmDialog + title="settings.dialogConfirmLogout.title" + body="settings.dialogConfirmLogout.prompt" + onConfirm={() => { + void userService.logout().then(() => { + navigate("/"); + }); + }} + /> + ), + }); return ( <Page noTopPadding> @@ -275,23 +289,7 @@ export default function SettingPage() { <SettingSection title="settings.subheader.customization"> <LanguageChangeSettingItem /> </SettingSection> - <ChangePasswordDialog {...dialogPropsMap["change-password"]} /> - {user && ( - <> - <ConfirmDialog - title="settings.dialogConfirmLogout.title" - body="settings.dialogConfirmLogout.prompt" - onConfirm={() => { - void userService.logout().then(() => { - navigate("/"); - }); - }} - {...dialogPropsMap["logout"]} - /> - <ChangeAvatarDialog {...dialogPropsMap["change-avatar"]} /> - <ChangeNicknameDialog {...dialogPropsMap["change-nickname"]} /> - </> - )} + <DialogProvider controller={controller} /> </Page> ); } diff --git a/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx b/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx index 43e81d67..fc7b882f 100644 --- a/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx +++ b/FrontEnd/src/pages/timeline/MarkdownPostEdit.tsx @@ -2,7 +2,10 @@ import * as React from "react"; import classnames from "classnames"; import { useTranslation } from "react-i18next"; -import { getHttpTimelineClient, HttpTimelinePostInfo } from "~src/http/timeline"; +import { + getHttpTimelineClient, + HttpTimelinePostInfo, +} from "~src/http/timeline"; import TimelinePostBuilder from "~src/services/TimelinePostBuilder"; @@ -13,6 +16,7 @@ import Spinner from "~src/components/Spinner"; import IconButton from "~src/components/button/IconButton"; import "./MarkdownPostEdit.css"; +import { DialogProvider, useDialog } from "~src/components/dialog"; export interface MarkdownPostEditProps { owner: string; @@ -39,12 +43,19 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({ const [process, setProcess] = React.useState<boolean>(false); - const [showLeaveConfirmDialog, setShowLeaveConfirmDialog] = - React.useState<boolean>(false); + const { controller, switchDialog } = useDialog({ + "leave-confirm": ( + <ConfirmDialog + onConfirm={onClose} + title="timeline.dropDraft" + body="timeline.confirmLeave" + /> + ), + }); const [text, _setText] = React.useState<string>(""); const [images, _setImages] = React.useState<{ file: File; url: string }[]>( - [] + [], ); const [previewHtml, _setPreviewHtml] = React.useState<string>(""); @@ -92,7 +103,7 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({ timelineName, { dataList, - } + }, ); onPosted(post); onClose(); @@ -123,7 +134,7 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({ if (canLeave) { onClose(); } else { - setShowLeaveConfirmDialog(true); + switchDialog("leave-confirm"); } }} /> @@ -167,7 +178,7 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({ color="danger" className={classnames( "timeline-markdown-post-edit-image-delete-button", - process && "d-none" + process && "d-none", )} onClick={() => { getBuilder().deleteImage(index); @@ -201,13 +212,7 @@ const MarkdownPostEdit: React.FC<MarkdownPostEditProps> = ({ }, ]} /> - <ConfirmDialog - onClose={() => setShowLeaveConfirmDialog(false)} - onConfirm={onClose} - open={showLeaveConfirmDialog} - title="timeline.dropDraft" - body="timeline.confirmLeave" - /> + <DialogProvider controller={controller} /> </> ); }; diff --git a/FrontEnd/src/pages/timeline/TimelineCard.tsx b/FrontEnd/src/pages/timeline/TimelineCard.tsx index f17a3ce9..133f1ef4 100644 --- a/FrontEnd/src/pages/timeline/TimelineCard.tsx +++ b/FrontEnd/src/pages/timeline/TimelineCard.tsx @@ -8,7 +8,7 @@ import { HttpTimelineInfo } from "~src/http/timeline"; import { getHttpBookmarkClient } from "~src/http/bookmark"; import { useMobile } from "~src/components/hooks"; -import { Dialog, useDialog } from "~src/components/dialog"; +import { Dialog, DialogProvider, useDialog } from "~src/components/dialog"; import UserAvatar from "~src/components/user/UserAvatar"; import PopupMenu from "~src/components/menu/PopupMenu"; import FullPageDialog from "~src/components/dialog/FullPageDialog"; @@ -40,11 +40,17 @@ export default function TimelineCard(props: TimelinePageCardProps) { const isMobile = useMobile(); - const { createDialogSwitch, dialogPropsMap } = useDialog([ - "member", - "property", - "delete", - ]); + const { controller, createDialogSwitch } = useDialog({ + member: ( + <Dialog> + <TimelineMember timeline={timeline} onChange={onReload} /> + </Dialog> + ), + property: ( + <TimelinePropertyChangeDialog timeline={timeline} onChange={onReload} /> + ), + delete: <TimelineDeleteDialog timeline={timeline} />, + }); const content = ( <div> @@ -144,15 +150,7 @@ export default function TimelineCard(props: TimelinePageCardProps) { ) : ( <div style={{ display: collapse ? "none" : "block" }}>{content}</div> )} - <Dialog {...dialogPropsMap["member"]}> - <TimelineMember timeline={timeline} onChange={onReload} /> - </Dialog> - <TimelinePropertyChangeDialog - timeline={timeline} - onChange={onReload} - {...dialogPropsMap["property"]} - /> - <TimelineDeleteDialog timeline={timeline} {...dialogPropsMap["delete"]} /> + <DialogProvider controller={controller} /> </Card> ); } diff --git a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx index a7209e75..630ce4ca 100644 --- a/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx +++ b/FrontEnd/src/pages/timeline/TimelineDeleteDialog.tsx @@ -8,8 +8,6 @@ import OperationDialog from "~src/components/dialog/OperationDialog"; interface TimelineDeleteDialog { timeline: HttpTimelineInfo; - open: boolean; - onClose: () => void; } const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => { @@ -19,8 +17,6 @@ const TimelineDeleteDialog: React.FC<TimelineDeleteDialog> = (props) => { return ( <OperationDialog - open={props.open} - onClose={props.onClose} title="timeline.deleteDialog.title" color="danger" inputPromptNode={ diff --git a/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx b/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx index 3c41228a..3bc4dab3 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx @@ -88,7 +88,7 @@ function TimelinePostEditImage(props: TimelinePostEditImageProps) { /> {file != null && !error && ( <BlobImage - blob={file} + src={file} className="timeline-post-create-image" onLoad={() => onSelect(file)} onError={() => { diff --git a/FrontEnd/src/pages/timeline/TimelinePostView.tsx b/FrontEnd/src/pages/timeline/TimelinePostView.tsx index 6b87ef2a..5de09b28 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostView.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostView.tsx @@ -9,7 +9,7 @@ import { pushAlert } from "~src/services/alert"; import { useClickOutside } from "~src/components/hooks"; import UserAvatar from "~src/components/user/UserAvatar"; -import { useDialog } from "~src/components/dialog"; +import { DialogProvider, useDialog } from "~src/components/dialog"; import FlatButton from "~src/components/button/FlatButton"; import ConfirmDialog from "~src/components/dialog/ConfirmDialog"; import TimelinePostContentView from "./TimelinePostContentView"; @@ -33,13 +33,33 @@ export default function TimelinePostView(props: TimelinePostViewProps) { const [operationMaskVisible, setOperationMaskVisible] = useState<boolean>(false); - const { switchDialog, dialogPropsMap } = useDialog(["delete"], { - onClose: { - delete: () => { - setOperationMaskVisible(false); + const { controller, switchDialog } = useDialog( + { + delete: ( + <ConfirmDialog + title="timeline.post.deleteDialog.title" + body="timeline.post.deleteDialog.prompt" + onConfirm={() => { + void getHttpTimelineClient() + .deletePost(post.timelineOwnerV2, post.timelineNameV2, post.id) + .then(onDeleted, () => { + pushAlert({ + type: "danger", + message: "timeline.deletePostFailed", + }); + }); + }} + /> + ), + }, + { + onClose: { + delete: () => { + setOperationMaskVisible(false); + }, }, }, - }); + ); const [maskElement, setMaskElement] = useState<HTMLElement | null>(null); useClickOutside(maskElement, () => setOperationMaskVisible(false)); @@ -98,21 +118,7 @@ export default function TimelinePostView(props: TimelinePostViewProps) { </div> ) : null} </TimelinePostCard> - <ConfirmDialog - title="timeline.post.deleteDialog.title" - body="timeline.post.deleteDialog.prompt" - onConfirm={() => { - void getHttpTimelineClient() - .deletePost(post.timelineOwnerV2, post.timelineNameV2, post.id) - .then(onDeleted, () => { - pushAlert({ - type: "danger", - message: "timeline.deletePostFailed", - }); - }); - }} - {...dialogPropsMap.delete} - /> + <DialogProvider controller={controller} /> </TimelinePostContainer> ); } diff --git a/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx index afd83a5f..ee5388cb 100644 --- a/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePropertyChangeDialog.tsx @@ -11,8 +11,6 @@ import { import OperationDialog from "~src/components/dialog/OperationDialog"; export interface TimelinePropertyChangeDialogProps { - open: boolean; - onClose: () => void; timeline: HttpTimelineInfo; onChange: () => void; } @@ -63,8 +61,6 @@ const TimelinePropertyChangeDialog: React.FC< }, }, }} - open={props.open} - onClose={props.onClose} onProcess={({ title, visibility, description }) => { const req: HttpTimelinePatchRequest = {}; if (title !== timeline.title) { |