Fix some submit button issues (#2487)

* Prevent PostForm submit button spam

* Keep CreatePost PostForm visible during submission

* Keep PostListing PostForm visible during submission

* Keep PostForm navigation warning enabled during submission

* Remove `finished` from MarkdownTextAreaProps

* Handle CommentForm submission failures

* Keep CommentForm navigation warning enabled during submission

* Handle PrivateMessageForm submission failures

* Bypass navigation warning for successful CreatePrivateMessage

* Fix absolute import, add eslint rule

* Cleaner handleCommentSubmit

---------

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
This commit is contained in:
matc-pub 2024-06-02 17:46:32 +02:00 committed by GitHub
parent 14ae45fe95
commit 02fcfa26ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 270 additions and 251 deletions

View file

@ -79,6 +79,17 @@ export default [
"unicorn/filename-case": 0, "unicorn/filename-case": 0,
"jsx-a11y/media-has-caption": 0, "jsx-a11y/media-has-caption": 0,
"jsx-a11y/label-has-associated-control": 0, "jsx-a11y/label-has-associated-control": 0,
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["assets/*", "client/*", "server/*", "shared/*"],
message: "Use relative import instead.",
},
],
},
],
}, },
}, },
]; ];

View file

@ -2,18 +2,23 @@ import { capitalizeFirstLetter } from "@utils/helpers";
import { Component } from "inferno"; import { Component } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { CreateComment, EditComment, Language } from "lemmy-js-client"; import {
CommentResponse,
CreateComment,
EditComment,
Language,
} from "lemmy-js-client";
import { CommentNodeI } from "../../interfaces"; import { CommentNodeI } from "../../interfaces";
import { I18NextService, UserService } from "../../services"; import { I18NextService, UserService } from "../../services";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
import { RequestState } from "../../services/HttpService";
interface CommentFormProps { interface CommentFormProps {
/** /**
* Can either be the parent, or the editable comment. The right side is a postId. * Can either be the parent, or the editable comment. The right side is a postId.
*/ */
node: CommentNodeI | number; node: CommentNodeI | number;
finished?: boolean;
edit?: boolean; edit?: boolean;
disabled?: boolean; disabled?: boolean;
focus?: boolean; focus?: boolean;
@ -21,7 +26,9 @@ interface CommentFormProps {
allLanguages: Language[]; allLanguages: Language[];
siteLanguages: number[]; siteLanguages: number[];
containerClass?: string; containerClass?: string;
onUpsertComment(form: EditComment | CreateComment): void; onUpsertComment(
form: EditComment | CreateComment,
): Promise<RequestState<CommentResponse>>;
} }
export class CommentForm extends Component<CommentFormProps, any> { export class CommentForm extends Component<CommentFormProps, any> {
@ -50,7 +57,6 @@ export class CommentForm extends Component<CommentFormProps, any> {
initialContent={initialContent} initialContent={initialContent}
showLanguage showLanguage
buttonTitle={this.buttonTitle} buttonTitle={this.buttonTitle}
finished={this.props.finished}
replyType={typeof this.props.node !== "number"} replyType={typeof this.props.node !== "number"}
focus={this.props.focus} focus={this.props.focus}
disabled={this.props.disabled} disabled={this.props.disabled}
@ -83,33 +89,38 @@ export class CommentForm extends Component<CommentFormProps, any> {
: capitalizeFirstLetter(I18NextService.i18n.t("reply")); : capitalizeFirstLetter(I18NextService.i18n.t("reply"));
} }
handleCommentSubmit(content: string, language_id?: number) { async handleCommentSubmit(
content: string,
language_id?: number,
): Promise<boolean> {
const { node, onUpsertComment, edit } = this.props; const { node, onUpsertComment, edit } = this.props;
let response: RequestState<CommentResponse>;
if (typeof node === "number") { if (typeof node === "number") {
const post_id = node; const post_id = node;
onUpsertComment({ response = await onUpsertComment({
content, content,
post_id, post_id,
language_id, language_id,
}); });
} else if (edit) {
const comment_id = node.comment_view.comment.id;
response = await onUpsertComment({
content,
comment_id,
language_id,
});
} else { } else {
if (edit) { const post_id = node.comment_view.post.id;
const comment_id = node.comment_view.comment.id; const parent_id = node.comment_view.comment.id;
onUpsertComment({ response = await onUpsertComment({
content, content,
comment_id, parent_id,
language_id, post_id,
}); language_id,
} else { });
const post_id = node.comment_view.post.id;
const parent_id = node.comment_view.comment.id;
this.props.onUpsertComment({
content,
parent_id,
post_id,
language_id,
});
}
} }
return response.state !== "failed";
} }
} }

View file

@ -2,7 +2,7 @@ import { colorList, getCommentParentId } from "@utils/app";
import { futureDaysToUnixTime, numToSI } from "@utils/helpers"; import { futureDaysToUnixTime, numToSI } from "@utils/helpers";
import classNames from "classnames"; import classNames from "classnames";
import { isBefore, parseISO, subMinutes } from "date-fns"; import { isBefore, parseISO, subMinutes } from "date-fns";
import { Component, InfernoNode, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { import {
AddAdmin, AddAdmin,
@ -33,7 +33,6 @@ import {
SaveComment, SaveComment,
TransferCommunity, TransferCommunity,
} from "lemmy-js-client"; } from "lemmy-js-client";
import deepEqual from "lodash.isequal";
import { commentTreeMaxDepth } from "../../config"; import { commentTreeMaxDepth } from "../../config";
import { import {
CommentNodeI, CommentNodeI,
@ -87,7 +86,6 @@ interface CommentNodeProps {
allLanguages: Language[]; allLanguages: Language[];
siteLanguages: number[]; siteLanguages: number[];
hideImages?: boolean; hideImages?: boolean;
finished: Map<CommentId, boolean | undefined>;
onSaveComment(form: SaveComment): Promise<void>; onSaveComment(form: SaveComment): Promise<void>;
onCommentReplyRead(form: MarkCommentReplyAsRead): void; onCommentReplyRead(form: MarkCommentReplyAsRead): void;
onPersonMentionRead(form: MarkPersonMentionAsRead): void; onPersonMentionRead(form: MarkPersonMentionAsRead): void;
@ -139,6 +137,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
super(props, context); super(props, context);
this.handleReplyCancel = this.handleReplyCancel.bind(this); this.handleReplyCancel = this.handleReplyCancel.bind(this);
this.handleCreateComment = this.handleCreateComment.bind(this);
this.handleEditComment = this.handleEditComment.bind(this);
this.handleReportComment = this.handleReportComment.bind(this); this.handleReportComment = this.handleReportComment.bind(this);
this.handleRemoveComment = this.handleRemoveComment.bind(this); this.handleRemoveComment = this.handleRemoveComment.bind(this);
this.handleReplyClick = this.handleReplyClick.bind(this); this.handleReplyClick = this.handleReplyClick.bind(this);
@ -164,22 +164,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
return this.commentView.comment.id; return this.commentView.comment.id;
} }
componentWillReceiveProps(
nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>,
): void {
if (!deepEqual(this.props, nextProps)) {
this.setState({
showEdit: false,
showAdvanced: false,
createOrEditCommentLoading: false,
upvoteLoading: false,
downvoteLoading: false,
readLoading: false,
fetchChildrenLoading: false,
});
}
}
render() { render() {
const node = this.props.node; const node = this.props.node;
const cv = this.commentView; const cv = this.commentView;
@ -283,12 +267,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
edit edit
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked} disabled={this.props.locked}
finished={this.props.finished.get(id)}
focus focus
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages} siteLanguages={this.props.siteLanguages}
containerClass="comment-comment-container" containerClass="comment-comment-container"
onUpsertComment={this.props.onEditComment} onUpsertComment={this.handleEditComment}
/> />
)} )}
{!this.state.showEdit && !this.state.collapsed && ( {!this.state.showEdit && !this.state.collapsed && (
@ -425,12 +408,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
node={node} node={node}
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked} disabled={this.props.locked}
finished={this.props.finished.get(id)}
focus focus
allLanguages={this.props.allLanguages} allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages} siteLanguages={this.props.siteLanguages}
containerClass="comment-comment-container" containerClass="comment-comment-container"
onUpsertComment={this.props.onCreateComment} onUpsertComment={this.handleCreateComment}
/> />
)} )}
{!this.state.collapsed && node.children.length > 0 && ( {!this.state.collapsed && node.children.length > 0 && (
@ -447,7 +429,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
hideImages={this.props.hideImages} hideImages={this.props.hideImages}
isChild={!this.props.isTopLevel} isChild={!this.props.isTopLevel}
depth={this.props.node.depth + 1} depth={this.props.node.depth + 1}
finished={this.props.finished}
onCommentReplyRead={this.props.onCommentReplyRead} onCommentReplyRead={this.props.onCommentReplyRead}
onPersonMentionRead={this.props.onPersonMentionRead} onPersonMentionRead={this.props.onPersonMentionRead}
onCreateComment={this.props.onCreateComment} onCreateComment={this.props.onCreateComment}
@ -559,6 +540,26 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
this.setState({ showReply: false, showEdit: false }); this.setState({ showReply: false, showEdit: false });
} }
async handleCreateComment(
form: CreateComment,
): Promise<RequestState<CommentResponse>> {
const res = await this.props.onCreateComment(form);
if (res.state !== "failed") {
this.setState({ showReply: false, showEdit: false });
}
return res;
}
async handleEditComment(
form: EditComment,
): Promise<RequestState<CommentResponse>> {
const res = await this.props.onEditComment(form);
if (res.state !== "failed") {
this.setState({ showReply: false, showEdit: false });
}
return res;
}
isPersonMentionType(item: CommentNodeView): item is PersonMentionView { isPersonMentionType(item: CommentNodeView): item is PersonMentionView {
return item.person_mention?.id !== undefined; return item.person_mention?.id !== undefined;
} }

View file

@ -7,7 +7,6 @@ import {
BanFromCommunity, BanFromCommunity,
BanPerson, BanPerson,
BlockPerson, BlockPerson,
CommentId,
CommentResponse, CommentResponse,
CommunityModeratorView, CommunityModeratorView,
CreateComment, CreateComment,
@ -52,7 +51,6 @@ interface CommentNodesProps {
hideImages?: boolean; hideImages?: boolean;
isChild?: boolean; isChild?: boolean;
depth?: number; depth?: number;
finished: Map<CommentId, boolean | undefined>;
onSaveComment(form: SaveComment): Promise<void>; onSaveComment(form: SaveComment): Promise<void>;
onCommentReplyRead(form: MarkCommentReplyAsRead): void; onCommentReplyRead(form: MarkCommentReplyAsRead): void;
onPersonMentionRead(form: MarkPersonMentionAsRead): void; onPersonMentionRead(form: MarkPersonMentionAsRead): void;
@ -124,7 +122,6 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
hideImages={this.props.hideImages} hideImages={this.props.hideImages}
onCommentReplyRead={this.props.onCommentReplyRead} onCommentReplyRead={this.props.onCommentReplyRead}
onPersonMentionRead={this.props.onPersonMentionRead} onPersonMentionRead={this.props.onPersonMentionRead}
finished={this.props.finished}
onCreateComment={this.props.onCreateComment} onCreateComment={this.props.onCreateComment}
onEditComment={this.props.onEditComment} onEditComment={this.props.onEditComment}
onCommentVote={this.props.onCommentVote} onCommentVote={this.props.onCommentVote}

View file

@ -90,7 +90,6 @@ export class CommentReport extends Component<
siteLanguages={[]} siteLanguages={[]}
hideImages hideImages
// All of these are unused, since its viewonly // All of these are unused, since its viewonly
finished={new Map()}
onSaveComment={async () => {}} onSaveComment={async () => {}}
onBlockPerson={async () => {}} onBlockPerson={async () => {}}
onDeleteComment={async () => {}} onDeleteComment={async () => {}}

View file

@ -3,7 +3,7 @@ import { numToSI, randomStr } from "@utils/helpers";
import autosize from "autosize"; import autosize from "autosize";
import classNames from "classnames"; import classNames from "classnames";
import { NoOptionI18nKeys } from "i18next"; import { NoOptionI18nKeys } from "i18next";
import { Component, InfernoNode, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Prompt } from "inferno-router"; import { Prompt } from "inferno-router";
import { Language } from "lemmy-js-client"; import { Language } from "lemmy-js-client";
import { import {
@ -41,7 +41,6 @@ interface MarkdownTextAreaProps {
replyType?: boolean; replyType?: boolean;
focus?: boolean; focus?: boolean;
disabled?: boolean; disabled?: boolean;
finished?: boolean;
/** /**
* Whether to show the language selector * Whether to show the language selector
*/ */
@ -49,7 +48,7 @@ interface MarkdownTextAreaProps {
hideNavigationWarnings?: boolean; hideNavigationWarnings?: boolean;
onContentChange?(val: string): void; onContentChange?(val: string): void;
onReplyCancel?(): void; onReplyCancel?(): void;
onSubmit?(content: string, languageId?: number): void; onSubmit?(content: string, languageId?: number): Promise<boolean>;
allLanguages: Language[]; // TODO should probably be nullable allLanguages: Language[]; // TODO should probably be nullable
siteLanguages: number[]; // TODO same siteLanguages: number[]; // TODO same
} }
@ -115,27 +114,6 @@ export class MarkdownTextArea extends Component<
} }
} }
componentWillReceiveProps(
nextProps: MarkdownTextAreaProps & { children?: InfernoNode },
) {
if (nextProps.finished) {
this.setState({
previewMode: false,
imageUploadStatus: undefined,
loading: false,
content: undefined,
});
if (this.props.replyType) {
this.props.onReplyCancel?.();
}
const textarea: any = document.getElementById(this.id);
const form: any = document.getElementById(this.formId);
form.reset();
setTimeout(() => autosize.update(textarea), 10);
}
}
render() { render() {
const languageId = this.state.languageId; const languageId = this.state.languageId;
@ -149,8 +127,8 @@ export class MarkdownTextArea extends Component<
message={I18NextService.i18n.t("block_leaving")} message={I18NextService.i18n.t("block_leaving")}
when={ when={
!this.props.hideNavigationWarnings && !this.props.hideNavigationWarnings &&
!!this.state.content && ((!!this.state.content && !this.state.submitted) ||
!this.state.submitted this.state.loading)
} }
/> />
<div className="mb-3 row"> <div className="mb-3 row">
@ -575,11 +553,15 @@ export class MarkdownTextArea extends Component<
this.setState({ languageId: val[0] }); this.setState({ languageId: val[0] });
} }
handleSubmit(i: MarkdownTextArea, event: any) { async handleSubmit(i: MarkdownTextArea, event: any) {
event.preventDefault(); event.preventDefault();
if (i.state.content) { if (i.state.content) {
i.setState({ loading: true, submitted: true }); i.setState({ loading: true, submitted: true });
i.props.onSubmit?.(i.state.content, i.state.languageId); const success = await i.props.onSubmit?.(
i.state.content,
i.state.languageId,
);
i.setState({ loading: false, submitted: success ?? true });
} }
} }

View file

@ -6,7 +6,6 @@ import {
editWith, editWith,
enableDownvotes, enableDownvotes,
enableNsfw, enableNsfw,
getCommentParentId,
getDataTypeString, getDataTypeString,
postToCommentSortType, postToCommentSortType,
setIsoData, setIsoData,
@ -42,7 +41,6 @@ import {
BanPersonResponse, BanPersonResponse,
BlockCommunity, BlockCommunity,
BlockPerson, BlockPerson,
CommentId,
CommentReplyResponse, CommentReplyResponse,
CommentResponse, CommentResponse,
CommunityResponse, CommunityResponse,
@ -136,7 +134,6 @@ interface State {
commentsRes: RequestState<GetCommentsResponse>; commentsRes: RequestState<GetCommentsResponse>;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
showSidebarMobile: boolean; showSidebarMobile: boolean;
finished: Map<CommentId, boolean | undefined>;
isIsomorphic: boolean; isIsomorphic: boolean;
} }
@ -201,7 +198,6 @@ export class Community extends Component<CommunityRouteProps, State> {
commentsRes: EMPTY_REQUEST, commentsRes: EMPTY_REQUEST,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
showSidebarMobile: false, showSidebarMobile: false,
finished: new Map(),
isIsomorphic: false, isIsomorphic: false,
}; };
private readonly mainContentRef: RefObject<HTMLElement>; private readonly mainContentRef: RefObject<HTMLElement>;
@ -528,7 +524,6 @@ export class Community extends Component<CommunityRouteProps, State> {
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(this.state.commentsRes.data.comments)} nodes={commentsToFlatNodes(this.state.commentsRes.data.comments)}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
finished={this.state.finished}
isTopLevel isTopLevel
showContext showContext
enableDownvotes={enableDownvotes(siteRes)} enableDownvotes={enableDownvotes(siteRes)}
@ -820,6 +815,9 @@ export class Community extends Component<CommunityRouteProps, State> {
const createCommentRes = await HttpService.client.createComment(form); const createCommentRes = await HttpService.client.createComment(form);
this.createAndUpdateComments(createCommentRes); this.createAndUpdateComments(createCommentRes);
if (createCommentRes.state === "failed") {
toast(I18NextService.i18n.t(createCommentRes.err.message), "danger");
}
return createCommentRes; return createCommentRes;
} }
@ -827,6 +825,9 @@ export class Community extends Component<CommunityRouteProps, State> {
const editCommentRes = await HttpService.client.editComment(form); const editCommentRes = await HttpService.client.editComment(form);
this.findAndUpdateCommentEdit(editCommentRes); this.findAndUpdateCommentEdit(editCommentRes);
if (editCommentRes.state === "failed") {
toast(I18NextService.i18n.t(editCommentRes.err.message), "danger");
}
return editCommentRes; return editCommentRes;
} }
@ -1038,7 +1039,6 @@ export class Community extends Component<CommunityRouteProps, State> {
res.data.comment_view, res.data.comment_view,
s.commentsRes.data.comments, s.commentsRes.data.comments,
); );
s.finished.set(res.data.comment_view.comment.id, true);
} }
return s; return s;
}); });
@ -1060,12 +1060,6 @@ export class Community extends Component<CommunityRouteProps, State> {
this.setState(s => { this.setState(s => {
if (s.commentsRes.state === "success" && res.state === "success") { if (s.commentsRes.state === "success" && res.state === "success") {
s.commentsRes.data.comments.unshift(res.data.comment_view); s.commentsRes.data.comments.unshift(res.data.comment_view);
// Set finished for the parent
s.finished.set(
getCommentParentId(res.data.comment_view.comment) ?? 0,
true,
);
} }
return s; return s;
}); });

View file

@ -5,7 +5,6 @@ import {
editWith, editWith,
enableDownvotes, enableDownvotes,
enableNsfw, enableNsfw,
getCommentParentId,
getDataTypeString, getDataTypeString,
myAuth, myAuth,
postToCommentSortType, postToCommentSortType,
@ -37,7 +36,6 @@ import {
BanPerson, BanPerson,
BanPersonResponse, BanPersonResponse,
BlockPerson, BlockPerson,
CommentId,
CommentReplyResponse, CommentReplyResponse,
CommentResponse, CommentResponse,
CreateComment, CreateComment,
@ -125,7 +123,6 @@ interface HomeState {
subscribedCollapsed: boolean; subscribedCollapsed: boolean;
tagline?: string; tagline?: string;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
finished: Map<CommentId, boolean | undefined>;
isIsomorphic: boolean; isIsomorphic: boolean;
} }
@ -274,7 +271,6 @@ export class Home extends Component<HomeRouteProps, HomeState> {
showTrendingMobile: false, showTrendingMobile: false,
showSidebarMobile: false, showSidebarMobile: false,
subscribedCollapsed: false, subscribedCollapsed: false,
finished: new Map(),
isIsomorphic: false, isIsomorphic: false,
}; };
@ -770,7 +766,6 @@ export class Home extends Component<HomeRouteProps, HomeState> {
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(comments)} nodes={commentsToFlatNodes(comments)}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
finished={this.state.finished}
isTopLevel isTopLevel
showCommunity showCommunity
showContext showContext
@ -973,6 +968,9 @@ export class Home extends Component<HomeRouteProps, HomeState> {
const createCommentRes = await HttpService.client.createComment(form); const createCommentRes = await HttpService.client.createComment(form);
this.createAndUpdateComments(createCommentRes); this.createAndUpdateComments(createCommentRes);
if (createCommentRes.state === "failed") {
toast(I18NextService.i18n.t(createCommentRes.err.message), "danger");
}
return createCommentRes; return createCommentRes;
} }
@ -980,6 +978,9 @@ export class Home extends Component<HomeRouteProps, HomeState> {
const editCommentRes = await HttpService.client.editComment(form); const editCommentRes = await HttpService.client.editComment(form);
this.findAndUpdateCommentEdit(editCommentRes); this.findAndUpdateCommentEdit(editCommentRes);
if (editCommentRes.state === "failed") {
toast(I18NextService.i18n.t(editCommentRes.err.message), "danger");
}
return editCommentRes; return editCommentRes;
} }
@ -1168,7 +1169,6 @@ export class Home extends Component<HomeRouteProps, HomeState> {
res.data.comment_view, res.data.comment_view,
s.commentsRes.data.comments, s.commentsRes.data.comments,
); );
s.finished.set(res.data.comment_view.comment.id, true);
} }
return s; return s;
}); });
@ -1190,12 +1190,6 @@ export class Home extends Component<HomeRouteProps, HomeState> {
this.setState(s => { this.setState(s => {
if (s.commentsRes.state === "success" && res.state === "success") { if (s.commentsRes.state === "success" && res.state === "success") {
s.commentsRes.data.comments.unshift(res.data.comment_view); s.commentsRes.data.comments.unshift(res.data.comment_view);
// Set finished for the parent
s.finished.set(
getCommentParentId(res.data.comment_view.comment) ?? 0,
true,
);
} }
return s; return s;
}); });

View file

@ -5,7 +5,6 @@ import {
editPrivateMessage, editPrivateMessage,
editWith, editWith,
enableDownvotes, enableDownvotes,
getCommentParentId,
myAuth, myAuth,
setIsoData, setIsoData,
updatePersonBlock, updatePersonBlock,
@ -28,7 +27,6 @@ import {
BanPerson, BanPerson,
BanPersonResponse, BanPersonResponse,
BlockPerson, BlockPerson,
CommentId,
CommentReplyResponse, CommentReplyResponse,
CommentReplyView, CommentReplyView,
CommentReportResponse, CommentReportResponse,
@ -132,7 +130,6 @@ interface InboxState {
sort: CommentSortType; sort: CommentSortType;
page: number; page: number;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
finished: Map<CommentId, boolean | undefined>;
isIsomorphic: boolean; isIsomorphic: boolean;
} }
@ -157,7 +154,6 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
mentionsRes: EMPTY_REQUEST, mentionsRes: EMPTY_REQUEST,
messagesRes: EMPTY_REQUEST, messagesRes: EMPTY_REQUEST,
markAllAsReadRes: EMPTY_REQUEST, markAllAsReadRes: EMPTY_REQUEST,
finished: new Map(),
isIsomorphic: false, isIsomorphic: false,
}; };
@ -512,7 +508,6 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
{ comment_view: i.view as CommentView, children: [], depth: 0 }, { comment_view: i.view as CommentView, children: [], depth: 0 },
]} ]}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
finished={this.state.finished}
markable markable
showCommunity showCommunity
showContext showContext
@ -551,7 +546,6 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
depth: 0, depth: 0,
}, },
]} ]}
finished={this.state.finished}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
markable markable
showCommunity showCommunity
@ -623,7 +617,6 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(replies)} nodes={commentsToFlatNodes(replies)}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
finished={this.state.finished}
markable markable
showCommunity showCommunity
showContext showContext
@ -670,7 +663,6 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
key={umv.person_mention.id} key={umv.person_mention.id}
nodes={[{ comment_view: umv, children: [], depth: 0 }]} nodes={[{ comment_view: umv, children: [], depth: 0 }]}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
finished={this.state.finished}
markable markable
showCommunity showCommunity
showContext showContext
@ -996,9 +988,13 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
this.findAndUpdateMessage(res); this.findAndUpdateMessage(res);
} }
async handleEditMessage(form: EditPrivateMessage) { async handleEditMessage(form: EditPrivateMessage): Promise<boolean> {
const res = await HttpService.client.editPrivateMessage(form); const res = await HttpService.client.editPrivateMessage(form);
this.findAndUpdateMessage(res); this.findAndUpdateMessage(res);
if (res.state === "failed") {
toast(I18NextService.i18n.t(res.err.message), "danger");
}
return res.state !== "failed";
} }
async handleMarkMessageAsRead(form: MarkPrivateMessageAsRead) { async handleMarkMessageAsRead(form: MarkPrivateMessageAsRead) {
@ -1015,7 +1011,7 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
this.reportToast(res); this.reportToast(res);
} }
async handleCreateMessage(form: CreatePrivateMessage) { async handleCreateMessage(form: CreatePrivateMessage): Promise<boolean> {
const res = await HttpService.client.createPrivateMessage(form); const res = await HttpService.client.createPrivateMessage(form);
this.setState(s => { this.setState(s => {
if (s.messagesRes.state === "success" && res.state === "success") { if (s.messagesRes.state === "success" && res.state === "success") {
@ -1026,6 +1022,10 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
return s; return s;
}); });
if (res.state === "failed") {
toast(I18NextService.i18n.t(res.err.message), "danger");
}
return res.state !== "failed";
} }
findAndUpdateMessage(res: RequestState<PrivateMessageResponse>) { findAndUpdateMessage(res: RequestState<PrivateMessageResponse>) {
@ -1094,6 +1094,8 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
) { ) {
if (res.state === "success") { if (res.state === "success") {
toast(I18NextService.i18n.t("report_created")); toast(I18NextService.i18n.t("report_created"));
} else if (res.state === "failed") {
toast(I18NextService.i18n.t(res.err.message), "danger");
} }
} }
@ -1113,11 +1115,6 @@ export class Inbox extends Component<InboxRouteProps, InboxState> {
s.mentionsRes.data.mentions, s.mentionsRes.data.mentions,
); );
} }
// Set finished for the parent
s.finished.set(
getCommentParentId(res.data.comment_view.comment) ?? 0,
true,
);
return s; return s;
}); });
} }

