]> Untitled Git - lemmy.git/blobdiff - ui/src/components/comment-form.tsx
routes.api: fix get_captcha endpoint (#1135)
[lemmy.git] / ui / src / components / comment-form.tsx
index 22f871d22385dfd0bd3929593e69f35d2ce18efa..dbd14dc76adc1ff82939d23f10714a2a60970331 100644 (file)
@@ -1,29 +1,19 @@
-import { Component, linkEvent } from 'inferno';
+import { Component } from 'inferno';
+import { Link } from 'inferno-router';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { Prompt } from 'inferno-router';
 import {
   CommentNode as CommentNodeI,
   CommentForm as CommentFormI,
   WebSocketJsonResponse,
   UserOperation,
   CommentResponse,
-} from '../interfaces';
-import {
-  capitalizeFirstLetter,
-  mdToHtml,
-  randomStr,
-  markdownHelpUrl,
-  toast,
-  setupTribute,
-  wsJsonToRes,
-  pictrsDeleteToast,
-} from '../utils';
+} from 'lemmy-js-client';
+import { capitalizeFirstLetter, wsJsonToRes } from '../utils';
 import { WebSocketService, UserService } from '../services';
-import autosize from 'autosize';
-import Tribute from 'tributejs/src/Tribute.js';
-import emojiShortName from 'emoji-short-name';
 import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
