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:
Jay Sitter 2023-06-25 00:56:12 -04:00
commit 0b6f7ad8f3
13 changed files with 229 additions and 205 deletions

View file

@ -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,

View file

@ -124,7 +124,8 @@
.emoji-picker-container {
position: absolute;
top: 30px;
top: 0;
left: 50%;
z-index: 1000;
transform: translateX(-50%);
}

View file

@ -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} />
)}

View file

@ -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>
)}
</>
) : (

View file

@ -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) {

View file

@ -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,
})}
/>
);
}

View file

@ -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>

View file

@ -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 });
}
}

View file

@ -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);

View file

@ -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>

View file

@ -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}

View file

@ -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 });
}
}

View file

@ -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")}