View file

@ -6,7 +6,6 @@ import {
BanFromCommunity, BanFromCommunity,
BanPerson, BanPerson,
BlockPerson, BlockPerson,
CommentId,
CommentResponse, CommentResponse,
CommentView, CommentView,
CreateComment, CreateComment,
@ -49,7 +48,6 @@ import { RequestState } from "../../services/HttpService";
interface PersonDetailsProps { interface PersonDetailsProps {
personRes: GetPersonDetailsResponse; personRes: GetPersonDetailsResponse;
finished: Map<CommentId, boolean | undefined>;
admins: PersonView[]; admins: PersonView[];
allLanguages: Language[]; allLanguages: Language[];
siteLanguages: number[]; siteLanguages: number[];
@ -153,7 +151,6 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
key={i.id} key={i.id}
nodes={[{ comment_view: c, children: [], depth: 0 }]} nodes={[{ comment_view: c, children: [], depth: 0 }]}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
finished={this.props.finished}
admins={this.props.admins} admins={this.props.admins}
noBorder noBorder
showCommunity showCommunity
@ -266,7 +263,6 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
nodes={commentsToFlatNodes(this.props.personRes.comments)} nodes={commentsToFlatNodes(this.props.personRes.comments)}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
admins={this.props.admins} admins={this.props.admins}
finished={this.props.finished}
isTopLevel isTopLevel
showCommunity showCommunity
showContext showContext

View file

@ -4,7 +4,6 @@ import {
editWith, editWith,
enableDownvotes, enableDownvotes,
enableNsfw, enableNsfw,
getCommentParentId,
setIsoData, setIsoData,
updatePersonBlock, updatePersonBlock,
voteDisplayMode, voteDisplayMode,
@ -38,7 +37,6 @@ import {
BanPerson, BanPerson,
BanPersonResponse, BanPersonResponse,
BlockPerson, BlockPerson,
CommentId,
CommentReplyResponse, CommentReplyResponse,
CommentResponse, CommentResponse,
Community, Community,
@ -120,7 +118,6 @@ interface ProfileState {
showBanDialog: boolean; showBanDialog: boolean;
removeData: boolean; removeData: boolean;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
finished: Map<CommentId, boolean | undefined>;
isIsomorphic: boolean; isIsomorphic: boolean;
} }
@ -206,7 +203,6 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
showBanDialog: false, showBanDialog: false,
removeData: false, removeData: false,
finished: new Map(),
isIsomorphic: false, isIsomorphic: false,
}; };
@ -491,7 +487,6 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
sort={sort} sort={sort}
page={page} page={page}
limit={fetchLimit} limit={fetchLimit}
finished={this.state.finished}
enableDownvotes={enableDownvotes(siteRes)} enableDownvotes={enableDownvotes(siteRes)}
voteDisplayMode={voteDisplayMode(siteRes)} voteDisplayMode={voteDisplayMode(siteRes)}
enableNsfw={enableNsfw(siteRes)} enableNsfw={enableNsfw(siteRes)}
@ -1178,7 +1173,6 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
res.data.comment_view, res.data.comment_view,
s.personRes.data.comments, s.personRes.data.comments,
); );
s.finished.set(res.data.comment_view.comment.id, true);
} }
return s; return s;
}); });
@ -1200,11 +1194,6 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
this.setState(s => { this.setState(s => {
if (s.personRes.state === "success" && res.state === "success") { if (s.personRes.state === "success" && res.state === "success") {
s.personRes.data.comments.unshift(res.data.comment_view); s.personRes.data.comments.unshift(res.data.comment_view);
// Set finished for the parent
s.finished.set(
getCommentParentId(res.data.comment_view.comment) ?? 0,
true,
);
} }
return s; return s;
}); });

