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-activedescendant-has-tabindex": 1,
"jsx-a11y/aria-role": 1, "jsx-a11y/aria-role": 1,
"jsx-a11y/click-events-have-key-events": 1, "jsx-a11y/click-events-have-key-events": 1,
"jsx-a11y/iframe-has-title": 1,
"jsx-a11y/interactive-supports-focus": 1, "jsx-a11y/interactive-supports-focus": 1,
"jsx-a11y/no-redundant-roles": 1,
"jsx-a11y/no-static-element-interactions": 1, "jsx-a11y/no-static-element-interactions": 1,
"jsx-a11y/role-has-required-aria-props": 1, "jsx-a11y/role-has-required-aria-props": 1,
"curly": 0, "curly": 0,

View file

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

View file

@ -33,12 +33,13 @@ export class App extends Component<any, any> {
<> <>
<Provider i18next={I18NextService.i18n}> <Provider i18next={I18NextService.i18n}>
<div id="app" className="lemmy-site"> <div id="app" className="lemmy-site">
<a <button
className="skip-link bg-light text-dark p-2 text-decoration-none position-absolute start-0 z-3" type="button"
className="btn btn-text skip-link bg-light position-absolute start-0 z-3"
onClick={linkEvent(this, this.handleJumpToContent)} onClick={linkEvent(this, this.handleJumpToContent)}
> >
${I18NextService.i18n.t("jump_to_content", "Jump to content")} {I18NextService.i18n.t("jump_to_content", "Jump to content")}
</a> </button>
{siteView && ( {siteView && (
<Theme defaultTheme={siteView.local_site.default_theme} /> <Theme defaultTheme={siteView.local_site.default_theme} />
)} )}

View file

@ -347,10 +347,10 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</li> </li>
)} )}
{person && ( {person && (
<div id="dropdownUser" className="dropdown"> <li id="dropdownUser" className="dropdown">
<button <button
type="button"
className="btn dropdown-toggle" className="btn dropdown-toggle"
role="button"
aria-expanded="false" aria-expanded="false"
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
> >
@ -398,7 +398,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</button> </button>
</li> </li>
</ul> </ul>
</div> </li>
)} )}
</> </>
) : ( ) : (

View file

@ -12,6 +12,10 @@ interface EmojiPickerState {
showPicker: boolean; showPicker: boolean;
} }
function closeEmojiMartOnEsc(i, event): void {
event.key === "Escape" && i.setState({ showPicker: false });
}
export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> { export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
private emptyState: EmojiPickerState = { private emptyState: EmojiPickerState = {
showPicker: false, showPicker: false,
@ -23,6 +27,7 @@ export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
this.state = this.emptyState; this.state = this.emptyState;
this.handleEmojiClick = this.handleEmojiClick.bind(this); this.handleEmojiClick = this.handleEmojiClick.bind(this);
} }
render() { render() {
return ( return (
<span className="emoji-picker"> <span className="emoji-picker">
@ -38,8 +43,8 @@ export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
{this.state.showPicker && ( {this.state.showPicker && (
<> <>
<div className="position-relative"> <div className="position-relative" role="dialog">
<div className="emoji-picker-container position-absolute w-100"> <div className="emoji-picker-container">
<EmojiMart <EmojiMart
onEmojiClick={this.handleEmojiClick} onEmojiClick={this.handleEmojiClick}
pickerOptions={{}} 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) { togglePicker(i: EmojiPicker, e: any) {
e.preventDefault(); e.preventDefault();
i.setState({ showPicker: !i.state.showPicker }); 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) { handleEmojiClick(e: any) {

View file

@ -35,6 +35,7 @@ export class Icon extends Component<IconProps, any> {
interface SpinnerProps { interface SpinnerProps {
large?: boolean; large?: boolean;
className?: string;
} }
export class Spinner extends Component<SpinnerProps, any> { export class Spinner extends Component<SpinnerProps, any> {
@ -46,7 +47,9 @@ export class Spinner extends Component<SpinnerProps, any> {
return ( return (
<Icon <Icon
icon="spinner" 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"; import ProgressBar from "./progress-bar";
interface MarkdownTextAreaProps { interface MarkdownTextAreaProps {
/**
* Initial content inside the textarea
*/
initialContent?: string; initialContent?: string;
/**
* Numerical ID of the language to select in dropdown
*/
initialLanguageId?: number; initialLanguageId?: number;
placeholder?: string; placeholder?: string;
buttonTitle?: string; buttonTitle?: string;
maxLength?: number; 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; replyType?: boolean;
focus?: boolean; focus?: boolean;
disabled?: boolean; disabled?: boolean;
finished?: boolean; finished?: boolean;
/**
* Whether to show the language selector
*/
showLanguage?: boolean; showLanguage?: boolean;
hideNavigationWarnings?: boolean; hideNavigationWarnings?: boolean;
onContentChange?(val: string): void; onContentChange?(val: string): void;
@ -276,19 +289,6 @@ export class MarkdownTextArea extends Component<
{/* A flex expander */} {/* A flex expander */}
<div className="flex-grow-1"></div> <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 && ( {this.props.replyType && (
<button <button
type="button" type="button"
@ -298,16 +298,26 @@ export class MarkdownTextArea extends Component<
{I18NextService.i18n.t("cancel")} {I18NextService.i18n.t("cancel")}
</button> </button>
)} )}
{this.state.content && ( <button
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 <button
className={`btn btn-sm btn-secondary ms-2 ${ type="submit"
this.state.previewMode && "active" className="btn btn-sm btn-secondary ms-2"
}`} disabled={this.isDisabled || !this.state.content}
onClick={linkEvent(this, this.handlePreviewToggle)}
> >
{this.state.previewMode {this.state.loading && <Spinner className="me-1" />}
? I18NextService.i18n.t("edit") {this.props.buttonTitle}
: I18NextService.i18n.t("preview")}
</button> </button>
)} )}
</div> </div>

View file

@ -44,7 +44,6 @@ interface AdminSettingsState {
instancesRes: RequestState<GetFederatedInstancesResponse>; instancesRes: RequestState<GetFederatedInstancesResponse>;
bannedRes: RequestState<BannedPersonsResponse>; bannedRes: RequestState<BannedPersonsResponse>;
leaveAdminTeamRes: RequestState<GetSiteResponse>; leaveAdminTeamRes: RequestState<GetSiteResponse>;
emojiLoading: boolean;
loading: boolean; loading: boolean;
themeList: string[]; themeList: string[];
isIsomorphic: boolean; isIsomorphic: boolean;
@ -59,7 +58,6 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
bannedRes: { state: "empty" }, bannedRes: { state: "empty" },
instancesRes: { state: "empty" }, instancesRes: { state: "empty" },
leaveAdminTeamRes: { state: "empty" }, leaveAdminTeamRes: { state: "empty" },
emojiLoading: false,
loading: false, loading: false,
themeList: [], themeList: [],
isIsomorphic: false, isIsomorphic: false,
@ -215,7 +213,6 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
onCreate={this.handleCreateEmoji} onCreate={this.handleCreateEmoji}
onDelete={this.handleDeleteEmoji} onDelete={this.handleDeleteEmoji}
onEdit={this.handleEditEmoji} onEdit={this.handleEditEmoji}
loading={this.state.emojiLoading}
/> />
</div> </div>
</div> </div>
@ -345,35 +342,23 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
} }
async handleEditEmoji(form: EditCustomEmoji) { async handleEditEmoji(form: EditCustomEmoji) {
this.setState({ emojiLoading: true });
const res = await HttpService.client.editCustomEmoji(form); const res = await HttpService.client.editCustomEmoji(form);
if (res.state === "success") { if (res.state === "success") {
updateEmojiDataModel(res.data.custom_emoji); updateEmojiDataModel(res.data.custom_emoji);
} }
this.setState({ emojiLoading: false });
} }
async handleDeleteEmoji(form: DeleteCustomEmoji) { async handleDeleteEmoji(form: DeleteCustomEmoji) {
this.setState({ emojiLoading: true });
const res = await HttpService.client.deleteCustomEmoji(form); const res = await HttpService.client.deleteCustomEmoji(form);
if (res.state === "success") { if (res.state === "success") {
removeFromEmojiDataModel(res.data.id); removeFromEmojiDataModel(res.data.id);
} }
this.setState({ emojiLoading: false });
} }
async handleCreateEmoji(form: CreateCustomEmoji) { async handleCreateEmoji(form: CreateCustomEmoji) {
this.setState({ emojiLoading: true });
const res = await HttpService.client.createCustomEmoji(form); const res = await HttpService.client.createCustomEmoji(form);
if (res.state === "success") { if (res.state === "success") {
updateEmojiDataModel(res.data.custom_emoji); updateEmojiDataModel(res.data.custom_emoji);
} }
this.setState({ emojiLoading: false });
} }
} }

View file

@ -1,4 +1,5 @@
import { myAuthRequired, setIsoData } from "@utils/app"; import { myAuthRequired, setIsoData } from "@utils/app";
import { capitalizeFirstLetter } from "@utils/helpers";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
CreateCustomEmoji, CreateCustomEmoji,
@ -11,14 +12,13 @@ import { HttpService, I18NextService } from "../../services";
import { pictrsDeleteToast, toast } from "../../toast"; import { pictrsDeleteToast, toast } from "../../toast";
import { EmojiMart } from "../common/emoji-mart"; import { EmojiMart } from "../common/emoji-mart";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Icon } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
import { Paginator } from "../common/paginator"; import { Paginator } from "../common/paginator";
interface EmojiFormProps { interface EmojiFormProps {
onEdit(form: EditCustomEmoji): void; onEdit(form: EditCustomEmoji): void;
onCreate(form: CreateCustomEmoji): void; onCreate(form: CreateCustomEmoji): void;
onDelete(form: DeleteCustomEmoji): void; onDelete(form: DeleteCustomEmoji): void;
loading: boolean;
} }
interface EmojiFormState { interface EmojiFormState {
@ -36,6 +36,7 @@ interface CustomEmojiViewForm {
keywords: string; keywords: string;
changed: boolean; changed: boolean;
page: number; page: number;
loading: boolean;
} }
export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> { 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(" "), keywords: x.keywords.map(x => x.keyword).join(" "),
changed: false, changed: false,
page: 1 + Math.floor(index / this.itemsPerPage), page: 1 + Math.floor(index / this.itemsPerPage),
loading: false,
})), })),
page: 1, page: 1,
}; };
@ -119,33 +121,39 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
.map((cv, index) => ( .map((cv, index) => (
<tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}> <tr key={index} ref={e => (this.scrollRef[cv.shortcode] = e)}>
<td style="text-align:center;"> <td style="text-align:center;">
<label {cv.image_url.length > 0 && (
htmlFor={index.toString()} <img
className="pointer text-muted small fw-bold" className="icon-emoji-admin"
> src={cv.image_url}
{cv.image_url.length > 0 && ( alt={cv.alt_text}
<img />
className="icon-emoji-admin" )}
src={cv.image_url} {cv.image_url.length === 0 && (
/> <form>
)} <label
{cv.image_url.length == 0 && ( className="btn btn-sm btn-secondary pointer"
<span className="btn btn-sm btn-secondary"> htmlFor={`file-uploader-${index}`}
Upload data-tippy-content={I18NextService.i18n.t(
</span> "upload_image"
)} )}
</label> >
<input {capitalizeFirstLetter(
name={index.toString()} I18NextService.i18n.t("upload")
id={index.toString()} )}
type="file" <input
accept="image/*" name={`file-uploader-${index}`}
className="d-none" id={`file-uploader-${index}`}
onChange={linkEvent( type="file"
{ form: this, index: index }, accept="image/*"
this.handleImageUpload className="d-none"
)} onChange={linkEvent(
/> { form: this, index: index },
this.handleImageUpload
)}
/>
</label>
</form>
)}
</td> </td>
<td className="text-right"> <td className="text-right">
<input <input
@ -213,8 +221,9 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
<span title={this.getEditTooltip(cv)}> <span title={this.getEditTooltip(cv)}>
<button <button
className={ className={
(cv.changed ? "text-success " : "text-muted ") + (this.canEdit(cv)
"btn btn-link btn-animate" ? "text-success "
: "text-muted ") + "btn btn-link btn-animate"
} }
onClick={linkEvent( onClick={linkEvent(
{ i: this, cv: cv }, { i: this, cv: cv },
@ -222,17 +231,15 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
)} )}
data-tippy-content={I18NextService.i18n.t("save")} data-tippy-content={I18NextService.i18n.t("save")}
aria-label={I18NextService.i18n.t("save")} aria-label={I18NextService.i18n.t("save")}
disabled={ disabled={!this.canEdit(cv)}
this.props.loading ||
!this.canEdit(cv) ||
!cv.changed
}
> >
{/* <Icon {cv.loading ? (
icon="edit" <Spinner />
classes={`icon-inline`} ) : (
/> */} capitalizeFirstLetter(
Save I18NextService.i18n.t("save")
)
)}
</button> </button>
</span> </span>
<button <button
@ -243,7 +250,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
)} )}
data-tippy-content={I18NextService.i18n.t("delete")} data-tippy-content={I18NextService.i18n.t("delete")}
aria-label={I18NextService.i18n.t("delete")} aria-label={I18NextService.i18n.t("delete")}
disabled={this.props.loading} disabled={cv.loading}
title={I18NextService.i18n.t("delete")} title={I18NextService.i18n.t("delete")}
> >
<Icon <Icon
@ -281,7 +288,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
this.state.customEmojis.filter( this.state.customEmojis.filter(
x => x.shortcode == cv.shortcode && x.id != cv.id x => x.shortcode == cv.shortcode && x.id != cv.id
).length == 0; ).length == 0;
return noEmptyFields && noDuplicateShortCodes; return noEmptyFields && noDuplicateShortCodes && !cv.loading && cv.changed;
} }
getEditTooltip(cv: CustomEmojiViewForm) { getEditTooltip(cv: CustomEmojiViewForm) {
@ -339,19 +346,36 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
} }
handleEmojiImageUrlChange( handleEmojiImageUrlChange(
props: { form: EmojiForm; index: number; overrideValue: string | null }, {
form,
index,
overrideValue,
}: { form: EmojiForm; index: number; overrideValue: string | null },
event: any event: any
) { ) {
const custom_emojis = [...props.form.state.customEmojis]; form.setState(prevState => {
const pagedIndex = const custom_emojis = [...form.state.customEmojis];
(props.form.state.page - 1) * props.form.itemsPerPage + props.index; const pagedIndex = (form.state.page - 1) * form.itemsPerPage + index;
const item = { const item = {
...props.form.state.customEmojis[pagedIndex], ...form.state.customEmojis[pagedIndex],
image_url: props.overrideValue ?? event.target.value, image_url: overrideValue ?? event.target.value,
changed: true, changed: true,
}; };
custom_emojis[Number(pagedIndex)] = item; 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( handleEmojiAltTextChange(
@ -409,7 +433,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
.split(" ") .split(" ")
.filter(x => x.length > 0) as string[]; .filter(x => x.length > 0) as string[];
const uniqueKeywords = Array.from(new Set(keywords)); const uniqueKeywords = Array.from(new Set(keywords));
if (d.cv.id != 0) { if (d.cv.id !== 0) {
d.i.props.onEdit({ d.i.props.onEdit({
id: d.cv.id, id: d.cv.id,
category: d.cv.category, category: d.cv.category,
@ -432,24 +456,33 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
handleAddEmojiClick(form: EmojiForm, event: any) { handleAddEmojiClick(form: EmojiForm, event: any) {
event.preventDefault(); event.preventDefault();
const custom_emojis = [...form.state.customEmojis]; form.setState(prevState => {
const page = const page =
1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage); 1 + Math.floor(prevState.customEmojis.length / form.itemsPerPage);
const item: CustomEmojiViewForm = { const item: CustomEmojiViewForm = {
id: 0, id: 0,
shortcode: "", shortcode: "",
alt_text: "", alt_text: "",
category: "", category: "",
image_url: "", image_url: "",
keywords: "", keywords: "",
changed: true, changed: false,
page: page, 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; let file: any;
if (event.target) { if (event.target) {
event.preventDefault(); event.preventDefault();
@ -458,20 +491,25 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
file = event; 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 => { HttpService.client.uploadImage({ image: file }).then(res => {
console.log("pictrs upload:"); console.log("pictrs upload:");
console.log(res); console.log(res);
if (res.state === "success") { if (res.state === "success") {
if (res.data.msg === "ok") { if (res.data.msg === "ok") {
pictrsDeleteToast(file.name, res.data.delete_url as string); pictrsDeleteToast(file.name, res.data.delete_url as string);
} else { form.handleEmojiImageUrlChange(
toast(JSON.stringify(res), "danger"); { form: form, index: index, overrideValue: res.data.url as string },
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 },
event event
); );
} else {
toast(JSON.stringify(res), "danger");
} }
} else if (res.state === "failed") { } else if (res.state === "failed") {
console.error(res.msg); 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-tippy-content={I18NextService.i18n.t("more")}
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
aria-expanded="false" aria-expanded="false"
aria-controls="advancedButtonsDropdown" aria-controls={`advancedButtonsDropdown${post.id}`}
aria-label={I18NextService.i18n.t("more")} aria-label={I18NextService.i18n.t("more")}
> >
<Icon icon="more-vertical" inline /> <Icon icon="more-vertical" inline />
</button> </button>
<ul className="dropdown-menu" id="advancedButtonsDropdown"> <ul
className="dropdown-menu"
id={`advancedButtonsDropdown${post.id}`}
>
{!this.myPost ? ( {!this.myPost ? (
<> <>
<li>{this.reportButton}</li> <li>{this.reportButton}</li>

View file

@ -115,7 +115,9 @@ export class CreatePrivateMessage extends Component<
return ( return (
<div className="row"> <div className="row">
<div className="col-12 col-lg-6 offset-lg-3 mb-4"> <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 <PrivateMessageForm
onCreate={this.handlePrivateMessageCreate} onCreate={this.handlePrivateMessageCreate}
recipient={res.person_view.person} recipient={res.person_view.person}

View file

@ -1,6 +1,6 @@
import { myAuthRequired } from "@utils/app"; import { myAuthRequired } from "@utils/app";
import { capitalizeFirstLetter } from "@utils/helpers"; import { capitalizeFirstLetter } from "@utils/helpers";
import { Component, InfernoNode, linkEvent } from "inferno"; import { Component, InfernoNode } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { import {
CreatePrivateMessage, CreatePrivateMessage,
@ -11,7 +11,7 @@ import {
import { relTags } from "../../config"; import { relTags } from "../../config";
import { I18NextService } from "../../services"; import { I18NextService } from "../../services";
import { setupTippy } from "../../tippy"; import { setupTippy } from "../../tippy";
import { Icon, Spinner } from "../common/icon"; import { Icon } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
import NavigationPrompt from "../common/navigation-prompt"; import NavigationPrompt from "../common/navigation-prompt";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
@ -19,6 +19,7 @@ import { PersonListing } from "../person/person-listing";
interface PrivateMessageFormProps { interface PrivateMessageFormProps {
recipient: Person; recipient: Person;
privateMessageView?: PrivateMessageView; // If a pm is given, that means this is an edit privateMessageView?: PrivateMessageView; // If a pm is given, that means this is an edit
replyType?: boolean;
onCancel?(): any; onCancel?(): any;
onCreate?(form: CreatePrivateMessage): void; onCreate?(form: CreatePrivateMessage): void;
onEdit?(form: EditPrivateMessage): void; onEdit?(form: EditPrivateMessage): void;
@ -28,7 +29,6 @@ interface PrivateMessageFormState {
content?: string; content?: string;
loading: boolean; loading: boolean;
previewMode: boolean; previewMode: boolean;
showDisclaimer: boolean;
submitted: boolean; submitted: boolean;
} }
@ -39,7 +39,6 @@ export class PrivateMessageForm extends Component<
state: PrivateMessageFormState = { state: PrivateMessageFormState = {
loading: false, loading: false,
previewMode: false, previewMode: false,
showDisclaimer: false,
content: this.props.privateMessageView content: this.props.privateMessageView
? this.props.privateMessageView.private_message.content ? this.props.privateMessageView.private_message.content
: undefined, : undefined,
@ -71,98 +70,60 @@ export class PrivateMessageForm extends Component<
render() { render() {
return ( return (
<form <form className="private-message-form">
className="private-message-form"
onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}
>
<NavigationPrompt <NavigationPrompt
when={ when={
!this.state.loading && !!this.state.content && !this.state.submitted !this.state.loading && !!this.state.content && !this.state.submitted
} }
/> />
{!this.props.privateMessageView && ( {!this.props.privateMessageView && (
<div className="mb-3 row"> <div className="mb-3 row align-items-baseline">
<label className="col-sm-2 col-form-label"> <label className="col-sm-2 col-form-label">
{capitalizeFirstLetter(I18NextService.i18n.t("to"))} {capitalizeFirstLetter(I18NextService.i18n.t("to"))}
</label> </label>
<div className="col-sm-10 form-control-plaintext"> <div className="col-sm-10">
<PersonListing person={this.props.recipient} /> <PersonListing person={this.props.recipient} />
</div> </div>
</div> </div>
)} )}
<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"
rel={relTags}
href="https://element.io/get-started"
>
#
</a>
</T>
</div>
<div className="mb-3 row"> <div className="mb-3 row">
<label className="col-sm-2 col-form-label"> <label className="col-sm-2 col-form-label">
{I18NextService.i18n.t("message")} {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> </label>
<div className="col-sm-10"> <div className="col-sm-10">
<MarkdownTextArea <MarkdownTextArea
onSubmit={() => {
this.handlePrivateMessageSubmit(this, event);
}}
initialContent={this.state.content} initialContent={this.state.content}
onContentChange={this.handleContentChange} onContentChange={this.handleContentChange}
allLanguages={[]} allLanguages={[]}
siteLanguages={[]} siteLanguages={[]}
hideNavigationWarnings 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>
</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">
#
<a
className="alert-link"
rel={relTags}
href="https://element.io/get-started"
>
#
</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>
</div>
</div>
</form> </form>
); );
} }
@ -200,8 +161,4 @@ export class PrivateMessageForm extends Component<
event.preventDefault(); event.preventDefault();
i.setState({ previewMode: !i.state.previewMode }); 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"> <li className="list-inline-item">
<button <button
type="button"
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleMarkRead)} onClick={linkEvent(this, this.handleMarkRead)}
data-tippy-content={ 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">{this.reportButton}</li>
<li className="list-inline-item"> <li className="list-inline-item">
<button <button
type="button"
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleReplyClick)} onClick={linkEvent(this, this.handleReplyClick)}
data-tippy-content={I18NextService.i18n.t("reply")} data-tippy-content={I18NextService.i18n.t("reply")}
@ -188,6 +190,7 @@ export class PrivateMessage extends Component<
<> <>
<li className="list-inline-item"> <li className="list-inline-item">
<button <button
type="button"
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleEditClick)} onClick={linkEvent(this, this.handleEditClick)}
data-tippy-content={I18NextService.i18n.t("edit")} data-tippy-content={I18NextService.i18n.t("edit")}
@ -198,6 +201,7 @@ export class PrivateMessage extends Component<
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<button <button
type="button"
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleDeleteClick)} onClick={linkEvent(this, this.handleDeleteClick)}
data-tippy-content={ data-tippy-content={
@ -228,6 +232,7 @@ export class PrivateMessage extends Component<
)} )}
<li className="list-inline-item"> <li className="list-inline-item">
<button <button
type="button"
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleViewSource)} onClick={linkEvent(this, this.handleViewSource)}
data-tippy-content={I18NextService.i18n.t("view_source")} data-tippy-content={I18NextService.i18n.t("view_source")}
@ -276,10 +281,17 @@ export class PrivateMessage extends Component<
</form> </form>
)} )}
{this.state.showReply && ( {this.state.showReply && (
<PrivateMessageForm <div className="row">
recipient={otherPerson} <div className="col-sm-6">
onCreate={this.props.onCreate} <PrivateMessageForm
/> privateMessageView={message_view}
replyType={true}
recipient={otherPerson}
onCreate={this.props.onCreate}
onCancel={this.handleReplyCancel}
/>
</div>
</div>
)} )}
{/* A collapsed clearfix */} {/* A collapsed clearfix */}
{this.state.collapsed && <div className="row col-12"></div>} {this.state.collapsed && <div className="row col-12"></div>}
@ -290,6 +302,7 @@ export class PrivateMessage extends Component<
get reportButton() { get reportButton() {
return ( return (
<button <button
type="button"
className="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleShowReportDialog)} onClick={linkEvent(this, this.handleShowReportDialog)}
data-tippy-content={I18NextService.i18n.t("show_report_dialog")} data-tippy-content={I18NextService.i18n.t("show_report_dialog")}