aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src')
-rw-r--r--FrontEnd/src/app/index.sass22
-rw-r--r--FrontEnd/src/app/locales/en/translation.json4
-rw-r--r--FrontEnd/src/app/locales/zh/translation.json4
-rw-r--r--FrontEnd/src/app/services/TimelinePostBuilder.ts42
-rw-r--r--FrontEnd/src/app/views/common/ConfirmDialog.tsx40
-rw-r--r--FrontEnd/src/app/views/common/FlatButton.tsx36
-rw-r--r--FrontEnd/src/app/views/common/TabPages.tsx74
-rw-r--r--FrontEnd/src/app/views/common/common.sass4
-rw-r--r--FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx187
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx2
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx167
-rw-r--r--FrontEnd/src/app/views/timeline-common/timeline-common.sass8
12 files changed, 474 insertions, 116 deletions
diff --git a/FrontEnd/src/app/index.sass b/FrontEnd/src/app/index.sass
index 39dce693..2079cad8 100644
--- a/FrontEnd/src/app/index.sass
+++ b/FrontEnd/src/app/index.sass
@@ -45,8 +45,15 @@ small
cursor: pointer
padding: 0.2em 0.5em
border-radius: 0.2em
- &:hover
+ &:hover:not(.disabled)
background-color: $gray-200
+ &.disabled
+ cursor: default
+ @each $color, $value in $theme-colors
+ &.#{$color}
+ color: $value
+ &.disabled
+ color: adjust-color($value, $lightness: +15%)
.cursor-pointer
cursor: pointer
@@ -81,10 +88,10 @@ textarea
.text-yellow
color: $yellow
-@each $color, $value in $theme-colors
- .text-button
- background: transparent
- border: none
+.text-button
+ background: transparent
+ border: none
+ @each $color, $value in $theme-colors
&.#{$color}
color: $value
&:hover
@@ -95,3 +102,8 @@ textarea
i
line-height: 1
+
+.markdown-container
+ img
+ max-height: 200px
+ max-width: 100%
diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json
index 4002ee4f..65ddbe0c 100644
--- a/FrontEnd/src/app/locales/en/translation.json
+++ b/FrontEnd/src/app/locales/en/translation.json
@@ -2,7 +2,9 @@
"welcome": "Welcome!",
"search": "Search",
"edit": "Edit",
+ "image": "Image",
"done": "Done",
+ "preview": "Preview",
"loadFailReload": "Load failed, <1>click here to reload</1>.",
"error": {
"network": "Network error.",
@@ -65,6 +67,8 @@
"send": "Send",
"deletePostFailed": "Failed to delete post.",
"sendPostFailed": "Failed to send post.",
+ "dropDraft": "Drop Draft",
+ "confirmLeave": "Are you sure to leave? All content you typed would be lost.",
"visibility": {
"public": "public to everyone",
"register": "only registed people can see",
diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json
index 3f966d7c..f6971241 100644
--- a/FrontEnd/src/app/locales/zh/translation.json
+++ b/FrontEnd/src/app/locales/zh/translation.json
@@ -2,7 +2,9 @@
"welcome": "欢迎!",
"search": "搜索",
"edit": "编辑",
+ "image": "图片",
"done": "完成",
+ "preview": "预览",
"loadFailReload": "加载失败,<1>点击重试</1>。",
"error": {
"network": "网络错误。",
@@ -65,6 +67,8 @@
"send": "发送",
"deletePostFailed": "删除消息失败。",
"sendPostFailed": "发送消息失败。",
+ "dropDraft": "放弃草稿",
+ "confirmLeave": "确定要离开吗?所有输入的内容将会丢失。",
"visibility": {
"public": "对所有人公开",
"register": "仅注册可见",
diff --git a/FrontEnd/src/app/services/TimelinePostBuilder.ts b/FrontEnd/src/app/services/TimelinePostBuilder.ts
index 8594fa49..40279eca 100644
--- a/FrontEnd/src/app/services/TimelinePostBuilder.ts
+++ b/FrontEnd/src/app/services/TimelinePostBuilder.ts
@@ -1,8 +1,3 @@
-import {
- escapeHtml,
- replaceEntities,
- unescapeMd,
-} from "remarkable/lib/common/utils";
import { Remarkable } from "remarkable";
import { UiLogicError } from "@/common";
@@ -10,7 +5,7 @@ import { UiLogicError } from "@/common";
import { base64 } from "@/http/common";
import { HttpTimelinePostPostRequest } from "@/http/timeline";
-export class TimelinePostBuilder {
+export default class TimelinePostBuilder {
private _onChange: () => void;
private _text = "";
private _images: { file: File; url: string }[] = [];
@@ -18,28 +13,16 @@ export class TimelinePostBuilder {
constructor(onChange: () => void) {
this._onChange = onChange;
+ const oldImageRenderer = this._md.renderer.rules.image;
this._md.renderer.rules.image = ((
_t: TimelinePostBuilder
): Remarkable.Rule<Remarkable.ImageToken, string> =>
function (tokens, idx, options /*, env */) {
const i = parseInt(tokens[idx].src);
- const src =
- ' src="' +
- (isNaN(i) && i > 0 && i <= _t._images.length
- ? escapeHtml(tokens[idx].src)
- : _t._images[i - 1].url) +
- '"';
- const title = tokens[idx].title
- ? ' title="' + escapeHtml(replaceEntities(tokens[idx].title)) + '"'
- : "";
- const alt =
- ' alt="' +
- (tokens[idx].alt
- ? escapeHtml(replaceEntities(unescapeMd(tokens[idx].alt)))
- : "") +
- '"';
- const suffix = options?.xhtmlOut ? " /" : "";
- return "<img" + src + alt + title + suffix + ">";
+ if (!isNaN(i) && i > 0 && i <= _t._images.length) {
+ tokens[idx].src = _t._images[i - 1].url;
+ }
+ return oldImageRenderer(tokens, idx, options);
})(this);
}
@@ -49,6 +32,7 @@ export class TimelinePostBuilder {
}
appendImage(file: File): void {
+ this._images = this._images.slice();
this._images.push({
file,
url: URL.createObjectURL(file),
@@ -69,6 +53,8 @@ export class TimelinePostBuilder {
newIndex = this._images.length - 1;
}
+ this._images = this._images.slice();
+
const [old] = this._images.splice(oldIndex, 1);
this._images.splice(newIndex, 0, old);
@@ -80,16 +66,26 @@ export class TimelinePostBuilder {
throw new UiLogicError("Old index out of range.");
}
+ this._images = this._images.slice();
+
URL.revokeObjectURL(this._images[index].url);
this._images.splice(index, 1);
this._onChange();
}
+ get text(): string {
+ return this._text;
+ }
+
get images(): { file: File; url: string }[] {
return this._images;
}
+ get isEmpty(): boolean {
+ return this._text.length === 0 && this._images.length === 0;
+ }
+
renderHtml(): string {
return this._md.render(this._text);
}
diff --git a/FrontEnd/src/app/views/common/ConfirmDialog.tsx b/FrontEnd/src/app/views/common/ConfirmDialog.tsx
new file mode 100644
index 00000000..72940c51
--- /dev/null
+++ b/FrontEnd/src/app/views/common/ConfirmDialog.tsx
@@ -0,0 +1,40 @@
+import { convertI18nText, I18nText } from "@/common";
+import React from "react";
+import { Modal, Button } from "react-bootstrap";
+import { useTranslation } from "react-i18next";
+
+const ConfirmDialog: React.FC<{
+ onClose: () => void;
+ onConfirm: () => void;
+ title: I18nText;
+ body: I18nText;
+}> = ({ onClose, onConfirm, title, body }) => {
+ const { t } = useTranslation();
+
+ return (
+ <Modal onHide={onClose} show centered>
+ <Modal.Header>
+ <Modal.Title className="text-danger">
+ {convertI18nText(title, t)}
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>{convertI18nText(body, t)}</Modal.Body>
+ <Modal.Footer>
+ <Button variant="secondary" onClick={onClose}>
+ {t("operationDialog.cancel")}
+ </Button>
+ <Button
+ variant="danger"
+ onClick={() => {
+ onConfirm();
+ onClose();
+ }}
+ >
+ {t("operationDialog.confirm")}
+ </Button>
+ </Modal.Footer>
+ </Modal>
+ );
+};
+
+export default ConfirmDialog;
diff --git a/FrontEnd/src/app/views/common/FlatButton.tsx b/FrontEnd/src/app/views/common/FlatButton.tsx
new file mode 100644
index 00000000..80bb654c
--- /dev/null
+++ b/FrontEnd/src/app/views/common/FlatButton.tsx
@@ -0,0 +1,36 @@
+import React from "react";
+import clsx from "clsx";
+
+import { BootstrapThemeColor } from "@/common";
+
+export interface FlatButtonProps {
+ variant?: BootstrapThemeColor | string;
+ disabled?: boolean;
+ className?: string;
+ style?: React.CSSProperties;
+ onClick?: () => void;
+}
+
+const FlatButton: React.FC<FlatButtonProps> = (props) => {
+ const { disabled, className, style } = props;
+ const variant = props.variant ?? "primary";
+
+ const onClick = disabled ? undefined : props.onClick;
+
+ return (
+ <div
+ className={clsx(
+ "flat-button",
+ variant,
+ disabled ? "disabled" : null,
+ className
+ )}
+ style={style}
+ onClick={onClick}
+ >
+ {props.children}
+ </div>
+ );
+};
+
+export default FlatButton;
diff --git a/FrontEnd/src/app/views/common/TabPages.tsx b/FrontEnd/src/app/views/common/TabPages.tsx
new file mode 100644
index 00000000..424e769f
--- /dev/null
+++ b/FrontEnd/src/app/views/common/TabPages.tsx
@@ -0,0 +1,74 @@
+import React from "react";
+import { Nav } from "react-bootstrap";
+import { useTranslation } from "react-i18next";
+
+import { convertI18nText, I18nText, UiLogicError } from "@/common";
+
+export interface TabPage {
+ id: string;
+ tabText: I18nText;
+ page: React.ReactNode;
+}
+
+export interface TabPagesProps {
+ pages: TabPage[];
+ actions?: React.ReactNode;
+ className?: string;
+ style?: React.CSSProperties;
+ navClassName?: string;
+ navStyle?: React.CSSProperties;
+ pageContainerClassName?: string;
+ pageContainerStyle?: React.CSSProperties;
+}
+
+const TabPages: React.FC<TabPagesProps> = ({
+ pages,
+ actions,
+ className,
+ style,
+ navClassName,
+ navStyle,
+ pageContainerClassName,
+ pageContainerStyle,
+}) => {
+ if (pages.length === 0) {
+ throw new UiLogicError("Page list can't be empty.");
+ }
+
+ const { t } = useTranslation();
+
+ const [tab, setTab] = React.useState<string>(pages[0].id);
+
+ const currentPage = pages.find((p) => p.id === tab);
+
+ if (currentPage == null) {
+ throw new UiLogicError("Current tab value is bad.");
+ }
+
+ return (
+ <div className={className} style={style}>
+ <Nav variant="tabs" className={navClassName} style={navStyle}>
+ {pages.map((page) => (
+ <Nav.Item key={page.id}>
+ <Nav.Link
+ active={tab === page.id}
+ onClick={() => {
+ setTab(page.id);
+ }}
+ >
+ {convertI18nText(page.tabText, t)}
+ </Nav.Link>
+ </Nav.Item>
+ ))}
+ {actions != null && (
+ <div className="ml-auto cru-tab-pages-action-area">{actions}</div>
+ )}
+ </Nav>
+ <div className={pageContainerClassName} style={pageContainerStyle}>
+ {currentPage.page}
+ </div>
+ </div>
+ );
+};
+
+export default TabPages;
diff --git a/FrontEnd/src/app/views/common/common.sass b/FrontEnd/src/app/views/common/common.sass
index 0a30d995..f3022d19 100644
--- a/FrontEnd/src/app/views/common/common.sass
+++ b/FrontEnd/src/app/views/common/common.sass
@@ -92,3 +92,7 @@
.cru-menu-divider
border-top: 1px solid $gray-200
+
+.cru-tab-pages-action-area
+ display: flex
+ align-items: center
diff --git a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx
new file mode 100644
index 00000000..f4351db0
--- /dev/null
+++ b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx
@@ -0,0 +1,187 @@
+import React from "react";
+import { Form } 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<MarkdownPostEditProps> = ({
+ timeline: timelineName,
+ onPosted,
+ onClose,
+ onPostError,
+ className,
+ style,
+}) => {
+ const { t } = useTranslation();
+
+ const [canLeave, setCanLeave] = React.useState<boolean>(true);
+
+ const [process, setProcess] = React.useState<boolean>(false);
+
+ const [
+ showLeaveConfirmDialog,
+ setShowLeaveConfirmDialog,
+ ] = React.useState<boolean>(false);
+
+ const [text, _setText] = React.useState<string>("");
+ const [images, _setImages] = React.useState<{ file: File; url: string }[]>(
+ []
+ );
+ const [previewHtml, _setPreviewHtml] = React.useState<string>("");
+
+ const _builder = React.useRef<TimelinePostBuilder | null>(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;
+ };
+
+ React.useEffect(() => {
+ return () => {
+ getBuilder().dispose();
+ };
+ }, []);
+
+ React.useEffect(() => {
+ window.onbeforeunload = () => {
+ if (!canLeave) {
+ return t("timeline.confirmLeave");
+ }
+ };
+
+ return () => {
+ window.onbeforeunload = null;
+ };
+ }, [canLeave, t]);
+
+ const send = async (): Promise<void> => {
+ setProcess(true);
+ try {
+ const dataList = await getBuilder().build();
+ const post = await getHttpTimelineClient().postPost(timelineName, {
+ dataList,
+ });
+ onPosted(post);
+ onClose();
+ } catch (e) {
+ setProcess(false);
+ onPostError();
+ }
+ };
+
+ return (
+ <>
+ <Prompt when={!canLeave} message={t("timeline.confirmLeave")} />
+ <TabPages
+ className={className}
+ style={style}
+ pageContainerClassName="py-2"
+ actions={
+ <>
+ <FlatButton
+ className="mr-2"
+ variant="danger"
+ onClick={() => {
+ if (canLeave) {
+ onClose();
+ } else {
+ setShowLeaveConfirmDialog(true);
+ }
+ }}
+ >
+ {t("operationDialog.cancel")}
+ </FlatButton>
+ <FlatButton onClick={send} disabled={canLeave}>
+ {t("timeline.send")}
+ </FlatButton>
+ </>
+ }
+ pages={[
+ {
+ id: "text",
+ tabText: "edit",
+ page: (
+ <Form.Control
+ as="textarea"
+ value={text}
+ disabled={process}
+ onChange={(event) => {
+ getBuilder().setMarkdownText(event.currentTarget.value);
+ }}
+ />
+ ),
+ },
+ {
+ id: "images",
+ tabText: "image",
+ page: (
+ <div className="timeline-markdown-post-edit-page">
+ {images.map((image) => (
+ <img
+ key={image.url}
+ src={image.url}
+ className="timeline-markdown-post-edit-image"
+ />
+ ))}
+ <Form.File
+ label={t("chooseImage")}
+ accept="image/*"
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
+ const { files } = event.currentTarget;
+ if (files != null && files.length !== 0) {
+ getBuilder().appendImage(files[0]);
+ }
+ }}
+ disabled={process}
+ />
+ </div>
+ ),
+ },
+ {
+ id: "preview",
+ tabText: "preview",
+ page: (
+ <div
+ className="markdown-container timeline-markdown-post-edit-page"
+ dangerouslySetInnerHTML={{ __html: previewHtml }}
+ />
+ ),
+ },
+ ]}
+ />
+ {showLeaveConfirmDialog && (
+ <ConfirmDialog
+ onClose={() => setShowLeaveConfirmDialog(false)}
+ onConfirm={onClose}
+ title="timeline.dropDraft"
+ body="timeline.confirmLeave"
+ />
+ )}
+ </>
+ );
+};
+
+export default MarkdownPostEdit;
diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx
index d836d1db..58fae4c7 100644
--- a/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx
+++ b/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx
@@ -152,7 +152,7 @@ const MarkdownView: React.FC<TimelinePostContentViewProps> = (props) => {
}
return (
<div
- className={className}
+ className={clsx(className, "markdown-container")}
style={style}
dangerouslySetInnerHTML={{
__html: markdownHtml,
diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx
index 025b41c0..a474d2f6 100644
--- a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx
+++ b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx
@@ -18,6 +18,7 @@ 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;
@@ -121,26 +122,21 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
const [process, setProcess] = React.useState<boolean>(false);
- const [kind, setKind] = React.useState<PostKind>("text");
+ const [kind, setKind] = React.useState<Exclude<PostKind, "markdown">>("text");
+ const [showMarkdown, setShowMarkdown] = React.useState<boolean>(false);
const [text, setText] = React.useState<string>("");
- const [markdown, setMarkdown] = React.useState<string>("");
const [image, setImage] = React.useState<File | null>(null);
const draftTextLocalStorageKey = `timeline.${timeline.name}.postDraft.text`;
- const draftMarkdownLocalStorageKey = `timeline.${timeline.name}.postDraft.markdown`;
React.useEffect(() => {
setText(window.localStorage.getItem(draftTextLocalStorageKey) ?? "");
- setMarkdown(
- window.localStorage.getItem(draftMarkdownLocalStorageKey) ?? ""
- );
- }, [draftTextLocalStorageKey, draftMarkdownLocalStorageKey]);
+ }, [draftTextLocalStorageKey]);
const canSend =
(kind === "text" && text.length !== 0) ||
- (kind === "image" && image != null) ||
- (kind === "markdown" && markdown.length !== 0);
+ (kind === "image" && image != null);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const containerRef = React.useRef<HTMLDivElement>(null!);
@@ -160,6 +156,13 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
};
});
+ const onPostError = (): void => {
+ pushAlert({
+ type: "danger",
+ message: "timeline.sendPostFailed",
+ });
+ };
+
const onSend = async (): Promise<void> => {
setProcess(true);
@@ -171,12 +174,6 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
data: await base64(text),
};
break;
- case "markdown":
- requestData = {
- contentType: "text/markdown",
- data: await base64(markdown),
- };
- break;
case "image":
if (image == null) {
throw new UiLogicError(
@@ -201,20 +198,14 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
if (kind === "text") {
setText("");
window.localStorage.removeItem(draftTextLocalStorageKey);
- } else if (kind === "markdown") {
- setMarkdown("");
- window.localStorage.removeItem(draftMarkdownLocalStorageKey);
}
setProcess(false);
setKind("text");
onPosted(data);
},
(_) => {
- pushAlert({
- type: "danger",
- message: "timeline.sendPostFailed",
- });
setProcess(false);
+ onPostError();
}
);
};
@@ -224,73 +215,75 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => {
ref={containerRef}
className={clsx("container-fluid bg-light", className)}
>
- <Row>
- <Col className="px-1 py-1">
- {(() => {
- if (kind === "text") {
- return (
- <TimelinePostEditText
- className="w-100 h-100 timeline-post-edit"
- text={text}
- disabled={process}
- onChange={(t) => {
- setText(t);
- window.localStorage.setItem(draftTextLocalStorageKey, t);
- }}
- />
- );
- } else if (kind === "image") {
- return (
- <TimelinePostEditImage onSelect={setImage} disabled={process} />
- );
- } else if (kind === "markdown") {
- return (
- <TimelinePostEditText
- className="w-100 h-100 timeline-post-edit"
- text={markdown}
- disabled={process}
- onChange={(t) => {
- setMarkdown(t);
- window.localStorage.setItem(
- draftMarkdownLocalStorageKey,
- t
- );
- }}
+ {showMarkdown ? (
+ <MarkdownPostEdit
+ className="w-100"
+ onClose={() => setShowMarkdown(false)}
+ timeline={timeline.name}
+ onPosted={onPosted}
+ onPostError={onPostError}
+ />
+ ) : (
+ <Row>
+ <Col className="px-1 py-1">
+ {(() => {
+ if (kind === "text") {
+ return (
+ <TimelinePostEditText
+ className="w-100 h-100 timeline-post-edit"
+ text={text}
+ disabled={process}
+ onChange={(t) => {
+ setText(t);
+ window.localStorage.setItem(draftTextLocalStorageKey, t);
+ }}
+ />
+ );
+ } else if (kind === "image") {
+ return (
+ <TimelinePostEditImage
+ onSelect={setImage}
+ disabled={process}
+ />
+ );
+ }
+ })()}
+ </Col>
+ <Col xs="auto" className="align-self-end m-1">
+ <div className="d-block text-center mt-1 mb-2">
+ <PopupMenu
+ items={(["text", "image", "markdown"] as const).map((kind) => ({
+ type: "button",
+ text: `timeline.post.type.${kind}`,
+ iconClassName: postKindIconClassNameMap[kind],
+ onClick: () => {
+ if (kind === "markdown") {
+ setShowMarkdown(true);
+ } else {
+ setKind(kind);
+ }
+ },
+ }))}
+ >
+ <i
+ className={clsx(
+ postKindIconClassNameMap[kind],
+ "icon-button large"
+ )}
/>
- );
- }
- })()}
- </Col>
- <Col xs="auto" className="align-self-end m-1">
- <div className="d-block text-center mt-1 mb-2">
- <PopupMenu
- items={(["text", "image", "markdown"] as const).map((kind) => ({
- type: "button",
- text: `timeline.post.type.${kind}`,
- iconClassName: postKindIconClassNameMap[kind],
- onClick: () => {
- setKind(kind);
- },
- }))}
+ </PopupMenu>
+ </div>
+ <LoadingButton
+ variant="primary"
+ onClick={onSend}
+ disabled={!canSend}
+ loading={process}
>
- <i
- className={clsx(
- postKindIconClassNameMap[kind],
- "icon-button large"
- )}
- />
- </PopupMenu>
- </div>
- <LoadingButton
- variant="primary"
- onClick={onSend}
- disabled={!canSend}
- loading={process}
- >
- {t("timeline.send")}
- </LoadingButton>
- </Col>
- </Row>
+ {t("timeline.send")}
+ </LoadingButton>
+ </Col>
+ </Row>
+ )}
</div>
);
};
diff --git a/FrontEnd/src/app/views/timeline-common/timeline-common.sass b/FrontEnd/src/app/views/timeline-common/timeline-common.sass
index 43c4d0f7..04318674 100644
--- a/FrontEnd/src/app/views/timeline-common/timeline-common.sass
+++ b/FrontEnd/src/app/views/timeline-common/timeline-common.sass
@@ -170,3 +170,11 @@ $timeline-line-color-current: #36c2e6
top: 56px
right: 0
margin: 0.5em
+
+.timeline-markdown-post-edit-page
+ overflow: scroll
+ max-height: 300px
+
+.timeline-markdown-post-edit-image
+ max-width: 100%
+ max-height: 200px