View file

@ -27,7 +27,6 @@ import {
wrapClient, wrapClient,
} from "../../services/HttpService"; } from "../../services/HttpService";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
import { PostForm } from "./post-form"; import { PostForm } from "./post-form";
import { getHttpBaseInternal } from "../../utils/env"; import { getHttpBaseInternal } from "../../utils/env";
import { IRoutePropsWithFetch } from "../../routes"; import { IRoutePropsWithFetch } from "../../routes";
@ -178,39 +177,28 @@ export class CreatePost extends Component<
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
/> />
{this.state.loading ? ( <div className="row">
<h5> <div id="createPostForm" className="col-12 col-lg-6 offset-lg-3 mb-4">
<Spinner large /> <h1 className="h4 mb-4">{I18NextService.i18n.t("create_post")}</h1>
</h5> <PostForm
) : ( onCreate={this.handlePostCreate}
<div className="row"> params={locationState}
<div enableDownvotes={enableDownvotes(siteRes)}
id="createPostForm" voteDisplayMode={voteDisplayMode(siteRes)}
className="col-12 col-lg-6 offset-lg-3 mb-4" enableNsfw={enableNsfw(siteRes)}
> allLanguages={siteRes.all_languages}
<h1 className="h4 mb-4"> siteLanguages={siteRes.discussion_languages}
{I18NextService.i18n.t("create_post")} selectedCommunityChoice={selectedCommunityChoice}
</h1> onSelectCommunity={this.handleSelectedCommunityChange}
<PostForm initialCommunities={
onCreate={this.handlePostCreate} this.state.initialCommunitiesRes.state === "success"
params={locationState} ? this.state.initialCommunitiesRes.data.communities
enableDownvotes={enableDownvotes(siteRes)} : []
voteDisplayMode={voteDisplayMode(siteRes)} }
enableNsfw={enableNsfw(siteRes)} loading={loading}
allLanguages={siteRes.all_languages} />
siteLanguages={siteRes.discussion_languages}
selectedCommunityChoice={selectedCommunityChoice}
onSelectCommunity={this.handleSelectedCommunityChange}
initialCommunities={
this.state.initialCommunitiesRes.state === "success"
? this.state.initialCommunitiesRes.data.communities
: []
}
loading={loading}
/>
</div>
</div> </div>
)} </div>
</div> </div>
); );
} }
@ -242,11 +230,13 @@ export class CreatePost extends Component<
}); });
} }
async handlePostCreate(form: CreatePostI) { async handlePostCreate(form: CreatePostI, bypassNavWarning: () => void) {
this.setState({ loading: true });
const res = await HttpService.client.createPost(form); const res = await HttpService.client.createPost(form);
if (res.state === "success") { if (res.state === "success") {
const postId = res.data.post_view.post.id; const postId = res.data.post_view.post.id;
bypassNavWarning();
this.props.history.replace(`/post/${postId}`); this.props.history.replace(`/post/${postId}`);
} else if (res.state === "failed") { } else if (res.state === "failed") {
this.setState({ this.setState({

View file

@ -54,8 +54,8 @@ interface PostFormProps {
siteLanguages: number[]; siteLanguages: number[];
params?: PostFormParams; params?: PostFormParams;
onCancel?(): void; onCancel?(): void;
onCreate?(form: CreatePost): void; onCreate?(form: CreatePost, bypassNavWarning: () => void): void;
onEdit?(form: EditPost): void; onEdit?(form: EditPost, bypassNavWarning: () => void): void;
enableNsfw?: boolean; enableNsfw?: boolean;
enableDownvotes?: boolean; enableDownvotes?: boolean;
voteDisplayMode: LocalUserVoteDisplayMode; voteDisplayMode: LocalUserVoteDisplayMode;
@ -85,6 +85,7 @@ interface PostFormState {
communitySearchOptions: Choice[]; communitySearchOptions: Choice[];
previewMode: boolean; previewMode: boolean;
submitted: boolean; submitted: boolean;
bypassNavWarning: boolean;
} }
function handlePostSubmit(i: PostForm, event: any) { function handlePostSubmit(i: PostForm, event: any) {
@ -93,34 +94,46 @@ function handlePostSubmit(i: PostForm, event: any) {
if ((i.state.form.url ?? "") === "") { if ((i.state.form.url ?? "") === "") {
i.setState(s => ((s.form.url = undefined), s)); i.setState(s => ((s.form.url = undefined), s));
} }
// This forces `props.loading` to become true, then false, to enable the
// submit button again.
i.setState({ submitted: true }); i.setState({ submitted: true });
const pForm = i.state.form; const pForm = i.state.form;
const pv = i.props.post_view; const pv = i.props.post_view;
if (pv) { if (pv) {
i.props.onEdit?.({ i.props.onEdit?.(
post_id: pv.post.id, {
name: pForm.name, post_id: pv.post.id,
url: pForm.url, name: pForm.name,
body: pForm.body, url: pForm.url,
nsfw: pForm.nsfw, body: pForm.body,
language_id: pForm.language_id, nsfw: pForm.nsfw,
custom_thumbnail: pForm.custom_thumbnail, language_id: pForm.language_id,
alt_text: pForm.alt_text, custom_thumbnail: pForm.custom_thumbnail,
}); alt_text: pForm.alt_text,
},
() => {
i.setState({ bypassNavWarning: true });
},
);
} else if (pForm.name && pForm.community_id) { } else if (pForm.name && pForm.community_id) {
i.props.onCreate?.({ i.props.onCreate?.(
name: pForm.name, {
community_id: pForm.community_id, name: pForm.name,
url: pForm.url, community_id: pForm.community_id,
body: pForm.body, url: pForm.url,
nsfw: pForm.nsfw, body: pForm.body,
language_id: pForm.language_id, nsfw: pForm.nsfw,
honeypot: pForm.honeypot, language_id: pForm.language_id,
custom_thumbnail: pForm.custom_thumbnail, honeypot: pForm.honeypot,
alt_text: pForm.alt_text, custom_thumbnail: pForm.custom_thumbnail,
}); alt_text: pForm.alt_text,
},
() => {
i.setState({ bypassNavWarning: true });
},
);
} }
} }
@ -247,6 +260,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
previewMode: false, previewMode: false,
communitySearchOptions: [], communitySearchOptions: [],
submitted: false, submitted: false,
bypassNavWarning: false,
}; };
postTitleRef = createRef<HTMLTextAreaElement>(); postTitleRef = createRef<HTMLTextAreaElement>();
@ -347,6 +361,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
nextProps.initialCommunities?.map(communityToChoice) ?? [], nextProps.initialCommunities?.map(communityToChoice) ?? [],
}); });
} }
if (this.props.loading && !nextProps.loading) {
this.setState({ submitted: false, bypassNavWarning: false });
}
} }
render() { render() {
@ -364,7 +381,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.state.form.name || this.state.form.name ||
this.state.form.url || this.state.form.url ||
this.state.form.body this.state.form.body
) && !this.state.submitted ) && !this.state.bypassNavWarning
} }
/> />
<div className="mb-3 row"> <div className="mb-3 row">
@ -618,7 +635,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<div className="mb-3 row"> <div className="mb-3 row">
<div className="col-sm-10"> <div className="col-sm-10">
<button <button
disabled={!this.state.form.community_id || this.props.loading} disabled={
!this.state.form.community_id ||
this.props.loading ||
this.state.submitted
}
type="submit" type="submit"
className="btn btn-secondary me-2" className="btn btn-secondary me-2"
> >

View file

@ -842,13 +842,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
// The actual editing is done in the receive for post // The actual editing is done in the receive for post
async handleEditPost(form: EditPost) { async handleEditPost(form: EditPost) {
this.setState({ showEdit: false, loading: true }); this.setState({ loading: true });
const res = await this.props.onPostEdit(form); const res = await this.props.onPostEdit(form);
if (res.state === "success") { if (res.state === "success") {
toast(I18NextService.i18n.t("edited_post")); toast(I18NextService.i18n.t("edited_post"));
this.setState({ loading: false, showEdit: false });
} else if (res.state === "failed") { } else if (res.state === "failed") {
toast(I18NextService.i18n.t(res.err.message), "danger"); toast(I18NextService.i18n.t(res.err.message), "danger");
this.setState({ loading: false });
} }
} }

View file

@ -120,8 +120,8 @@ interface PostState {
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
showSidebarMobile: boolean; showSidebarMobile: boolean;
maxCommentsShown: number; maxCommentsShown: number;
finished: Map<CommentId, boolean | undefined>;
isIsomorphic: boolean; isIsomorphic: boolean;
lastCreatedCommentId?: CommentId;
} }
const defaultCommentSort: CommentSortType = "Hot"; const defaultCommentSort: CommentSortType = "Hot";
@ -219,7 +219,6 @@ export class Post extends Component<PostRouteProps, PostState> {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
showSidebarMobile: false, showSidebarMobile: false,
maxCommentsShown: commentsShownInterval, maxCommentsShown: commentsShownInterval,
finished: new Map(),
isIsomorphic: false, isIsomorphic: false,
}; };
@ -236,6 +235,8 @@ export class Post extends Component<PostRouteProps, PostState> {
this.handleFollow = this.handleFollow.bind(this); this.handleFollow = this.handleFollow.bind(this);
this.handleModRemoveCommunity = this.handleModRemoveCommunity.bind(this); this.handleModRemoveCommunity = this.handleModRemoveCommunity.bind(this);
this.handleCreateComment = this.handleCreateComment.bind(this); this.handleCreateComment = this.handleCreateComment.bind(this);
this.handleCreateToplevelComment =
this.handleCreateToplevelComment.bind(this);
this.handleEditComment = this.handleEditComment.bind(this); this.handleEditComment = this.handleEditComment.bind(this);
this.handleSaveComment = this.handleSaveComment.bind(this); this.handleSaveComment = this.handleSaveComment.bind(this);
this.handleBlockCommunity = this.handleBlockCommunity.bind(this); this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
@ -585,7 +586,8 @@ export class Post extends Component<PostRouteProps, PostState> {
) && ( ) && (
<CommentForm <CommentForm
key={ key={
this.context.router.history.location.key this.context.router.history.location.key +
this.state.lastCreatedCommentId
// reset on new location, otherwise <Prompt /> stops working // reset on new location, otherwise <Prompt /> stops working
} }
node={res.post_view.post.id} node={res.post_view.post.id}
@ -593,8 +595,7 @@ export class Post extends Component<PostRouteProps, PostState> {
allLanguages={siteRes.all_languages} allLanguages={siteRes.all_languages}
siteLanguages={siteRes.discussion_languages} siteLanguages={siteRes.discussion_languages}
containerClass="post-comment-container" containerClass="post-comment-container"
onUpsertComment={this.handleCreateComment} onUpsertComment={this.handleCreateToplevelComment}
finished={this.state.finished.get(0)}
/> />
)} )}
<div className="d-block d-md-none"> <div className="d-block d-md-none">
@ -774,7 +775,6 @@ export class Post extends Component<PostRouteProps, PostState> {
enableDownvotes={enableDownvotes(siteRes)} enableDownvotes={enableDownvotes(siteRes)}
voteDisplayMode={voteDisplayMode(siteRes)} voteDisplayMode={voteDisplayMode(siteRes)}
showContext showContext
finished={this.state.finished}
allLanguages={siteRes.all_languages} allLanguages={siteRes.all_languages}
siteLanguages={siteRes.discussion_languages} siteLanguages={siteRes.discussion_languages}
onSaveComment={this.handleSaveComment} onSaveComment={this.handleSaveComment}
@ -885,7 +885,6 @@ export class Post extends Component<PostRouteProps, PostState> {
admins={siteRes.admins} admins={siteRes.admins}
enableDownvotes={enableDownvotes(siteRes)} enableDownvotes={enableDownvotes(siteRes)}
voteDisplayMode={voteDisplayMode(siteRes)} voteDisplayMode={voteDisplayMode(siteRes)}
finished={this.state.finished}
allLanguages={siteRes.all_languages} allLanguages={siteRes.all_languages}
siteLanguages={siteRes.discussion_languages} siteLanguages={siteRes.discussion_languages}
onSaveComment={this.handleSaveComment} onSaveComment={this.handleSaveComment}
@ -1063,6 +1062,14 @@ export class Post extends Component<PostRouteProps, PostState> {
return res; return res;
} }
async handleCreateToplevelComment(form: CreateComment) {
const res = await this.handleCreateComment(form);
if (res.state === "success") {
this.setState({ lastCreatedCommentId: res.data.comment_view.comment.id });
}
return res;
}
async handleCreateComment(form: CreateComment) { async handleCreateComment(form: CreateComment) {
const createCommentRes = await HttpService.client.createComment(form); const createCommentRes = await HttpService.client.createComment(form);
this.createAndUpdateComments(createCommentRes); this.createAndUpdateComments(createCommentRes);
@ -1380,12 +1387,12 @@ export class Post extends Component<PostRouteProps, PostState> {
); );
comments.splice(foundCommentParentIndex + 1, 0, newComment); comments.splice(foundCommentParentIndex + 1, 0, newComment);
// Set finished for the parent
s.finished.set(newCommentParentId ?? 0, true);
} }
return s; return s;
}); });
if (res.state === "failed") {
toast(I18NextService.i18n.t(res.err.message), "danger");
}
} }
findAndUpdateCommentEdit(res: RequestState<CommentResponse>) { findAndUpdateCommentEdit(res: RequestState<CommentResponse>) {
@ -1395,13 +1402,14 @@ export class Post extends Component<PostRouteProps, PostState> {
res.data.comment_view, res.data.comment_view,
s.commentsRes.data.comments, s.commentsRes.data.comments,
); );
s.finished.set(res.data.comment_view.comment.id, true);
} }
return s; return s;
}); });
if (res.state === "failed") {
toast(I18NextService.i18n.t(res.err.message), "danger");
}
} }
// No need to set finished on a comment vote, save, etc
findAndUpdateComment(res: RequestState<CommentResponse>) { findAndUpdateComment(res: RequestState<CommentResponse>) {
this.setState(s => { this.setState(s => {
if (s.commentsRes.state === "success" && res.state === "success") { if (s.commentsRes.state === "success" && res.state === "success") {
@ -1412,6 +1420,9 @@ export class Post extends Component<PostRouteProps, PostState> {
} }
return s; return s;
}); });
if (res.state === "failed") {
toast(I18NextService.i18n.t(res.err.message), "danger");
}
} }
findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) { findAndUpdateCommentReply(res: RequestState<CommentReplyResponse>) {
@ -1424,6 +1435,9 @@ export class Post extends Component<PostRouteProps, PostState> {
} }
return s; return s;
}); });
if (res.state === "failed") {
toast(I18NextService.i18n.t(res.err.message), "danger");
}
} }
updateModerators(res: RequestState<AddModToCommunityResponse>) { updateModerators(res: RequestState<AddModToCommunityResponse>) {

View file

@ -168,14 +168,22 @@ export class CreatePrivateMessage extends Component<
); );
} }
async handlePrivateMessageCreate(form: CreatePrivateMessageI) { async handlePrivateMessageCreate(
form: CreatePrivateMessageI,
bypassNavWarning: () => void,
): Promise<boolean> {
const res = await HttpService.client.createPrivateMessage(form); const res = await HttpService.client.createPrivateMessage(form);
if (res.state === "success") { if (res.state === "success") {
toast(I18NextService.i18n.t("message_sent")); toast(I18NextService.i18n.t("message_sent"));
bypassNavWarning();
// Navigate to the front // Navigate to the front
this.context.router.history.push("/"); this.context.router.history.push("/");
} else if (res.state === "failed") {
toast(I18NextService.i18n.t(res.err.message), "danger");
} }
return res.state !== "failed";
} }
} }

