mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-23 11:21:26 +00:00
Merge remote-tracking branch 'lemmy/main' into fix/fix-font-classes-bs5
* lemmy/main: (35 commits) fix(a11y): Fix non-list item being inside ul list in navbar fix: Fix non-unique ID attribute on re-used element fix: Fix some emoji escape logic fix: Button doesn't need tabindex fix: Fix incorrect function reference fix: Emoji picker can be closed with escape key, other a11y fixes fix: Fix some a11y issues on jump to content button fix: Clarify a comment fix: Fix merge error Remove federation worker count fix: Add triangle alert icon to language warning added litely-compact changed where custom compact code goes added darkly-compact - issue 552 Refactor first load handling Fix issue when navigating awat from settings Give function better name Change function name Make date distance format use correct verbiage Extract date fns setup ...
This commit is contained in:
commit
0b6f7ad8f3
13 changed files with 229 additions and 205 deletions
|
@ -26,9 +26,7 @@
|
|||
"jsx-a11y/aria-activedescendant-has-tabindex": 1,
|
||||
"jsx-a11y/aria-role": 1,
|
||||
"jsx-a11y/click-events-have-key-events": 1,
|
||||
"jsx-a11y/iframe-has-title": 1,
|
||||
"jsx-a11y/interactive-supports-focus": 1,
|
||||
"jsx-a11y/no-redundant-roles": 1,
|
||||
"jsx-a11y/no-static-element-interactions": 1,
|
||||
"jsx-a11y/role-has-required-aria-props": 1,
|
||||
"curly": 0,
|
||||
|
|
|
@ -124,7 +124,8 @@
|
|||
|
||||
.emoji-picker-container {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
z-index: 1000;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
|
|
@ -33,12 +33,13 @@ export class App extends Component<any, any> {
|
|||
<>
|
||||
<Provider i18next={I18NextService.i18n}>
|
||||
<div id="app" className="lemmy-site">
|
||||
<a
|
||||
className="skip-link bg-light text-dark p-2 text-decoration-none position-absolute start-0 z-3"
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-text skip-link bg-light position-absolute start-0 z-3"
|
||||
onClick={linkEvent(this, this.handleJumpToContent)}
|
||||
>
|
||||
${I18NextService.i18n.t("jump_to_content", "Jump to content")}
|
||||
</a>
|
||||
{I18NextService.i18n.t("jump_to_content", "Jump to content")}
|
||||
</button>
|
||||
{siteView && (
|
||||
<Theme defaultTheme={siteView.local_site.default_theme} />
|
||||
)}
|
||||
|
|
|
@ -347,10 +347,10 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
</li>
|
||||
)}
|
||||
{person && (
|
||||
<div id="dropdownUser" className="dropdown">
|
||||
<li id="dropdownUser" className="dropdown">
|
||||
<button
|
||||
type="button"
|
||||
className="btn dropdown-toggle"
|
||||
role="button"
|
||||
aria-expanded="false"
|
||||
data-bs-toggle="dropdown"
|
||||
>
|
||||
|
@ -398,7 +398,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
|
|
@ -12,6 +12,10 @@ interface EmojiPickerState {
|
|||
showPicker: boolean;
|
||||
}
|
||||
|
||||
function closeEmojiMartOnEsc(i, event): void {
|
||||
event.key === "Escape" && i.setState({ showPicker: false });
|
||||
}
|
||||
|
||||
export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
|
||||
private emptyState: EmojiPickerState = {
|
||||
showPicker: false,
|
||||
|
@ -23,6 +27,7 @@ export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
|
|||
this.state = this.emptyState;
|
||||
this.handleEmojiClick = this.handleEmojiClick.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span className="emoji-picker">
|
||||
|
@ -38,8 +43,8 @@ export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
|
|||
|
||||
{this.state.showPicker && (
|
||||
<>
|
||||
<div className="position-relative">
|
||||
<div className="emoji-picker-container position-absolute w-100">
|
||||
<div className="position-relative" role="dialog">
|
||||
<div className="emoji-picker-container">
|
||||
<EmojiMart
|
||||
onEmojiClick={this.handleEmojiClick}
|
||||
pickerOptions={{}}
|
||||
|
@ -56,9 +61,17 @@ export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
|
|||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("keyup", e => closeEmojiMartOnEsc(this, e));
|
||||
}
|
||||
|
||||
togglePicker(i: EmojiPicker, e: any) {
|
||||
e.preventDefault();
|
||||
i.setState({ showPicker: !i.state.showPicker });
|
||||
|
||||
i.state.showPicker
|
||||
? document.addEventListener("keyup", e => closeEmojiMartOnEsc(i, e))
|
||||
: document.removeEventListener("keyup", e => closeEmojiMartOnEsc(i, e));
|
||||
}
|
||||
|
||||
handleEmojiClick(e: any) {
|
||||
|
|
|
@ -35,6 +35,7 @@ export class Icon extends Component<IconProps, any> {
|
|||
|
||||
interface SpinnerProps {
|
||||
large?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export class Spinner extends Component<SpinnerProps, any> {
|
||||
|
@ -46,7 +47,9 @@ export class Spinner extends Component<SpinnerProps, any> {
|
|||
return (
|
||||
<Icon
|
||||
icon="spinner"
|
||||
classes={`spin ${this.props.large && "spinner-large"}`}
|
||||
classes={classNames("spin", this.props.className, {
|
||||
"spinner-large": this.props.large,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,15 +23,28 @@ import NavigationPrompt from "./navigation-prompt";
|
|||
import ProgressBar from "./progress-bar";
|
||||
|
||||
interface MarkdownTextAreaProps {
|
||||
/**
|
||||
* Initial content inside the textarea
|
||||
*/
|
||||
initialContent?: string;
|
||||
/**
|
||||
* Numerical ID of the language to select in dropdown
|
||||
*/
|
||||
initialLanguageId?: number;
|
||||
placeholder?: string;
|
||||
buttonTitle?: string;
|
||||
maxLength?: number;
|
||||
/**
|
||||
* Whether this form is for a reply to a Private Message.
|
||||
* If true, a "Cancel" button is shown that will close the reply.
|
||||
*/
|
||||
replyType?: boolean;
|
||||
focus?: boolean;
|
||||
disabled?: boolean;
|
||||
finished?: boolean;
|
||||
/**
|
||||
* Whether to show the language selector
|
||||
*/
|
||||
showLanguage?: boolean;
|
||||
hideNavigationWarnings?: boolean;
|
||||
onContentChange?(val: string): void;
|
||||
|
@ -276,19 +289,6 @@ export class MarkdownTextArea extends Component<
|
|||
{/* A flex expander */}
|
||||
<div className="flex-grow-1"></div>
|
||||
|
||||
{this.props.buttonTitle && (
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-secondary ms-2"
|
||||
disabled={this.isDisabled}
|
||||
>
|
||||
{this.state.loading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<span>{this.props.buttonTitle}</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{this.props.replyType && (
|
||||
<button
|
||||
type="button"
|
||||
|
@ -298,17 +298,27 @@ export class MarkdownTextArea extends Component<
|
|||
{I18NextService.i18n.t("cancel")}
|
||||
</button>
|
||||
)}
|
||||
{this.state.content && (
|
||||
<button
|
||||
className={`btn btn-sm btn-secondary ms-2 ${
|
||||
this.state.previewMode && "active"
|
||||
}`}
|
||||
type="button"
|
||||
disabled={!this.state.content}
|
||||
className={classNames("btn btn-sm btn-secondary ms-2", {
|
||||
active: this.state.previewMode,
|
||||
})}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
>
|
||||
{this.state.previewMode
|
||||
? I18NextService.i18n.t("edit")
|
||||
: I18NextService.i18n.t("preview")}
|
||||
</button>
|
||||
{this.props.buttonTitle && (
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-secondary ms-2"
|
||||
disabled={this.isDisabled || !this.state.content}
|
||||
>
|
||||
{this.state.loading && <Spinner className="me-1" />}
|
||||
{this.props.buttonTitle}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -44,7 +44,6 @@ interface AdminSettingsState {
|
|||
instancesRes: RequestState<GetFederatedInstancesResponse>;
|
||||
bannedRes: RequestState<BannedPersonsResponse>;
|
||||
leaveAdminTeamRes: RequestState<GetSiteResponse>;
|
||||
emojiLoading: boolean;
|
||||
loading: boolean;
|
||||
themeList: string[];
|
||||
isIsomorphic: boolean;
|
||||
|
@ -59,7 +58,6 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
|||
bannedRes: { state: "empty" },
|
||||
instancesRes: { state: "empty" },
|
||||
leaveAdminTeamRes: { state: "empty" },
|
||||
emojiLoading: false,
|
||||
loading: false,
|
||||
themeList: [],
|
||||
isIsomorphic: false,
|
||||
|
@ -215,7 +213,6 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
|||
onCreate={this.handleCreateEmoji}
|
||||
onDelete={this.handleDeleteEmoji}
|
||||
onEdit={this.handleEditEmoji}
|
||||
loading={this.state.emojiLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -345,35 +342,23 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
|||
}
|
||||
|
||||
async handleEditEmoji(form: EditCustomEmoji) {
|
||||
this.setState({ emojiLoading: true });
|
||||
|
||||
const res = await HttpService.client.editCustomEmoji(form);
|
||||
if (res.state === "success") {
|
||||
updateEmojiDataModel(res.data.custom_emoji);
|
||||
}
|
||||
|
||||
this.setState({ emojiLoading: false });
|
||||
}
|
||||
|
||||
async handleDeleteEmoji(form: DeleteCustomEmoji) {
|
||||
this.setState({ emojiLoading: true });
|
||||
|
||||
const res = await HttpService.client.deleteCustomEmoji(form);
|
||||
if (res.state === "success") {
|
||||
removeFromEmojiDataModel(res.data.id);
|
||||
}
|
||||
|
||||
this.setState({ emojiLoading: false });
|
||||
}
|
||||
|
||||
async handleCreateEmoji(form: CreateCustomEmoji) {
|
||||
this.setState({ emojiLoading: true });
|
||||
|
||||
const res = await HttpService.client.createCustomEmoji(form);
|
||||
if (res.state === "success") {
|
||||
updateEmojiDataModel(res.data.custom_emoji);
|
||||
}
|
||||
|
||||
this.setState({ emojiLoading: false });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { myAuthRequired, setIsoData } from "@utils/app";
|
||||
import { capitalizeFirstLetter } from "@utils/helpers";
|
||||
import { Component, linkEvent } from "inferno";
|
||||
import {
|
||||
CreateCustomEmoji,
|
||||
|
@ -11,14 +12,13 @@ import { HttpService, I18NextService } from "../../services";
|
|||
import { pictrsDeleteToast, toast } from "../../toast";
|
||||
import { EmojiMart } from "../common/emoji-mart";
|
||||
import { HtmlTags } from "../common/html-tags";
|
||||
import { Icon } from "../common/icon";
|
||||
import { Icon, Spinner } from "../common/icon";
|
||||
import { Paginator } from "../common/paginator";
|
||||
|
||||
interface EmojiFormProps {
|
||||
onEdit(form: EditCustomEmoji): void;
|
||||
onCreate(form: CreateCustomEmoji): void;
|
||||
onDelete(form: DeleteCustomEmoji): void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
interface EmojiFormState {
|
||||
|
@ -36,6 +36,7 @@ interface CustomEmojiViewForm {
|
|||
keywords: string;
|
||||
changed: boolean;
|
||||
page: number;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
||||
|
@ -52,6 +53,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
|||
keywords: x.keywords.map(x => x.keyword).join(" "),
|
||||
changed: false,
|
||||
page: 1 + Math.floor(index / this.itemsPerPage),
|
||||
loading: false,
|
||||
})),
|
||||
page: 1,
|
||||
};
|
||||
|
@ -119,25 +121,28 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
|||
.map((cv, index) => (
|
||||
<tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
|
||||
<td style="text-align:center;">
|
||||
<label
|
||||
htmlFor={index.toString()}
|
||||
className="pointer text-muted small fw-bold"
|
||||
>
|
||||
{cv.image_url.length > 0 && (
|
||||
<img
|
||||
className="icon-emoji-admin"
|
||||
src={cv.image_url}
|
||||
alt={cv.alt_text}
|
||||
/>
|
||||
)}
|
||||
{cv.image_url.length == 0 && (
|
||||
<span className="btn btn-sm btn-secondary">
|
||||
Upload
|
||||
</span>
|
||||
{cv.image_url.length === 0 && (
|
||||
<form>
|
||||
<label
|
||||
className="btn btn-sm btn-secondary pointer"
|
||||
htmlFor={`file-uploader-${index}`}
|
||||
data-tippy-content={I18NextService.i18n.t(
|
||||
"upload_image"
|
||||
)}
|
||||
>
|
||||
{capitalizeFirstLetter(
|
||||
I18NextService.i18n.t("upload")
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
name={index.toString()}
|
||||
id={index.toString()}
|
||||
name={`file-uploader-${index}`}
|
||||
id={`file-uploader-${index}`}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="d-none"
|
||||
|
@ -146,6 +151,9 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
|||
this.handleImageUpload
|
||||
)}
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<input
|
||||
|
@ -213,8 +221,9 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
|||
<span title={this.getEditTooltip(cv)}>
|
||||
<button
|
||||
className={
|
||||
(cv.changed ? "text-success " : "text-muted ") +
|
||||
"btn btn-link btn-animate"
|
||||
(this.canEdit(cv)
|
||||
? "text-success "
|
||||
: "text-muted ") + "btn btn-link btn-animate"
|
||||
}
|
||||
onClick={linkEvent(
|
||||
{ i: this, cv: cv },
|
||||
|
@ -222,17 +231,15 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
|||
)}
|
||||
data-tippy-content={I18NextService.i18n.t("save")}
|
||||
aria-label={I18NextService.i18n.t("save")}
|
||||
disabled={
|
||||
this.props.loading ||
|
||||
!this.canEdit(cv) ||
|
||||
!cv.changed
|
||||
}
|
||||
disabled={!this.canEdit(cv)}
|
||||
>
|
||||
{/* <Icon
|
||||
icon="edit"
|
||||
classes={`icon-inline`}
|
||||
/> */}
|
||||
Save
|
||||
{cv.loading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
capitalizeFirstLetter(
|
||||
I18NextService.i18n.t("save")
|
||||
)
|
||||
)}
|
||||
</button>
|
||||
</span>
|
||||
<button
|
||||
|
@ -243,7 +250,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
|||
)}
|
||||
data-tippy-content={I18NextService.i18n.t("delete")}
|
||||
aria-label={I18NextService.i18n.t("delete")}
|
||||
disabled={this.props.loading}
|
||||
disabled={cv.loading}
|
||||
title={I18NextService.i18n.t("delete")}
|
||||
>
|
||||
<Icon
|
||||
|
@ -281,7 +288,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
|||
this.state.customEmojis.filter(
|
||||
x => x.shortcode == cv.shortcode && x.id != cv.id
|
||||
).length == 0;
|
||||
return noEmptyFields && noDuplicateShortCodes;
|
||||
return noEmptyFields && noDuplicateShortCodes && !cv.loading && cv.changed;
|
||||
}
|
||||
|
||||
getEditTooltip(cv: CustomEmojiViewForm) {
|
||||
|
@ -339,19 +346,36 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
|||
}
|
||||
|
||||
handleEmojiImageUrlChange(
|
||||
props: { form: EmojiForm; index: number; overrideValue: string | null },
|
||||
{
|
||||
form,
|
||||
index,
|
||||
overrideValue,
|
||||
}: { form: EmojiForm; index: number; overrideValue: string | null },
|
||||
event: any
|
||||
) {
|
||||
const custom_emojis = [...props.form.state.customEmojis];
|
||||
const pagedIndex =
|
||||
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
|
||||
form.setState(prevState => {
|
||||
const custom_emojis = [...form.state.customEmojis];
|
||||
const pagedIndex = (form.state.page - 1) * form.itemsPerPage + index;
|
||||
const item = {
|
||||
...props.form.state.customEmojis[pagedIndex],
|
||||
image_url: props.overrideValue ?? event.target.value,
|
||||
...form.state.customEmojis[pagedIndex],
|
||||
image_url: overrideValue ?? event.target.value,
|
||||
changed: true,
|
||||
};
|
||||
custom_emojis[Number(pagedIndex)] = item;
|
||||
props.form.setState({ customEmojis: custom_emojis });
|
||||
return {
|
||||
...prevState,
|
||||
customEmojis: prevState.customEmojis.map((ce, i) =>
|
||||
i === pagedIndex
|
||||
? {
|
||||
...ce,
|
||||
image_url: overrideValue ?? event.target.value,
|
||||
changed: true,
|
||||
loading: false,
|
||||
}
|
||||
: ce
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
handleEmojiAltTextChange(
|
||||
|
@ -409,7 +433,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
|||
.split(" ")
|
||||
.filter(x => x.length > 0) as string[];
|
||||
const uniqueKeywords = Array.from(new Set(keywords));
|
||||
if (d.cv.id != 0) {
|
||||
if (d.cv.id !== 0) {
|
||||
d.i.props.onEdit({
|
||||
id: d.cv.id,
|
||||
category: d.cv.category,
|
||||
|
@ -432,9 +456,9 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
|||
|
||||
handleAddEmojiClick(form: EmojiForm, event: any) {
|
||||
event.preventDefault();
|
||||
const custom_emojis = [...form.state.customEmojis];
|
||||
form.setState(prevState => {
|
||||
const page =
|
||||
1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage);
|
||||
1 + Math.floor(prevState.customEmojis.length / form.itemsPerPage);
|
||||
const item: CustomEmojiViewForm = {
|
||||
id: 0,
|
||||
shortcode: "",
|
||||
|
@ -442,14 +466,23 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
|||
category: "",
|
||||
image_url: "",
|
||||
keywords: "",
|
||||
changed: true,
|
||||
changed: false,
|
||||
page: page,
|
||||
loading: false,
|
||||
};
|
||||
custom_emojis.push(item);
|
||||
form.setState({ customEmojis: custom_emojis, page: page });
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
customEmojis: [...prevState.customEmojis, item],
|
||||
page,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
handleImageUpload(props: { form: EmojiForm; index: number }, event: any) {
|
||||
handleImageUpload(
|
||||
{ form, index }: { form: EmojiForm; index: number },
|
||||
event: any
|
||||
) {
|
||||
let file: any;
|
||||
if (event.target) {
|
||||
event.preventDefault();
|
||||
|
@ -458,20 +491,25 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
|||
file = event;
|
||||
}
|
||||
|
||||
form.setState(prevState => ({
|
||||
...prevState,
|
||||
customEmojis: prevState.customEmojis.map((cv, i) =>
|
||||
i === index ? { ...cv, loading: true } : cv
|
||||
),
|
||||
}));
|
||||
|
||||
HttpService.client.uploadImage({ image: file }).then(res => {
|
||||
console.log("pictrs upload:");
|
||||
console.log(res);
|
||||
if (res.state === "success") {
|
||||
if (res.data.msg === "ok") {
|
||||
pictrsDeleteToast(file.name, res.data.delete_url as string);
|
||||
} else {
|
||||
toast(JSON.stringify(res), "danger");
|
||||
const hash = res.data.files?.at(0)?.file;
|
||||
const url = `${res.data.url}/${hash}`;
|
||||
props.form.handleEmojiImageUrlChange(
|
||||
{ form: props.form, index: props.index, overrideValue: url },
|
||||
form.handleEmojiImageUrlChange(
|
||||
{ form: form, index: index, overrideValue: res.data.url as string },
|
||||
event
|
||||
);
|
||||
} else {
|
||||
toast(JSON.stringify(res), "danger");
|
||||
}
|
||||
} else if (res.state === "failed") {
|
||||
console.error(res.msg);
|
||||
|
|
|
@ -707,13 +707,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
data-tippy-content={I18NextService.i18n.t("more")}
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
aria-controls="advancedButtonsDropdown"
|
||||
aria-controls={`advancedButtonsDropdown${post.id}`}
|
||||
aria-label={I18NextService.i18n.t("more")}
|
||||
>
|
||||
<Icon icon="more-vertical" inline />
|
||||
</button>
|
||||
|
||||
<ul className="dropdown-menu" id="advancedButtonsDropdown">
|
||||
<ul
|
||||
className="dropdown-menu"
|
||||
id={`advancedButtonsDropdown${post.id}`}
|
||||
>
|
||||
{!this.myPost ? (
|
||||
<>
|
||||
<li>{this.reportButton}</li>
|
||||
|
|
|
@ -115,7 +115,9 @@ export class CreatePrivateMessage extends Component<
|
|||
return (
|
||||
<div className="row">
|
||||
<div className="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||
<h5>{I18NextService.i18n.t("create_private_message")}</h5>
|
||||
<h1 className="h4">
|
||||
{I18NextService.i18n.t("create_private_message")}
|
||||
</h1>
|
||||
<PrivateMessageForm
|
||||
onCreate={this.handlePrivateMessageCreate}
|
||||
recipient={res.person_view.person}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { myAuthRequired } from "@utils/app";
|
||||
import { capitalizeFirstLetter } from "@utils/helpers";
|
||||
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||
import { Component, InfernoNode } from "inferno";
|
||||
import { T } from "inferno-i18next-dess";
|
||||
import {
|
||||
CreatePrivateMessage,
|
||||
|
@ -11,7 +11,7 @@ import {
|
|||
import { relTags } from "../../config";
|
||||
import { I18NextService } from "../../services";
|
||||
import { setupTippy } from "../../tippy";
|
||||
import { Icon, Spinner } from "../common/icon";
|
||||
import { Icon } from "../common/icon";
|
||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||
import NavigationPrompt from "../common/navigation-prompt";
|
||||
import { PersonListing } from "../person/person-listing";
|
||||
|
@ -19,6 +19,7 @@ import { PersonListing } from "../person/person-listing";
|
|||
interface PrivateMessageFormProps {
|
||||
recipient: Person;
|
||||
privateMessageView?: PrivateMessageView; // If a pm is given, that means this is an edit
|
||||
replyType?: boolean;
|
||||
onCancel?(): any;
|
||||
onCreate?(form: CreatePrivateMessage): void;
|
||||
onEdit?(form: EditPrivateMessage): void;
|
||||
|
@ -28,7 +29,6 @@ interface PrivateMessageFormState {
|
|||
content?: string;
|
||||
loading: boolean;
|
||||
previewMode: boolean;
|
||||
showDisclaimer: boolean;
|
||||
submitted: boolean;
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,6 @@ export class PrivateMessageForm extends Component<
|
|||
state: PrivateMessageFormState = {
|
||||
loading: false,
|
||||
previewMode: false,
|
||||
showDisclaimer: false,
|
||||
content: this.props.privateMessageView
|
||||
? this.props.privateMessageView.private_message.content
|
||||
: undefined,
|
||||
|
@ -71,56 +70,26 @@ export class PrivateMessageForm extends Component<
|
|||
|
||||
render() {
|
||||
return (
|
||||
<form
|
||||
className="private-message-form"
|
||||
onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}
|
||||
>
|
||||
<form className="private-message-form">
|
||||
<NavigationPrompt
|
||||
when={
|
||||
!this.state.loading && !!this.state.content && !this.state.submitted
|
||||
}
|
||||
/>
|
||||
{!this.props.privateMessageView && (
|
||||
<div className="mb-3 row">
|
||||
<div className="mb-3 row align-items-baseline">
|
||||
<label className="col-sm-2 col-form-label">
|
||||
{capitalizeFirstLetter(I18NextService.i18n.t("to"))}
|
||||
</label>
|
||||
|
||||
<div className="col-sm-10 form-control-plaintext">
|
||||
<div className="col-sm-10">
|
||||
<PersonListing person={this.props.recipient} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3 row">
|
||||
<label className="col-sm-2 col-form-label">
|
||||
{I18NextService.i18n.t("message")}
|
||||
<button
|
||||
className="btn btn-link text-warning d-inline-block"
|
||||
onClick={linkEvent(this, this.handleShowDisclaimer)}
|
||||
data-tippy-content={I18NextService.i18n.t(
|
||||
"private_message_disclaimer"
|
||||
)}
|
||||
aria-label={I18NextService.i18n.t("private_message_disclaimer")}
|
||||
>
|
||||
<Icon icon="alert-triangle" classes="icon-inline" />
|
||||
</button>
|
||||
</label>
|
||||
<div className="col-sm-10">
|
||||
<MarkdownTextArea
|
||||
initialContent={this.state.content}
|
||||
onContentChange={this.handleContentChange}
|
||||
allLanguages={[]}
|
||||
siteLanguages={[]}
|
||||
hideNavigationWarnings
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.showDisclaimer && (
|
||||
<div className="mb-3 row">
|
||||
<div className="offset-sm-2 col-sm-10">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<T i18nKey="private_message_disclaimer">
|
||||
<div className="alert alert-warning small">
|
||||
<Icon icon="alert-triangle" classes="icon-inline me-1" />
|
||||
<T parent="span" i18nKey="private_message_disclaimer">
|
||||
#
|
||||
<a
|
||||
className="alert-link"
|
||||
|
@ -131,36 +100,28 @@ export class PrivateMessageForm extends Component<
|
|||
</a>
|
||||
</T>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3 row">
|
||||
<div className="offset-sm-2 col-sm-10">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-secondary me-2"
|
||||
disabled={this.state.loading}
|
||||
>
|
||||
{this.state.loading ? (
|
||||
<Spinner />
|
||||
) : this.props.privateMessageView ? (
|
||||
capitalizeFirstLetter(I18NextService.i18n.t("save"))
|
||||
) : (
|
||||
capitalizeFirstLetter(I18NextService.i18n.t("send_message"))
|
||||
)}
|
||||
</button>
|
||||
{this.props.privateMessageView && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={linkEvent(this, this.handleCancel)}
|
||||
>
|
||||
{I18NextService.i18n.t("cancel")}
|
||||
</button>
|
||||
)}
|
||||
<ul className="d-inline-block float-right list-inline mb-1 text-muted fw-bold">
|
||||
<li className="list-inline-item"></li>
|
||||
</ul>
|
||||
<label className="col-sm-2 col-form-label">
|
||||
{I18NextService.i18n.t("message")}
|
||||
</label>
|
||||
<div className="col-sm-10">
|
||||
<MarkdownTextArea
|
||||
onSubmit={() => {
|
||||
this.handlePrivateMessageSubmit(this, event);
|
||||
}}
|
||||
initialContent={this.state.content}
|
||||
onContentChange={this.handleContentChange}
|
||||
allLanguages={[]}
|
||||
siteLanguages={[]}
|
||||
hideNavigationWarnings
|
||||
onReplyCancel={() => this.handleCancel(this)}
|
||||
replyType={this.props.replyType}
|
||||
buttonTitle={
|
||||
this.props.privateMessageView
|
||||
? capitalizeFirstLetter(I18NextService.i18n.t("save"))
|
||||
: capitalizeFirstLetter(I18NextService.i18n.t("send_message"))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -200,8 +161,4 @@ export class PrivateMessageForm extends Component<
|
|||
event.preventDefault();
|
||||
i.setState({ previewMode: !i.state.previewMode });
|
||||
}
|
||||
|
||||
handleShowDisclaimer(i: PrivateMessageForm) {
|
||||
i.setState({ showDisclaimer: !i.state.showDisclaimer });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,6 +145,7 @@ export class PrivateMessage extends Component<
|
|||
<>
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link btn-animate text-muted"
|
||||
onClick={linkEvent(this, this.handleMarkRead)}
|
||||
data-tippy-content={
|
||||
|
@ -174,6 +175,7 @@ export class PrivateMessage extends Component<
|
|||
<li className="list-inline-item">{this.reportButton}</li>
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link btn-animate text-muted"
|
||||
onClick={linkEvent(this, this.handleReplyClick)}
|
||||
data-tippy-content={I18NextService.i18n.t("reply")}
|
||||
|
@ -188,6 +190,7 @@ export class PrivateMessage extends Component<
|
|||
<>
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link btn-animate text-muted"
|
||||
onClick={linkEvent(this, this.handleEditClick)}
|
||||
data-tippy-content={I18NextService.i18n.t("edit")}
|
||||
|
@ -198,6 +201,7 @@ export class PrivateMessage extends Component<
|
|||
</li>
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link btn-animate text-muted"
|
||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||
data-tippy-content={
|
||||
|
@ -228,6 +232,7 @@ export class PrivateMessage extends Component<
|
|||
)}
|
||||
<li className="list-inline-item">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link btn-animate text-muted"
|
||||
onClick={linkEvent(this, this.handleViewSource)}
|
||||
data-tippy-content={I18NextService.i18n.t("view_source")}
|
||||
|
@ -276,10 +281,17 @@ export class PrivateMessage extends Component<
|
|||
</form>
|
||||
)}
|
||||
{this.state.showReply && (
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<PrivateMessageForm
|
||||
privateMessageView={message_view}
|
||||
replyType={true}
|
||||
recipient={otherPerson}
|
||||
onCreate={this.props.onCreate}
|
||||
onCancel={this.handleReplyCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* A collapsed clearfix */}
|
||||
{this.state.collapsed && <div className="row col-12"></div>}
|
||||
|
@ -290,6 +302,7 @@ export class PrivateMessage extends Component<
|
|||
get reportButton() {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link btn-animate text-muted py-0"
|
||||
onClick={linkEvent(this, this.handleShowReportDialog)}
|
||||
data-tippy-content={I18NextService.i18n.t("show_report_dialog")}
|
||||
|
|
Loading…
Reference in a new issue