+import { MarkdownTextArea } from './markdown-textarea';
 
 interface CommentFormProps {
   postId?: number;
@@ -31,20 +21,16 @@ interface CommentFormProps {
   onReplyCancel?(): any;
   edit?: boolean;
   disabled?: boolean;
+  focus?: boolean;
 }
 
 interface CommentFormState {
   commentForm: CommentFormI;
   buttonTitle: string;
-  previewMode: boolean;
-  loading: boolean;
-  imageLoading: boolean;
+  finished: boolean;
 }
 
 export class CommentForm extends Component<CommentFormProps, CommentFormState> {
-  private id = `comment-textarea-${randomStr()}`;
-  private formId = `comment-form-${randomStr()}`;
-  private tribute: Tribute;
   private subscription: Subscription;
   private emptyState: CommentFormState = {
     commentForm: {
@@ -62,15 +48,14 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
       : this.props.edit
       ? capitalizeFirstLetter(i18n.t('save'))
       : capitalizeFirstLetter(i18n.t('reply')),
-    previewMode: false,
-    loading: false,
-    imageLoading: false,
+    finished: false,
   };
 
   constructor(props: any, context: any) {
     super(props, context);
 
-    this.tribute = setupTribute();
+    this.handleCommentSubmit = this.handleCommentSubmit.bind(this);
+    this.handleReplyCancel = this.handleReplyCancel.bind(this);
 
     this.state = this.emptyState;
 
@@ -95,279 +80,54 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
       );
   }
 
-  componentDidMount() {
-    let textarea: any = document.getElementById(this.id);
-    autosize(textarea);
-    this.tribute.attach(textarea);
-    textarea.addEventListener('tribute-replaced', () => {
-      this.state.commentForm.content = textarea.value;
-      this.setState(this.state);
-      autosize.update(textarea);
-    });
-
-    // Quoting of selected text
-    let selectedText = window.getSelection().toString();
-    if (selectedText) {
-      let quotedText =
-        selectedText
-          .split('\n')
-          .map(t => `> ${t}`)
-          .join('\n') + '\n\n';
-      this.state.commentForm.content = quotedText;
-      this.setState(this.state);
-      // Not sure why this needs a delay
-      setTimeout(() => autosize.update(textarea), 10);
-    }
-
-    textarea.focus();
-  }
-
-  componentDidUpdate() {
-    if (this.state.commentForm.content) {
-      window.onbeforeunload = () => true;
-    } else {
-      window.onbeforeunload = undefined;
-    }
-  }
-
   componentWillUnmount() {
     this.subscription.unsubscribe();
-    window.onbeforeunload = null;
   }
 
   render() {
     return (
       <div class="mb-3">
-        <Prompt
-          when={this.state.commentForm.content}
-          message={i18n.t('block_leaving')}
-        />
-        <form
-          id={this.formId}
-          onSubmit={linkEvent(this, this.handleCommentSubmit)}
-        >
-          <div class="form-group row">
-            <div className={`col-sm-12`}>
-              <textarea
-                id={this.id}
-                className={`form-control ${this.state.previewMode && 'd-none'}`}
-                value={this.state.commentForm.content}
-                onInput={linkEvent(this, this.handleCommentContentChange)}
-                onPaste={linkEvent(this, this.handleImageUploadPaste)}
-                required
-                disabled={this.props.disabled}
-                rows={2}
-                maxLength={10000}
-              />
-              {this.state.previewMode && (
-                <div
-                  className="card card-body md-div"
-                  dangerouslySetInnerHTML={mdToHtml(
-                    this.state.commentForm.content
-                  )}
-                />
-              )}
-            </div>
+        {UserService.Instance.user ? (
+          <MarkdownTextArea
+            initialContent={this.state.commentForm.content}
+            buttonTitle={this.state.buttonTitle}
+            finished={this.state.finished}
+            replyType={!!this.props.node}
+            focus={this.props.focus}
+            disabled={this.props.disabled}
+            onSubmit={this.handleCommentSubmit}
+            onReplyCancel={this.handleReplyCancel}
+          />
+        ) : (
+          <div class="alert alert-light" role="alert">
+            <svg class="icon icon-inline mr-2">
+              <use xlinkHref="#icon-alert-triangle"></use>
+            </svg>
+            <T i18nKey="must_login" class="d-inline">
+              #
+              <Link class="alert-link" to="/login">
+                #
+              </Link>
+            </T>
           </div>
-          <div class="row">
-            <div class="col-sm-12">
-              <button
-                type="submit"
-                class="btn btn-sm btn-secondary mr-2"
-                disabled={this.props.disabled || this.state.loading}
-              >
-                {this.state.loading ? (
-                  <svg class="icon icon-spinner spin">
-                    <use xlinkHref="#icon-spinner"></use>
-                  </svg>
-                ) : (
-                  <span>{this.state.buttonTitle}</span>
-                )}
-              </button>
-              {this.state.commentForm.content && (
-                <button
-                  className={`btn btn-sm mr-2 btn-secondary ${
-                    this.state.previewMode && 'active'
-                  }`}
-                  onClick={linkEvent(this, this.handlePreviewToggle)}
-                >
-                  {i18n.t('preview')}
-                </button>
-              )}
-              {this.props.node && (
-                <button
-                  type="button"
-                  class="btn btn-sm btn-secondary mr-2"
-                  onClick={linkEvent(this, this.handleReplyCancel)}
-                >
-                  {i18n.t('cancel')}
-                </button>
-              )}
-              <a
-                href={markdownHelpUrl}
-                target="_blank"
-                class="d-inline-block float-right text-muted font-weight-bold"
-                title={i18n.t('formatting_help')}
-                rel="noopener"
-              >
-                <svg class="icon icon-inline">
-                  <use xlinkHref="#icon-help-circle"></use>
-                </svg>
-              </a>
-              <form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
-                <label
-                  htmlFor={`file-upload-${this.id}`}
-                  className={`${UserService.Instance.user && 'pointer'}`}
-                  data-tippy-content={i18n.t('upload_image')}
-                >
-                  <svg class="icon icon-inline">
-                    <use xlinkHref="#icon-image"></use>
-                  </svg>
-                </label>
-                <input
-                  id={`file-upload-${this.id}`}
-                  type="file"
-                  accept="image/*,video/*"
-                  name="file"
-                  class="d-none"
-                  disabled={!UserService.Instance.user}
-                  onChange={linkEvent(this, this.handleImageUpload)}
-                />
-              </form>
-              {this.state.imageLoading && (
-                <svg class="icon icon-spinner spin">
-                  <use xlinkHref="#icon-spinner"></use>
-                </svg>
-              )}
-            </div>
-          </div>
-        </form>
+        )}
       </div>
     );
   }
 
-  handleFinished(op: UserOperation, data: CommentResponse) {
-    let isReply =
-      this.props.node !== undefined && data.comment.parent_id !== null;
-    let xor =
-      +!(data.comment.parent_id !== null) ^ +(this.props.node !== undefined);
-
-    if (
-      (data.comment.creator_id == UserService.Instance.user.id &&
-        ((op == UserOperation.CreateComment &&
-          // If its a reply, make sure parent child match
-          isReply &&
-          data.comment.parent_id == this.props.node.comment.id) ||
-          // Otherwise, check the XOR of the two
-          (!isReply && xor))) ||
-      // If its a comment edit, only check that its from your user, and that its a
-      // text edit only
-
-      (data.comment.creator_id == UserService.Instance.user.id &&
-        op == UserOperation.EditComment &&
-        data.comment.content)
-    ) {
-      this.state.previewMode = false;
-      this.state.loading = false;
-      this.state.commentForm.content = '';
-      this.setState(this.state);
-      let form: any = document.getElementById(this.formId);
-      form.reset();
-      if (this.props.node) {
-        this.props.onReplyCancel();
-      }
-      autosize.update(form);
-      this.setState(this.state);
-    }
-  }
-
-  handleCommentSubmit(i: CommentForm, event: any) {
-    event.preventDefault();
-    if (i.props.edit) {
-      WebSocketService.Instance.editComment(i.state.commentForm);
+  handleCommentSubmit(msg: { val: string; formId: string }) {
+    this.state.commentForm.content = msg.val;
+    this.state.commentForm.form_id = msg.formId;
+    if (this.props.edit) {
+      WebSocketService.Instance.editComment(this.state.commentForm);
     } else {
-      WebSocketService.Instance.createComment(i.state.commentForm);
+      WebSocketService.Instance.createComment(this.state.commentForm);
     }
-
-    i.state.loading = true;
-    i.setState(i.state);
+    this.setState(this.state);
   }
 
-  handleCommentContentChange(i: CommentForm, event: any) {
-    i.state.commentForm.content = event.target.value;
-    i.setState(i.state);
-  }
-
-  handlePreviewToggle(i: CommentForm, event: any) {
-    event.preventDefault();
-    i.state.previewMode = !i.state.previewMode;
-    i.setState(i.state);
-  }
-
-  handleReplyCancel(i: CommentForm) {
-    i.props.onReplyCancel();
-  }
-
-  handleImageUploadPaste(i: CommentForm, event: any) {
-    let image = event.clipboardData.files[0];
-    if (image) {
-      i.handleImageUpload(i, image);
-    }
-  }
-
-  handleImageUpload(i: CommentForm, event: any) {
-    let file: any;
-    if (event.target) {
-      event.preventDefault();
-      file = event.target.files[0];
-    } else {
-      file = event;
-    }
-
-    const imageUploadUrl = `/pictrs/image`;
-    const formData = new FormData();
-    formData.append('images[]', file);
-
-    i.state.imageLoading = true;
-    i.setState(i.state);
-
-    fetch(imageUploadUrl, {
-      method: 'POST',
-      body: formData,
-    })
-      .then(res => res.json())
-      .then(res => {
-        console.log('pictrs upload:');
-        console.log(res);
-        if (res.msg == 'ok') {
-          let hash = res.files[0].file;
-          let url = `${window.location.origin}/pictrs/image/${hash}`;
-          let deleteToken = res.files[0].delete_token;
-          let deleteUrl = `${window.location.origin}/pictrs/image/delete/${deleteToken}/${hash}`;
-          let imageMarkdown = `![](${url})`;
-          let content = i.state.commentForm.content;
-          content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
-          i.state.commentForm.content = content;
-          i.state.imageLoading = false;
-          i.setState(i.state);
-          let textarea: any = document.getElementById(i.id);
-          autosize.update(textarea);
-          pictrsDeleteToast(
-            i18n.t('click_to_delete_picture'),
-            i18n.t('picture_deleted'),
-            deleteUrl
-          );
-        } else {
-          i.state.imageLoading = false;
-          i.setState(i.state);
-          toast(JSON.stringify(res), 'danger');
-        }
-      })
-      .catch(error => {
-        i.state.imageLoading = false;
-        i.setState(i.state);
-        toast(error, 'danger');
-      });
+  handleReplyCancel() {
+    this.props.onReplyCancel();
   }
 
   parseMessage(msg: WebSocketJsonResponse) {
@@ -375,12 +135,19 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
 
     // Only do the showing and hiding if logged in
     if (UserService.Instance.user) {
-      if (res.op == UserOperation.CreateComment) {
+      if (
+        res.op == UserOperation.CreateComment ||
+        res.op == UserOperation.EditComment
+      ) {
         let data = res.data as CommentResponse;
-        this.handleFinished(res.op, data);
-      } else if (res.op == UserOperation.EditComment) {
-        let data = res.data as CommentResponse;
-        this.handleFinished(res.op, data);
+
+        // This only finishes this form, if the randomly generated form_id matches the one received
+        if (this.state.commentForm.form_id == data.form_id) {
+          this.setState({ finished: true });
+
+          // Necessary because it broke tribute for some reaso
+          this.setState({ finished: false });
+        }
       }
     }
   }