View file

@ -1,5 +1,5 @@
import { capitalizeFirstLetter } from "@utils/helpers"; import { capitalizeFirstLetter } from "@utils/helpers";
import { Component, InfernoNode } from "inferno"; import { Component } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { Prompt } from "inferno-router"; import { Prompt } from "inferno-router";
import { import {
@ -19,8 +19,14 @@ interface PrivateMessageFormProps {
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; replyType?: boolean;
onCancel?(): any; onCancel?(): any;
onCreate?(form: CreatePrivateMessage): void; onCreate?(
onEdit?(form: EditPrivateMessage): void; form: CreatePrivateMessage,
bypassNavWarning: () => void,
): Promise<boolean>;
onEdit?(
form: EditPrivateMessage,
bypassNavWarning: () => void,
): Promise<boolean>;
} }
interface PrivateMessageFormState { interface PrivateMessageFormState {
@ -28,6 +34,7 @@ interface PrivateMessageFormState {
loading: boolean; loading: boolean;
previewMode: boolean; previewMode: boolean;
submitted: boolean; submitted: boolean;
bypassNavWarning?: boolean;
} }
export class PrivateMessageForm extends Component< export class PrivateMessageForm extends Component<
@ -51,21 +58,15 @@ export class PrivateMessageForm extends Component<
this.handlePrivateMessageSubmit.bind(this); this.handlePrivateMessageSubmit.bind(this);
} }
componentWillReceiveProps(
nextProps: Readonly<{ children?: InfernoNode } & PrivateMessageFormProps>,
): void {
if (this.props !== nextProps) {
this.setState({ loading: false, content: undefined, previewMode: false });
}
}
render() { render() {
return ( return (
<form className="private-message-form"> <form className="private-message-form">
<Prompt <Prompt
message={I18NextService.i18n.t("block_leaving")} message={I18NextService.i18n.t("block_leaving")}
when={ when={
!this.state.loading && !!this.state.content && !this.state.submitted !this.state.bypassNavWarning &&
((!!this.state.content && !this.state.submitted) ||
this.state.loading)
} }
/> />
{!this.props.privateMessageView && ( {!this.props.privateMessageView && (
@ -140,21 +141,34 @@ export class PrivateMessageForm extends Component<
); );
} }
handlePrivateMessageSubmit() { async handlePrivateMessageSubmit(): Promise<boolean> {
this.setState({ loading: true, submitted: true }); this.setState({ loading: true, submitted: true });
const pm = this.props.privateMessageView; const pm = this.props.privateMessageView;
const content = this.state.content ?? ""; const content = this.state.content ?? "";
let success: boolean | undefined;
if (pm) { if (pm) {
this.props.onEdit?.({ success = await this.props.onEdit?.(
private_message_id: pm.private_message.id, {
content, private_message_id: pm.private_message.id,
}); content,
},
() => {
this.setState({ bypassNavWarning: true });
},
);
} else { } else {
this.props.onCreate?.({ success = await this.props.onCreate?.(
content, {
recipient_id: this.props.recipient.id, content,
}); recipient_id: this.props.recipient.id,
},
() => {
this.setState({ bypassNavWarning: true });
},
);
} }
this.setState({ loading: false, submitted: success ?? true });
return success ?? true;
} }
handleContentChange(val: string) { handleContentChange(val: string) {

View file

@ -1,4 +1,4 @@
import { Component, InfernoNode, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
CreatePrivateMessage, CreatePrivateMessage,
CreatePrivateMessageReport, CreatePrivateMessageReport,
@ -32,8 +32,8 @@ interface PrivateMessageProps {
onDelete(form: DeletePrivateMessage): void; onDelete(form: DeletePrivateMessage): void;
onMarkRead(form: MarkPrivateMessageAsRead): void; onMarkRead(form: MarkPrivateMessageAsRead): void;
onReport(form: CreatePrivateMessageReport): void; onReport(form: CreatePrivateMessageReport): void;
onCreate(form: CreatePrivateMessage): void; onCreate(form: CreatePrivateMessage): Promise<boolean>;
onEdit(form: EditPrivateMessage): void; onEdit(form: EditPrivateMessage): Promise<boolean>;
} }
@tippyMixin @tippyMixin
@ -56,6 +56,8 @@ export class PrivateMessage extends Component<
this.handleReplyCancel = this.handleReplyCancel.bind(this); this.handleReplyCancel = this.handleReplyCancel.bind(this);
this.handleReportSubmit = this.handleReportSubmit.bind(this); this.handleReportSubmit = this.handleReportSubmit.bind(this);
this.hideReportDialog = this.hideReportDialog.bind(this); this.hideReportDialog = this.hideReportDialog.bind(this);
this.handleCreate = this.handleCreate.bind(this);
this.handleEdit = this.handleEdit.bind(this);
} }
get mine(): boolean { get mine(): boolean {
@ -65,22 +67,6 @@ export class PrivateMessage extends Component<
); );
} }
componentWillReceiveProps(
nextProps: Readonly<{ children?: InfernoNode } & PrivateMessageProps>,
): void {
if (this.props !== nextProps) {
this.setState({
showReply: false,
showEdit: false,
collapsed: false,
viewSource: false,
showReportDialog: false,
deleteLoading: false,
readLoading: false,
});
}
}
render() { render() {
const message_view = this.props.private_message_view; const message_view = this.props.private_message_view;
const otherPerson: Person = this.mine const otherPerson: Person = this.mine
@ -126,7 +112,7 @@ export class PrivateMessage extends Component<
<PrivateMessageForm <PrivateMessageForm
recipient={otherPerson} recipient={otherPerson}
privateMessageView={message_view} privateMessageView={message_view}
onEdit={this.props.onEdit} onEdit={this.handleEdit}
onCancel={this.handleReplyCancel} onCancel={this.handleReplyCancel}
/> />
)} )}
@ -265,7 +251,7 @@ export class PrivateMessage extends Component<
<PrivateMessageForm <PrivateMessageForm
replyType={true} replyType={true}
recipient={otherPerson} recipient={otherPerson}
onCreate={this.props.onCreate} onCreate={this.handleCreate}
onCancel={this.handleReplyCancel} onCancel={this.handleReplyCancel}
/> />
</div> </div>
@ -304,7 +290,6 @@ export class PrivateMessage extends Component<
handleEditClick(i: PrivateMessage) { handleEditClick(i: PrivateMessage) {
i.setState({ showEdit: true }); i.setState({ showEdit: true });
i.setState(i.state);
} }
handleDeleteClick(i: PrivateMessage) { handleDeleteClick(i: PrivateMessage) {
@ -319,6 +304,22 @@ export class PrivateMessage extends Component<
this.setState({ showReply: false, showEdit: false }); this.setState({ showReply: false, showEdit: false });
} }
async handleCreate(form: CreatePrivateMessage): Promise<boolean> {
const success = await this.props.onCreate(form);
if (success) {
this.setState({ showReply: false });
}
return success;
}
async handleEdit(form: EditPrivateMessage): Promise<boolean> {
const success = await this.props.onEdit(form);
if (success) {
this.setState({ showEdit: false });
}
return success;
}
handleMarkRead(i: PrivateMessage) { handleMarkRead(i: PrivateMessage) {
i.setState({ readLoading: true }); i.setState({ readLoading: true });
i.props.onMarkRead({ i.props.onMarkRead({

View file

@ -824,7 +824,6 @@ export class Search extends Component<SearchRouteProps, SearchState> {
allLanguages={siteRes.all_languages} allLanguages={siteRes.all_languages}
siteLanguages={siteRes.discussion_languages} siteLanguages={siteRes.discussion_languages}
// All of these are unused, since its viewonly // All of these are unused, since its viewonly
finished={new Map()}
onSaveComment={async () => {}} onSaveComment={async () => {}}
onBlockPerson={async () => {}} onBlockPerson={async () => {}}
onDeleteComment={async () => {}} onDeleteComment={async () => {}}
@ -886,7 +885,6 @@ export class Search extends Component<SearchRouteProps, SearchState> {
allLanguages={siteRes.all_languages} allLanguages={siteRes.all_languages}
siteLanguages={siteRes.discussion_languages} siteLanguages={siteRes.discussion_languages}
// All of these are unused, since its viewonly // All of these are unused, since its viewonly
finished={new Map()}
onSaveComment={async () => {}} onSaveComment={async () => {}}
onBlockPerson={async () => {}} onBlockPerson={async () => {}}
onDeleteComment={async () => {}} onDeleteComment={async () => {}}