mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-22 09:18:19 +00:00
Multiple image upload (#971)
* feat: Add multiple image upload * refactor: Slight cleanup * feat: Add progress bar for multi-image upload * fix: Fix progress bar * fix: Messed up fix last time * refactor: Use await where possible * Update translation logic * Did suggested PR changes * Updating translations * Fix i18 issue * Make prettier actually check src in hopes it will fix CI issue
This commit is contained in:
parent
a8d6df9688
commit
699c3ff4b1
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
@ -0,0 +1 @@
|
|||
src/shared/translations
|
|
@ -12,7 +12,7 @@
|
|||
"build:prod": "webpack --mode=production",
|
||||
"clean": "yarn run rimraf dist",
|
||||
"dev": "yarn start",
|
||||
"lint": "node generate_translations.js && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check 'src/**/*.tsx'",
|
||||
"lint": "node generate_translations.js && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check \"src/**/*.{ts,tsx}\"",
|
||||
"prepare": "husky install",
|
||||
"start": "yarn build:dev --watch"
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Icon } from "./icon";
|
|||
|
||||
interface EmojiPickerProps {
|
||||
onEmojiClick?(val: any): any;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface EmojiPickerState {
|
||||
|
@ -15,8 +16,9 @@ export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
|
|||
private emptyState: EmojiPickerState = {
|
||||
showPicker: false,
|
||||
};
|
||||
|
||||
state: EmojiPickerState;
|
||||
constructor(props: any, context: any) {
|
||||
constructor(props: EmojiPickerProps, context: any) {
|
||||
super(props, context);
|
||||
this.state = this.emptyState;
|
||||
this.handleEmojiClick = this.handleEmojiClick.bind(this);
|
||||
|
@ -28,6 +30,7 @@ export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
|
|||
className="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t("emoji")}
|
||||
aria-label={i18n.t("emoji")}
|
||||
disabled={this.props.disabled}
|
||||
onClick={linkEvent(this, this.togglePicker)}
|
||||
>
|
||||
<Icon icon="smile" classes="icon-inline" />
|
||||
|
|
|
@ -10,11 +10,12 @@ interface LanguageSelectProps {
|
|||
allLanguages: Language[];
|
||||
siteLanguages: number[];
|
||||
selectedLanguageIds?: number[];
|
||||
multiple: boolean;
|
||||
multiple?: boolean;
|
||||
onChange(val: number[]): any;
|
||||
showAll?: boolean;
|
||||
showSite?: boolean;
|
||||
iconVersion?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
||||
|
@ -55,19 +56,19 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
|||
)}
|
||||
<div className="form-group row">
|
||||
<label
|
||||
className={classNames("col-form-label", {
|
||||
"col-sm-3": this.props.multiple,
|
||||
"col-sm-2": !this.props.multiple,
|
||||
})}
|
||||
className={classNames(
|
||||
"col-form-label",
|
||||
`col-sm-${this.props.multiple ? 3 : 2}`
|
||||
)}
|
||||
htmlFor={this.id}
|
||||
>
|
||||
{i18n.t(this.props.multiple ? "language_plural" : "language")}
|
||||
</label>
|
||||
<div
|
||||
className={classNames("input-group", {
|
||||
"col-sm-9": this.props.multiple,
|
||||
"col-sm-10": !this.props.multiple,
|
||||
})}
|
||||
className={classNames(
|
||||
"input-group",
|
||||
`col-sm-${this.props.multiple ? 9 : 10}`
|
||||
)}
|
||||
>
|
||||
{this.selectBtn}
|
||||
{this.props.multiple && (
|
||||
|
@ -87,8 +88,8 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
|||
}
|
||||
|
||||
get selectBtn() {
|
||||
let selectedLangs = this.props.selectedLanguageIds;
|
||||
let filteredLangs = selectableLanguages(
|
||||
const selectedLangs = this.props.selectedLanguageIds;
|
||||
const filteredLangs = selectableLanguages(
|
||||
this.props.allLanguages,
|
||||
this.props.siteLanguages,
|
||||
this.props.showAll,
|
||||
|
@ -98,14 +99,17 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
|||
|
||||
return (
|
||||
<select
|
||||
className={classNames("lang-select-action", {
|
||||
"form-control custom-select": !this.props.iconVersion,
|
||||
"btn btn-sm text-muted": this.props.iconVersion,
|
||||
})}
|
||||
className={classNames(
|
||||
"lang-select-action",
|
||||
this.props.iconVersion
|
||||
? "btn btn-sm text-muted"
|
||||
: "form-control custom-select"
|
||||
)}
|
||||
id={this.id}
|
||||
onChange={linkEvent(this, this.handleLanguageChange)}
|
||||
aria-label="action"
|
||||
multiple={this.props.multiple}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
{filteredLangs.map(l => (
|
||||
<option
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
import autosize from "autosize";
|
||||
import { NoOptionI18nKeys } from "i18next";
|
||||
import { Component, linkEvent } from "inferno";
|
||||
import { Prompt } from "inferno-router";
|
||||
import { Language } from "lemmy-js-client";
|
||||
import { i18n } from "../../i18next";
|
||||
import { UserService } from "../../services";
|
||||
import {
|
||||
concurrentImageUpload,
|
||||
customEmojisLookup,
|
||||
isBrowser,
|
||||
markdownFieldCharacterLimit,
|
||||
markdownHelpUrl,
|
||||
maxUploadImages,
|
||||
mdToHtml,
|
||||
numToSI,
|
||||
pictrsDeleteToast,
|
||||
randomStr,
|
||||
relTags,
|
||||
|
@ -21,6 +25,7 @@ import {
|
|||
import { EmojiPicker } from "./emoji-picker";
|
||||
import { Icon, Spinner } from "./icon";
|
||||
import { LanguageSelect } from "./language-select";
|
||||
import ProgressBar from "./progress-bar";
|
||||
|
||||
interface MarkdownTextAreaProps {
|
||||
initialContent?: string;
|
||||
|
@ -41,12 +46,17 @@ interface MarkdownTextAreaProps {
|
|||
siteLanguages: number[]; // TODO same
|
||||
}
|
||||
|
||||
interface ImageUploadStatus {
|
||||
total: number;
|
||||
uploaded: number;
|
||||
}
|
||||
|
||||
interface MarkdownTextAreaState {
|
||||
content?: string;
|
||||
languageId?: number;
|
||||
previewMode: boolean;
|
||||
loading: boolean;
|
||||
imageLoading: boolean;
|
||||
imageUploadStatus?: ImageUploadStatus;
|
||||
}
|
||||
|
||||
export class MarkdownTextArea extends Component<
|
||||
|
@ -56,12 +66,12 @@ export class MarkdownTextArea extends Component<
|
|||
private id = `comment-textarea-${randomStr()}`;
|
||||
private formId = `comment-form-${randomStr()}`;
|
||||
private tribute: any;
|
||||
|
||||
state: MarkdownTextAreaState = {
|
||||
content: this.props.initialContent,
|
||||
languageId: this.props.initialLanguageId,
|
||||
previewMode: false,
|
||||
loading: false,
|
||||
imageLoading: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -110,8 +120,8 @@ export class MarkdownTextArea extends Component<
|
|||
this.props.onReplyCancel?.();
|
||||
}
|
||||
|
||||
let textarea: any = document.getElementById(this.id);
|
||||
let form: any = document.getElementById(this.formId);
|
||||
const textarea: any = document.getElementById(this.id);
|
||||
const form: any = document.getElementById(this.formId);
|
||||
form.reset();
|
||||
setTimeout(() => autosize.update(textarea), 10);
|
||||
}
|
||||
|
@ -139,7 +149,7 @@ export class MarkdownTextArea extends Component<
|
|||
onInput={linkEvent(this, this.handleContentChange)}
|
||||
onPaste={linkEvent(this, this.handleImageUploadPaste)}
|
||||
required
|
||||
disabled={this.props.disabled}
|
||||
disabled={this.isDisabled}
|
||||
rows={2}
|
||||
maxLength={this.props.maxLength ?? markdownFieldCharacterLimit}
|
||||
placeholder={this.props.placeholder}
|
||||
|
@ -150,6 +160,20 @@ export class MarkdownTextArea extends Component<
|
|||
dangerouslySetInnerHTML={mdToHtml(this.state.content)}
|
||||
/>
|
||||
)}
|
||||
{this.state.imageUploadStatus &&
|
||||
this.state.imageUploadStatus.total > 1 && (
|
||||
<ProgressBar
|
||||
className="mt-2"
|
||||
striped
|
||||
animated
|
||||
value={this.state.imageUploadStatus.uploaded}
|
||||
max={this.state.imageUploadStatus.total}
|
||||
text={i18n.t("pictures_uploded_progess", {
|
||||
uploaded: this.state.imageUploadStatus.uploaded,
|
||||
total: this.state.imageUploadStatus.total,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<label className="sr-only" htmlFor={this.id}>
|
||||
{i18n.t("body")}
|
||||
|
@ -161,7 +185,7 @@ export class MarkdownTextArea extends Component<
|
|||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-secondary mr-2"
|
||||
disabled={this.props.disabled || this.state.loading}
|
||||
disabled={this.isDisabled}
|
||||
>
|
||||
{this.state.loading ? (
|
||||
<Spinner />
|
||||
|
@ -200,36 +224,16 @@ export class MarkdownTextArea extends Component<
|
|||
languageId ? Array.of(languageId) : undefined
|
||||
}
|
||||
siteLanguages={this.props.siteLanguages}
|
||||
multiple={false}
|
||||
onChange={this.handleLanguageChange}
|
||||
disabled={this.isDisabled}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t("bold")}
|
||||
aria-label={i18n.t("bold")}
|
||||
onClick={linkEvent(this, this.handleInsertBold)}
|
||||
>
|
||||
<Icon icon="bold" classes="icon-inline" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t("italic")}
|
||||
aria-label={i18n.t("italic")}
|
||||
onClick={linkEvent(this, this.handleInsertItalic)}
|
||||
>
|
||||
<Icon icon="italic" classes="icon-inline" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t("link")}
|
||||
aria-label={i18n.t("link")}
|
||||
onClick={linkEvent(this, this.handleInsertLink)}
|
||||
>
|
||||
<Icon icon="link" classes="icon-inline" />
|
||||
</button>
|
||||
{this.getFormatButton("bold", this.handleInsertBold)}
|
||||
{this.getFormatButton("italic", this.handleInsertItalic)}
|
||||
{this.getFormatButton("link", this.handleInsertLink)}
|
||||
<EmojiPicker
|
||||
onEmojiClick={e => this.handleEmoji(this, e)}
|
||||
disabled={this.isDisabled}
|
||||
></EmojiPicker>
|
||||
<form className="btn btn-sm text-muted font-weight-bold">
|
||||
<label
|
||||
|
@ -239,7 +243,7 @@ export class MarkdownTextArea extends Component<
|
|||
}`}
|
||||
data-tippy-content={i18n.t("upload_image")}
|
||||
>
|
||||
{this.state.imageLoading ? (
|
||||
{this.state.imageUploadStatus ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Icon icon="image" classes="icon-inline" />
|
||||
|
@ -251,74 +255,22 @@ export class MarkdownTextArea extends Component<
|
|||
accept="image/*,video/*"
|
||||
name="file"
|
||||
className="d-none"
|
||||
disabled={!UserService.Instance.myUserInfo}
|
||||
multiple
|
||||
disabled={!UserService.Instance.myUserInfo || this.isDisabled}
|
||||
onChange={linkEvent(this, this.handleImageUpload)}
|
||||
/>
|
||||
</form>
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t("header")}
|
||||
aria-label={i18n.t("header")}
|
||||
onClick={linkEvent(this, this.handleInsertHeader)}
|
||||
>
|
||||
<Icon icon="header" classes="icon-inline" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t("strikethrough")}
|
||||
aria-label={i18n.t("strikethrough")}
|
||||
onClick={linkEvent(this, this.handleInsertStrikethrough)}
|
||||
>
|
||||
<Icon icon="strikethrough" classes="icon-inline" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t("quote")}
|
||||
aria-label={i18n.t("quote")}
|
||||
onClick={linkEvent(this, this.handleInsertQuote)}
|
||||
>
|
||||
<Icon icon="format_quote" classes="icon-inline" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t("list")}
|
||||
aria-label={i18n.t("list")}
|
||||
onClick={linkEvent(this, this.handleInsertList)}
|
||||
>
|
||||
<Icon icon="list" classes="icon-inline" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t("code")}
|
||||
aria-label={i18n.t("code")}
|
||||
onClick={linkEvent(this, this.handleInsertCode)}
|
||||
>
|
||||
<Icon icon="code" classes="icon-inline" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t("subscript")}
|
||||
aria-label={i18n.t("subscript")}
|
||||
onClick={linkEvent(this, this.handleInsertSubscript)}
|
||||
>
|
||||
<Icon icon="subscript" classes="icon-inline" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t("superscript")}
|
||||
aria-label={i18n.t("superscript")}
|
||||
onClick={linkEvent(this, this.handleInsertSuperscript)}
|
||||
>
|
||||
<Icon icon="superscript" classes="icon-inline" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t("spoiler")}
|
||||
aria-label={i18n.t("spoiler")}
|
||||
onClick={linkEvent(this, this.handleInsertSpoiler)}
|
||||
>
|
||||
<Icon icon="alert-triangle" classes="icon-inline" />
|
||||
</button>
|
||||
{this.getFormatButton("header", this.handleInsertHeader)}
|
||||
{this.getFormatButton(
|
||||
"strikethrough",
|
||||
this.handleInsertStrikethrough
|
||||
)}
|
||||
{this.getFormatButton("quote", this.handleInsertQuote)}
|
||||
{this.getFormatButton("list", this.handleInsertList)}
|
||||
{this.getFormatButton("code", this.handleInsertCode)}
|
||||
{this.getFormatButton("subscript", this.handleInsertSubscript)}
|
||||
{this.getFormatButton("superscript", this.handleInsertSuperscript)}
|
||||
{this.getFormatButton("spoiler", this.handleInsertSpoiler)}
|
||||
<a
|
||||
href={markdownHelpUrl}
|
||||
className="btn btn-sm text-muted font-weight-bold"
|
||||
|
@ -333,6 +285,39 @@ export class MarkdownTextArea extends Component<
|
|||
);
|
||||
}
|
||||
|
||||
getFormatButton(
|
||||
type: NoOptionI18nKeys,
|
||||
handleClick: (i: MarkdownTextArea, event: any) => void
|
||||
) {
|
||||
let iconType: string;
|
||||
|
||||
switch (type) {
|
||||
case "spoiler": {
|
||||
iconType = "alert-triangle";
|
||||
break;
|
||||
}
|
||||
case "quote": {
|
||||
iconType = "format_quote";
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
iconType = type;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t(type)}
|
||||
aria-label={i18n.t(type)}
|
||||
onClick={linkEvent(this, handleClick)}
|
||||
disabled={this.isDisabled}
|
||||
>
|
||||
<Icon icon={iconType} classes="icon-inline" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
handleEmoji(i: MarkdownTextArea, e: any) {
|
||||
let value = e.native;
|
||||
if (value == null) {
|
||||
|
@ -350,53 +335,87 @@ export class MarkdownTextArea extends Component<
|
|||
}
|
||||
|
||||
handleImageUploadPaste(i: MarkdownTextArea, event: any) {
|
||||
let image = event.clipboardData.files[0];
|
||||
const image = event.clipboardData.files[0];
|
||||
if (image) {
|
||||
i.handleImageUpload(i, image);
|
||||
}
|
||||
}
|
||||
|
||||
handleImageUpload(i: MarkdownTextArea, event: any) {
|
||||
let file: any;
|
||||
const files: File[] = [];
|
||||
if (event.target) {
|
||||
event.preventDefault();
|
||||
file = event.target.files[0];
|
||||
files.push(...event.target.files);
|
||||
} else {
|
||||
file = event;
|
||||
files.push(event);
|
||||
}
|
||||
|
||||
i.setState({ imageLoading: true });
|
||||
|
||||
uploadImage(file)
|
||||
.then(res => {
|
||||
console.log("pictrs upload:");
|
||||
console.log(res);
|
||||
if (res.msg === "ok") {
|
||||
const imageMarkdown = `![](${res.url})`;
|
||||
const content = i.state.content;
|
||||
i.setState({
|
||||
content: content ? `${content}\n${imageMarkdown}` : imageMarkdown,
|
||||
imageLoading: false,
|
||||
});
|
||||
i.contentChange();
|
||||
const textarea: any = document.getElementById(i.id);
|
||||
autosize.update(textarea);
|
||||
pictrsDeleteToast(
|
||||
`${i18n.t("click_to_delete_picture")}: ${file.name}`,
|
||||
`${i18n.t("picture_deleted")}: ${file.name}`,
|
||||
`${i18n.t("failed_to_delete_picture")}: ${file.name}`,
|
||||
res.delete_url as string
|
||||
);
|
||||
} else {
|
||||
i.setState({ imageLoading: false });
|
||||
toast(JSON.stringify(res), "danger");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
i.setState({ imageLoading: false });
|
||||
console.error(error);
|
||||
toast(error, "danger");
|
||||
if (files.length > maxUploadImages) {
|
||||
toast(
|
||||
i18n.t("too_many_images_upload", {
|
||||
count: maxUploadImages,
|
||||
formattedCount: numToSI(maxUploadImages),
|
||||
}),
|
||||
"danger"
|
||||
);
|
||||
} else {
|
||||
i.setState({
|
||||
imageUploadStatus: { total: files.length, uploaded: 0 },
|
||||
});
|
||||
|
||||
i.uploadImages(i, files).then(() => {
|
||||
i.setState({ imageUploadStatus: undefined });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async uploadImages(i: MarkdownTextArea, files: File[]) {
|
||||
let errorOccurred = false;
|
||||
const filesCopy = [...files];
|
||||
while (filesCopy.length > 0 && !errorOccurred) {
|
||||
try {
|
||||
await Promise.all(
|
||||
filesCopy.splice(0, concurrentImageUpload).map(async file => {
|
||||
await i.uploadSingleImage(i, file);
|
||||
|
||||
this.setState(({ imageUploadStatus }) => ({
|
||||
imageUploadStatus: {
|
||||
...(imageUploadStatus as Required<ImageUploadStatus>),
|
||||
uploaded: (imageUploadStatus?.uploaded ?? 0) + 1,
|
||||
},
|
||||
}));
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
errorOccurred = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async uploadSingleImage(i: MarkdownTextArea, file: File) {
|
||||
try {
|
||||
const res = await uploadImage(file);
|
||||
console.log("pictrs upload:");
|
||||
console.log(res);
|
||||
if (res.msg === "ok") {
|
||||
const imageMarkdown = `![](${res.url})`;
|
||||
i.setState(({ content }) => ({
|
||||
content: content ? `${content}\n${imageMarkdown}` : imageMarkdown,
|
||||
}));
|
||||
i.contentChange();
|
||||
const textarea: any = document.getElementById(i.id);
|
||||
autosize.update(textarea);
|
||||
pictrsDeleteToast(file.name, res.delete_url as string);
|
||||
} else {
|
||||
throw JSON.stringify(res);
|
||||
}
|
||||
} catch (error) {
|
||||
i.setState({ imageUploadStatus: undefined });
|
||||
console.error(error);
|
||||
toast(error, "danger");
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
contentChange() {
|
||||
|
@ -595,11 +614,11 @@ export class MarkdownTextArea extends Component<
|
|||
}
|
||||
|
||||
quoteInsert() {
|
||||
let textarea: any = document.getElementById(this.id);
|
||||
let selectedText = window.getSelection()?.toString();
|
||||
let content = this.state.content;
|
||||
const textarea: any = document.getElementById(this.id);
|
||||
const selectedText = window.getSelection()?.toString();
|
||||
const { content } = this.state;
|
||||
if (selectedText) {
|
||||
let quotedText =
|
||||
const quotedText =
|
||||
selectedText
|
||||
.split("\n")
|
||||
.map(t => `> ${t}`)
|
||||
|
@ -619,9 +638,16 @@ export class MarkdownTextArea extends Component<
|
|||
}
|
||||
|
||||
getSelectedText(): string {
|
||||
let textarea: any = document.getElementById(this.id);
|
||||
let start: number = textarea.selectionStart;
|
||||
let end: number = textarea.selectionEnd;
|
||||
const { selectionStart: start, selectionEnd: end } =
|
||||
document.getElementById(this.id) as any;
|
||||
return start !== end ? this.state.content?.substring(start, end) ?? "" : "";
|
||||
}
|
||||
|
||||
get isDisabled() {
|
||||
return (
|
||||
this.state.loading ||
|
||||
this.props.disabled ||
|
||||
!!this.state.imageUploadStatus
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
44
src/shared/components/common/progress-bar.tsx
Normal file
44
src/shared/components/common/progress-bar.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import classNames from "classnames";
|
||||
import { ThemeColor } from "shared/utils";
|
||||
|
||||
interface ProgressBarProps {
|
||||
className?: string;
|
||||
backgroundColor?: ThemeColor;
|
||||
barColor?: ThemeColor;
|
||||
striped?: boolean;
|
||||
animated?: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
value: number;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
const ProgressBar = ({
|
||||
value,
|
||||
animated = false,
|
||||
backgroundColor = "secondary",
|
||||
barColor = "primary",
|
||||
className,
|
||||
max = 100,
|
||||
min = 0,
|
||||
striped = false,
|
||||
text,
|
||||
}: ProgressBarProps) => (
|
||||
<div className={classNames("progress", `bg-${backgroundColor}`, className)}>
|
||||
<div
|
||||
className={classNames("progress-bar", `bg-${barColor}`, {
|
||||
"progress-bar-striped": striped,
|
||||
"progress-bar-animated": animated,
|
||||
})}
|
||||
role="progressbar"
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
aria-valuenow={value}
|
||||
style={`width: ${((value - min) / max) * 100}%;`}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ProgressBar;
|
|
@ -481,12 +481,7 @@ export class EmojiForm extends Component<any, EmojiFormState> {
|
|||
console.log("pictrs upload:");
|
||||
console.log(res);
|
||||
if (res.msg === "ok") {
|
||||
pictrsDeleteToast(
|
||||
`${i18n.t("click_to_delete_picture")}: ${file.name}`,
|
||||
`${i18n.t("picture_deleted")}: ${file.name}`,
|
||||
`${i18n.t("failed_to_delete_picture")}: ${file.name}`,
|
||||
res.delete_url as string
|
||||
);
|
||||
pictrsDeleteToast(file.name, res.delete_url as string);
|
||||
} else {
|
||||
toast(JSON.stringify(res), "danger");
|
||||
let hash = res.files?.at(0)?.file;
|
||||
|
|
|
@ -596,12 +596,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
if (res.msg === "ok") {
|
||||
i.state.form.url = res.url;
|
||||
i.setState({ imageLoading: false });
|
||||
pictrsDeleteToast(
|
||||
`${i18n.t("click_to_delete_picture")}: ${file.name}`,
|
||||
`${i18n.t("picture_deleted")}: ${file.name}`,
|
||||
`${i18n.t("failed_to_delete_picture")}: ${file.name}`,
|
||||
res.delete_url as string
|
||||
);
|
||||
pictrsDeleteToast(file.name, res.delete_url as string);
|
||||
} else {
|
||||
i.setState({ imageLoading: false });
|
||||
toast(JSON.stringify(res), "danger");
|
||||
|
|
|
@ -77,9 +77,34 @@ export const trendingFetchLimit = 6;
|
|||
export const mentionDropdownFetchLimit = 10;
|
||||
export const commentTreeMaxDepth = 8;
|
||||
export const markdownFieldCharacterLimit = 50000;
|
||||
export const maxUploadImages = 20;
|
||||
export const concurrentImageUpload = 4;
|
||||
|
||||
export const relTags = "noopener nofollow";
|
||||
|
||||
export type ThemeColor =
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "light"
|
||||
| "dark"
|
||||
| "success"
|
||||
| "danger"
|
||||
| "warning"
|
||||
| "info"
|
||||
| "blue"
|
||||
| "indigo"
|
||||
| "purple"
|
||||
| "pink"
|
||||
| "red"
|
||||
| "orange"
|
||||
| "yellow"
|
||||
| "green"
|
||||
| "teal"
|
||||
| "cyan"
|
||||
| "white"
|
||||
| "gray"
|
||||
| "gray-dark";
|
||||
|
||||
let customEmojis: EmojiMartCategory[] = [];
|
||||
export let customEmojisLookup: Map<string, CustomEmojiView> = new Map<
|
||||
string,
|
||||
|
@ -487,9 +512,9 @@ export function isCakeDay(published: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
export function toast(text: string, background = "success") {
|
||||
export function toast(text: string, background: ThemeColor = "success") {
|
||||
if (isBrowser()) {
|
||||
let backgroundColor = `var(--${background})`;
|
||||
const backgroundColor = `var(--${background})`;
|
||||
Toastify({
|
||||
text: text,
|
||||
backgroundColor: backgroundColor,
|
||||
|
@ -500,15 +525,19 @@ export function toast(text: string, background = "success") {
|
|||
}
|
||||
}
|
||||
|
||||
export function pictrsDeleteToast(
|
||||
clickToDeleteText: string,
|
||||
deletePictureText: string,
|
||||
failedDeletePictureText: string,
|
||||
deleteUrl: string
|
||||
) {
|
||||
export function pictrsDeleteToast(filename: string, deleteUrl: string) {
|
||||
if (isBrowser()) {
|
||||
let backgroundColor = `var(--light)`;
|
||||
let toast = Toastify({
|
||||
const clickToDeleteText = i18n.t("click_to_delete_picture", { filename });
|
||||
const deletePictureText = i18n.t("picture_deleted", {
|
||||
filename,
|
||||
});
|
||||
const failedDeletePictureText = i18n.t("failed_to_delete_picture", {
|
||||
filename,
|
||||
});
|
||||
|
||||
const backgroundColor = `var(--light)`;
|
||||
|
||||
const toast = Toastify({
|
||||
text: clickToDeleteText,
|
||||
backgroundColor: backgroundColor,
|
||||
gravity: "top",
|
||||
|
@ -528,6 +557,7 @@ export function pictrsDeleteToast(
|
|||
},
|
||||
close: true,
|
||||
});
|
||||
|
||||
toast.showToast();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue