]> Untitled Git - lemmy.git/commitdiff
Adding markdown buttons. Fixes #977 (#984)
authorDessalines <dessalines@users.noreply.github.com>
Fri, 17 Jul 2020 01:12:51 +0000 (21:12 -0400)
committerGitHub <noreply@github.com>
Fri, 17 Jul 2020 01:12:51 +0000 (21:12 -0400)
ui/src/components/comment-form.tsx
ui/src/components/community-form.tsx
ui/src/components/markdown-textarea.tsx [new file with mode: 0644]
ui/src/components/post-form.tsx
ui/src/components/private-message-form.tsx
ui/src/components/site-form.tsx
ui/src/components/symbols.tsx
ui/translations/en.json

index 72a4f398b5a6bf469cb521abaeb09e1ab268f032..6e45229b56dfcec3e01930821253818f21c8462a 100644 (file)
@@ -1,8 +1,7 @@
-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,
@@ -10,22 +9,11 @@ import {
   UserOperation,
   CommentResponse,
 } from '../interfaces';
-import {
-  capitalizeFirstLetter,
-  mdToHtml,
-  randomStr,
-  markdownHelpUrl,
-  toast,
-  setupTribute,
-  wsJsonToRes,
-  pictrsDeleteToast,
-} from '../utils';
+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;
@@ -39,15 +27,10 @@ interface CommentFormProps {
 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: {
@@ -65,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;
 
@@ -98,160 +80,24 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
       );
   }
 
-  componentDidMount() {
-    let textarea: any = document.getElementById(this.id);
-    if (textarea) {
-      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);
-      }
-
-      if (this.props.focus) {
-        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')}
-        />
         {UserService.Instance.user ? (
-          <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>
-            </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>
+          <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">
@@ -290,107 +136,23 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
         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.state.finished = true;
       this.setState(this.state);
     }
   }
 
-  handleCommentSubmit(i: CommentForm, event: any) {
-    event.preventDefault();
-    if (i.props.edit) {
-      WebSocketService.Instance.editComment(i.state.commentForm);
+  handleCommentSubmit(val: string) {
+    this.state.commentForm.content = val;
+    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) {
index 1a8ac65a9c82539ddc4f050a2dde5c4539510252..dc73ffc081536ea056261238c892795a53a5228a 100644 (file)
@@ -11,18 +11,11 @@ import {
   WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService } from '../services';
-import {
-  wsJsonToRes,
-  capitalizeFirstLetter,
-  toast,
-  randomStr,
-  setupTribute,
-} from '../utils';
-import Tribute from 'tributejs/src/Tribute.js';
-import autosize from 'autosize';
+import { wsJsonToRes, capitalizeFirstLetter, toast, randomStr } from '../utils';
 import { i18n } from '../i18next';
 
 import { Community } from '../interfaces';
+import { MarkdownTextArea } from './markdown-textarea';
 
 interface CommunityFormProps {
   community?: Community; // If a community is given, that means this is an edit
@@ -43,7 +36,6 @@ export class CommunityForm extends Component<
   CommunityFormState
 > {
   private id = `community-form-${randomStr()}`;
-  private tribute: Tribute;
   private subscription: Subscription;
 
   private emptyState: CommunityFormState = {
@@ -60,9 +52,12 @@ export class CommunityForm extends Component<
   constructor(props: any, context: any) {
     super(props, context);
 
-    this.tribute = setupTribute();
     this.state = this.emptyState;
 
+    this.handleCommunityDescriptionChange = this.handleCommunityDescriptionChange.bind(
+      this
+    );
+
     if (this.props.community) {
       this.state.communityForm = {
         name: this.props.community.name,
@@ -86,17 +81,6 @@ export class CommunityForm extends Component<
     WebSocketService.Instance.listCategories();
   }
 
-  componentDidMount() {
-    var textarea: any = document.getElementById(this.id);
-    autosize(textarea);
-    this.tribute.attach(textarea);
-    textarea.addEventListener('tribute-replaced', () => {
-      this.state.communityForm.description = textarea.value;
-      this.setState(this.state);
-      autosize.update(textarea);
-    });
-  }
-
   componentDidUpdate() {
     if (
       !this.state.loading &&
@@ -171,13 +155,9 @@ export class CommunityForm extends Component<
               {i18n.t('sidebar')}
             </label>
             <div class="col-12">
-              <textarea
-                id={this.id}
-                value={this.state.communityForm.description}
-                onInput={linkEvent(this, this.handleCommunityDescriptionChange)}
-                class="form-control"
-                rows={3}
-                maxLength={10000}
+              <MarkdownTextArea
+                initialContent={this.state.communityForm.description}
+                onContentChange={this.handleCommunityDescriptionChange}
               />
             </div>
           </div>
@@ -271,9 +251,9 @@ export class CommunityForm extends Component<
     i.setState(i.state);
   }
 
-  handleCommunityDescriptionChange(i: CommunityForm, event: any) {
-    i.state.communityForm.description = event.target.value;
-    i.setState(i.state);
+  handleCommunityDescriptionChange(val: string) {
+    this.state.communityForm.description = val;
+    this.setState(this.state);
   }
 
   handleCommunityCategoryChange(i: CommunityForm, event: any) {
diff --git a/ui/src/components/markdown-textarea.tsx b/ui/src/components/markdown-textarea.tsx
new file mode 100644 (file)
index 0000000..2f6d0a7
--- /dev/null
@@ -0,0 +1,509 @@
+import { Component, linkEvent } from 'inferno';
+import { Prompt } from 'inferno-router';
+import {
+  mdToHtml,
+  randomStr,
+  markdownHelpUrl,
+  toast,
+  setupTribute,
+  pictrsDeleteToast,
+  setupTippy,
+} from '../utils';
+import { UserService } from '../services';
+import autosize from 'autosize';
+import Tribute from 'tributejs/src/Tribute.js';
+import { i18n } from '../i18next';
+
+interface MarkdownTextAreaProps {
+  initialContent: string;
+  finished?: boolean;
+  buttonTitle?: string;
+  replyType?: boolean;
+  focus?: boolean;
+  disabled?: boolean;
+  onSubmit?(val: string): any;
+  onContentChange?(val: string): any;
+  onReplyCancel?(): any;
+}
+
+interface MarkdownTextAreaState {
+  content: string;
+  previewMode: boolean;
+  loading: boolean;
+  imageLoading: boolean;
+}
+
+export class MarkdownTextArea extends Component<
+  MarkdownTextAreaProps,
+  MarkdownTextAreaState
+> {
+  private id = `comment-textarea-${randomStr()}`;
+  private formId = `comment-form-${randomStr()}`;
+  private tribute: Tribute;
+  private emptyState: MarkdownTextAreaState = {
+    content: this.props.initialContent,
+    previewMode: false,
+    loading: false,
+    imageLoading: false,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+
+    this.tribute = setupTribute();
+    this.state = this.emptyState;
+  }
+
+  componentDidMount() {
+    let textarea: any = document.getElementById(this.id);
+    if (textarea) {
+      autosize(textarea);
+      this.tribute.attach(textarea);
+      textarea.addEventListener('tribute-replaced', () => {
+        this.state.content = textarea.value;
+        this.setState(this.state);
+        autosize.update(textarea);
+      });
+
+      this.quoteInsert();
+
+      if (this.props.focus) {
+        textarea.focus();
+      }
+
+      // TODO this is slow for some reason
+      setupTippy();
+    }
+  }
+
+  componentDidUpdate() {
+    if (this.state.content) {
+      window.onbeforeunload = () => true;
+    } else {
+      window.onbeforeunload = undefined;
+    }
+  }
+
+  componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
+    if (nextProps.finished) {
+      this.state.previewMode = false;
+      this.state.loading = false;
+      this.state.content = '';
+      this.setState(this.state);
+      if (this.props.replyType) {
+        this.props.onReplyCancel();
+      }
+
+      let textarea: any = document.getElementById(this.id);
+      let form: any = document.getElementById(this.formId);
+      form.reset();
+      setTimeout(() => autosize.update(textarea), 10);
+      this.setState(this.state);
+    }
+  }
+
+  componentWillUnmount() {
+    window.onbeforeunload = null;
+  }
+
+  render() {
+    return (
+      <form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}>
+        <Prompt when={this.state.content} message={i18n.t('block_leaving')} />
+        <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.content}
+              onInput={linkEvent(this, this.handleContentChange)}
+              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.content)}
+              />
+            )}
+          </div>
+        </div>
+        <div class="row">
+          <div class="col-sm-12 d-flex flex-wrap">
+            {this.props.buttonTitle && (
+              <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.props.buttonTitle}</span>
+                )}
+              </button>
+            )}
+            {this.props.replyType && (
+              <button
+                type="button"
+                class="btn btn-sm btn-secondary mr-2"
+                onClick={linkEvent(this, this.handleReplyCancel)}
+              >
+                {i18n.t('cancel')}
+              </button>
+            )}
+            {this.state.content && (
+              <button
+                className={`btn btn-sm btn-secondary mr-2 ${
+                  this.state.previewMode && 'active'
+                }`}
+                onClick={linkEvent(this, this.handlePreviewToggle)}
+              >
+                {i18n.t('preview')}
+              </button>
+            )}
+            {/* A flex expander */}
+            <div class="flex-grow-1"></div>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('bold')}
+              onClick={linkEvent(this, this.handleInsertBold)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-bold"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('italic')}
+              onClick={linkEvent(this, this.handleInsertItalic)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-italic"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('link')}
+              onClick={linkEvent(this, this.handleInsertLink)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-link"></use>
+              </svg>
+            </button>
+            <form class="btn btn-sm text-muted font-weight-bold">
+              <label
+                htmlFor={`file-upload-${this.id}`}
+                className={`mb-0 ${UserService.Instance.user && 'pointer'}`}
+                data-tippy-content={i18n.t('upload_image')}
+              >
+                {this.state.imageLoading ? (
+                  <svg class="icon icon-spinner spin">
+                    <use xlinkHref="#icon-spinner"></use>
+                  </svg>
+                ) : (
+                  <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>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('header')}
+              onClick={linkEvent(this, this.handleInsertHeader)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-header"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('strikethrough')}
+              onClick={linkEvent(this, this.handleInsertStrikethrough)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-strikethrough"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('quote')}
+              onClick={linkEvent(this, this.handleInsertQuote)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-format_quote"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('list')}
+              onClick={linkEvent(this, this.handleInsertList)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-list"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('code')}
+              onClick={linkEvent(this, this.handleInsertCode)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-code"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('spoiler')}
+              onClick={linkEvent(this, this.handleInsertSpoiler)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-alert-triangle"></use>
+              </svg>
+            </button>
+            <a
+              href={markdownHelpUrl}
+              target="_blank"
+              class="btn btn-sm 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>
+          </div>
+        </div>
+      </form>
+    );
+  }
+
+  handleImageUploadPaste(i: MarkdownTextArea, event: any) {
+    let image = event.clipboardData.files[0];
+    if (image) {
+      i.handleImageUpload(i, image);
+    }
+  }
+
+  handleImageUpload(i: MarkdownTextArea, 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.content;
+          content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
+          i.state.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');
+      });
+  }
+
+  handleContentChange(i: MarkdownTextArea, event: any) {
+    i.state.content = event.target.value;
+    i.setState(i.state);
+    if (i.props.onContentChange) {
+      i.props.onContentChange(i.state.content);
+    }
+  }
+
+  handlePreviewToggle(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.state.previewMode = !i.state.previewMode;
+    i.setState(i.state);
+  }
+
+  handleSubmit(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.state.loading = true;
+    i.setState(i.state);
+    i.props.onSubmit(i.state.content);
+  }
+
+  handleReplyCancel(i: MarkdownTextArea) {
+    i.props.onReplyCancel();
+  }
+
+  handleInsertLink(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    if (!i.state.content) {
+      i.state.content = '';
+    }
+    let textarea: any = document.getElementById(i.id);
+    let start: number = textarea.selectionStart;
+    let end: number = textarea.selectionEnd;
+
+    if (start !== end) {
+      let selectedText = i.state.content.substring(start, end);
+      i.state.content = `${i.state.content.substring(
+        0,
+        start
+      )} [${selectedText}]() ${i.state.content.substring(end)}`;
+      textarea.focus();
+      setTimeout(() => (textarea.selectionEnd = end + 4), 10);
+    } else {
+      i.state.content += '[]()';
+      textarea.focus();
+      setTimeout(() => (textarea.selectionEnd -= 1), 10);
+    }
+    i.setState(i.state);
+  }
+
+  simpleSurround(chars: string) {
+    this.simpleSurroundBeforeAfter(chars, chars);
+  }
+
+  simpleSurroundBeforeAfter(beforeChars: string, afterChars: string) {
+    if (!this.state.content) {
+      this.state.content = '';
+    }
+    let textarea: any = document.getElementById(this.id);
+    let start: number = textarea.selectionStart;
+    let end: number = textarea.selectionEnd;
+
+    if (start !== end) {
+      let selectedText = this.state.content.substring(start, end);
+      this.state.content = `${this.state.content.substring(
+        0,
+        start - 1
+      )} ${beforeChars}${selectedText}${afterChars} ${this.state.content.substring(
+        end + 1
+      )}`;
+    } else {
+      this.state.content += `${beforeChars}___${afterChars}`;
+    }
+    this.setState(this.state);
+    setTimeout(() => {
+      autosize.update(textarea);
+    }, 10);
+  }
+
+  handleInsertBold(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleSurround('**');
+  }
+
+  handleInsertItalic(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleSurround('*');
+  }
+
+  handleInsertCode(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleSurround('`');
+  }
+
+  handleInsertStrikethrough(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleSurround('~~');
+  }
+
+  handleInsertList(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleInsert('-');
+  }
+
+  handleInsertQuote(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleInsert('>');
+  }
+
+  handleInsertHeader(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleInsert('#');
+  }
+
+  simpleInsert(chars: string) {
+    if (!this.state.content) {
+      this.state.content = `${chars} `;
+    } else {
+      this.state.content += `\n${chars} `;
+    }
+
+    let textarea: any = document.getElementById(this.id);
+    textarea.focus();
+    setTimeout(() => {
+      autosize.update(textarea);
+    }, 10);
+    this.setState(this.state);
+  }
+
+  handleInsertSpoiler(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    let beforeChars = `\n::: spoiler ${i18n.t('spoiler')}\n`;
+    let afterChars = '\n:::\n';
+    i.simpleSurroundBeforeAfter(beforeChars, afterChars);
+  }
+
+  quoteInsert() {
+    let textarea: any = document.getElementById(this.id);
+    let selectedText = window.getSelection().toString();
+    if (selectedText) {
+      let quotedText =
+        selectedText
+          .split('\n')
+          .map(t => `> ${t}`)
+          .join('\n') + '\n\n';
+      this.state.content = quotedText;
+      this.setState(this.state);
+      // Not sure why this needs a delay
+      setTimeout(() => autosize.update(textarea), 10);
+    }
+  }
+}
index e5efeaac508e5e8d439878dbeb410a7e4ec594d6..f94f902fcbd8ad685a765681a903038748c39595 100644 (file)
@@ -1,6 +1,7 @@
 import { Component, linkEvent } from 'inferno';
 import { Prompt } from 'inferno-router';
 import { PostListings } from './post-listings';
+import { MarkdownTextArea } from './markdown-textarea';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import {
@@ -24,22 +25,16 @@ import {
   getPageTitle,
   validURL,
   capitalizeFirstLetter,
-  markdownHelpUrl,
   archiveUrl,
-  mdToHtml,
   debounce,
   isImage,
   toast,
   randomStr,
-  setupTribute,
   setupTippy,
   hostname,
   pictrsDeleteToast,
   validTitle,
 } from '../utils';
-import autosize from 'autosize';
-import Tribute from 'tributejs/src/Tribute.js';
-import emojiShortName from 'emoji-short-name';
 import Choices from 'choices.js';
 import { i18n } from '../i18next';
 
@@ -68,7 +63,6 @@ interface PostFormState {
 
 export class PostForm extends Component<PostFormProps, PostFormState> {
   private id = `post-form-${randomStr()}`;
-  private tribute: Tribute;
   private subscription: Subscription;
   private choices: Choices;
   private emptyState: PostFormState = {
@@ -94,8 +88,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     super(props, context);
     this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
     this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
-
-    this.tribute = setupTribute();
+    this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
 
     this.state = this.emptyState;
 
@@ -140,14 +133,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
   }
 
   componentDidMount() {
-    var textarea: any = document.getElementById(this.id);
-    autosize(textarea);
-    this.tribute.attach(textarea);
-    textarea.addEventListener('tribute-replaced', () => {
-      this.state.postForm.body = textarea.value;
-      this.setState(this.state);
-      autosize.update(textarea);
-    });
     setupTippy();
   }
 
@@ -305,41 +290,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
               {i18n.t('body')}
             </label>
             <div class="col-sm-10">
-              <textarea
-                id={this.id}
-                value={this.state.postForm.body}
-                onInput={linkEvent(this, this.handlePostBodyChange)}
-                className={`form-control ${this.state.previewMode && 'd-none'}`}
-                rows={4}
-                maxLength={10000}
+              <MarkdownTextArea
+                initialContent={this.state.postForm.body}
+                onContentChange={this.handlePostBodyChange}
               />
-              {this.state.previewMode && (
-                <div
-                  className="card card-body md-div"
-                  dangerouslySetInnerHTML={mdToHtml(this.state.postForm.body)}
-                />
-              )}
-              {this.state.postForm.body && (
-                <button
-                  className={`mt-1 mr-2 btn btn-sm btn-secondary ${
-                    this.state.previewMode && 'active'
-                  }`}
-                  onClick={linkEvent(this, this.handlePreviewToggle)}
-                >
-                  {i18n.t('preview')}
-                </button>
-              )}
-              <a
-                href={markdownHelpUrl}
-                target="_blank"
-                rel="noopener"
-                class="d-inline-block float-right text-muted font-weight-bold"
-                title={i18n.t('formatting_help')}
-              >
-                <svg class="icon icon-inline">
-                  <use xlinkHref="#icon-help-circle"></use>
-                </svg>
-              </a>
             </div>
           </div>
           {!this.props.post && (
@@ -499,9 +453,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     this.setState(this.state);
   }
 
-  handlePostBodyChange(i: PostForm, event: any) {
-    i.state.postForm.body = event.target.value;
-    i.setState(i.state);
+  handlePostBodyChange(val: string) {
+    this.state.postForm.body = val;
+    this.setState(this.state);
   }
 
   handlePostCommunityChange(i: PostForm, event: any) {
index 6ae7efe71d003836e70ce58d2689a46459ab7609..b8dc885396976dfa590f8eab7e308b42e8b838a8 100644 (file)
@@ -18,17 +18,12 @@ import {
 import { WebSocketService } from '../services';
 import {
   capitalizeFirstLetter,
-  markdownHelpUrl,
-  mdToHtml,
   wsJsonToRes,
   toast,
-  randomStr,
-  setupTribute,
   setupTippy,
 } from '../utils';
 import { UserListing } from './user-listing';
-import Tribute from 'tributejs/src/Tribute.js';
-import autosize from 'autosize';
+import { MarkdownTextArea } from './markdown-textarea';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
 
@@ -52,8 +47,6 @@ export class PrivateMessageForm extends Component<
   PrivateMessageFormProps,
   PrivateMessageFormState
 > {
-  private id = `message-form-${randomStr()}`;
-  private tribute: Tribute;
   private subscription: Subscription;
   private emptyState: PrivateMessageFormState = {
     privateMessageForm: {
@@ -69,9 +62,10 @@ export class PrivateMessageForm extends Component<
   constructor(props: any, context: any) {
     super(props, context);
 
-    this.tribute = setupTribute();
     this.state = this.emptyState;
 
+    this.handleContentChange = this.handleContentChange.bind(this);
+
     if (this.props.privateMessage) {
       this.state.privateMessageForm = {
         content: this.props.privateMessage.content,
@@ -99,14 +93,6 @@ export class PrivateMessageForm extends Component<
   }
 
   componentDidMount() {
-    var textarea: any = document.getElementById(this.id);
-    autosize(textarea);
-    this.tribute.attach(textarea);
-    textarea.addEventListener('tribute-replaced', () => {
-      this.state.privateMessageForm.content = textarea.value;
-      this.setState(this.state);
-      autosize.update(textarea);
-    });
     setupTippy();
   }
 
@@ -153,24 +139,23 @@ export class PrivateMessageForm extends Component<
             </div>
           )}
           <div class="form-group row">
-            <label class="col-sm-2 col-form-label">{i18n.t('message')}</label>
+            <label class="col-sm-2 col-form-label">
+              {i18n.t('message')}
+              <span
+                onClick={linkEvent(this, this.handleShowDisclaimer)}
+                class="ml-2 pointer text-danger"
+                data-tippy-content={i18n.t('disclaimer')}
+              >
+                <svg class={`icon icon-inline`}>
+                  <use xlinkHref="#icon-alert-triangle"></use>
+                </svg>
+              </span>
+            </label>
             <div class="col-sm-10">
-              <textarea
-                id={this.id}
-                value={this.state.privateMessageForm.content}
-                onInput={linkEvent(this, this.handleContentChange)}
-                className={`form-control ${this.state.previewMode && 'd-none'}`}
-                rows={4}
-                maxLength={10000}
+              <MarkdownTextArea
+                initialContent={this.state.privateMessageForm.content}
+                onContentChange={this.handleContentChange}
               />
-              {this.state.previewMode && (
-                <div
-                  className="card card-body md-div"
-                  dangerouslySetInnerHTML={mdToHtml(
-                    this.state.privateMessageForm.content
-                  )}
-                />
-              )}
             </div>
           </div>
 
@@ -184,7 +169,7 @@ export class PrivateMessageForm extends Component<
                       class="alert-link"
                       target="_blank"
                       rel="noopener"
-                      href="https://about.riot.im/"
+                      href="https://element.io/get-started"
                     >
                       #
                     </a>
@@ -210,16 +195,6 @@ export class PrivateMessageForm extends Component<
                   capitalizeFirstLetter(i18n.t('send_message'))
                 )}
               </button>
-              {this.state.privateMessageForm.content && (
-                <button
-                  className={`btn btn-secondary mr-2 ${
-                    this.state.previewMode && 'active'
-                  }`}
-                  onClick={linkEvent(this, this.handlePreviewToggle)}
-                >
-                  {i18n.t('preview')}
-                </button>
-              )}
               {this.props.privateMessage && (
                 <button
                   type="button"
@@ -230,30 +205,7 @@ export class PrivateMessageForm extends Component<
                 </button>
               )}
               <ul class="d-inline-block float-right list-inline mb-1 text-muted font-weight-bold">
-                <li class="list-inline-item">
-                  <span
-                    onClick={linkEvent(this, this.handleShowDisclaimer)}
-                    class="pointer"
-                    data-tippy-content={i18n.t('disclaimer')}
-                  >
-                    <svg class={`icon icon-inline`}>
-                      <use xlinkHref="#icon-alert-triangle"></use>
-                    </svg>
-                  </span>
-                </li>
-                <li class="list-inline-item">
-                  <a
-                    href={markdownHelpUrl}
-                    target="_blank"
-                    rel="noopener"
-                    class="text-muted"
-                    title={i18n.t('formatting_help')}
-                  >
-                    <svg class="icon icon-inline">
-                      <use xlinkHref="#icon-help-circle"></use>
-                    </svg>
-                  </a>
-                </li>
+                <li class="list-inline-item"></li>
               </ul>
             </div>
           </div>
@@ -284,9 +236,9 @@ export class PrivateMessageForm extends Component<
     i.setState(i.state);
   }
 
-  handleContentChange(i: PrivateMessageForm, event: any) {
-    i.state.privateMessageForm.content = event.target.value;
-    i.setState(i.state);
+  handleContentChange(val: string) {
+    this.state.privateMessageForm.content = val;
+    this.setState(this.state);
   }
 
   handleCancel(i: PrivateMessageForm) {
index 291251d3304f2161a3a27b480b0ccd92d30e0401..7719e1e598429915c73d22e7dc5bb73b3e35e876 100644 (file)
@@ -1,10 +1,9 @@
 import { Component, linkEvent } from 'inferno';
 import { Prompt } from 'inferno-router';
+import { MarkdownTextArea } from './markdown-textarea';
 import { Site, SiteForm as SiteFormI } from '../interfaces';
 import { WebSocketService } from '../services';
-import { capitalizeFirstLetter, randomStr, setupTribute } from '../utils';
-import autosize from 'autosize';
-import Tribute from 'tributejs/src/Tribute.js';
+import { capitalizeFirstLetter, randomStr } from '../utils';
 import { i18n } from '../i18next';
 
 interface SiteFormProps {
@@ -19,7 +18,6 @@ interface SiteFormState {
 
 export class SiteForm extends Component<SiteFormProps, SiteFormState> {
   private id = `site-form-${randomStr()}`;
-  private tribute: Tribute;
   private emptyState: SiteFormState = {
     siteForm: {
       enable_downvotes: true,
@@ -33,8 +31,10 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
   constructor(props: any, context: any) {
     super(props, context);
 
-    this.tribute = setupTribute();
     this.state = this.emptyState;
+    this.handleSiteDescriptionChange = this.handleSiteDescriptionChange.bind(
+      this
+    );
 
     if (this.props.site) {
       this.state.siteForm = {
@@ -47,17 +47,6 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
     }
   }
 
-  componentDidMount() {
-    var textarea: any = document.getElementById(this.id);
-    autosize(textarea);
-    this.tribute.attach(textarea);
-    textarea.addEventListener('tribute-replaced', () => {
-      this.state.siteForm.description = textarea.value;
-      this.setState(this.state);
-      autosize.update(textarea);
-    });
-  }
-
   // Necessary to stop the loading
   componentWillReceiveProps() {
     this.state.loading = false;
@@ -119,13 +108,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
               {i18n.t('sidebar')}
             </label>
             <div class="col-12">
-              <textarea
-                id={this.id}
-                value={this.state.siteForm.description}
-                onInput={linkEvent(this, this.handleSiteDescriptionChange)}
-                class="form-control"
-                rows={3}
-                maxLength={10000}
+              <MarkdownTextArea
+                initialContent={this.state.siteForm.description}
+                onContentChange={this.handleSiteDescriptionChange}
               />
             </div>
           </div>
@@ -238,9 +223,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
     i.setState(i.state);
   }
 
-  handleSiteDescriptionChange(i: SiteForm, event: any) {
-    i.state.siteForm.description = event.target.value;
-    i.setState(i.state);
+  handleSiteDescriptionChange(val: string) {
+    this.state.siteForm.description = val;
+    this.setState(this.state);
   }
 
   handleSiteEnableNsfwChange(i: SiteForm, event: any) {
index 3386dbe59da38d9a68defebfd456f5101321e529..786f28683a60f1a1d7ef8ddeb9bb75b9c8edcb29 100644 (file)
@@ -15,6 +15,27 @@ export class Symbols extends Component<any, any> {
         xmlnsXlink="http://www.w3.org/1999/xlink"
       >
         <defs>
+          <symbol id="icon-strikethrough" viewBox="0 0 28 28">
+            <path d="M27.5 14c0.281 0 0.5 0.219 0.5 0.5v1c0 0.281-0.219 0.5-0.5 0.5h-27c-0.281 0-0.5-0.219-0.5-0.5v-1c0-0.281 0.219-0.5 0.5-0.5h27zM7.547 13c-0.297-0.375-0.562-0.797-0.797-1.25-0.5-1.016-0.75-2-0.75-2.938 0-1.906 0.703-3.5 2.094-4.828s3.437-1.984 6.141-1.984c0.594 0 1.453 0.109 2.609 0.297 0.688 0.125 1.609 0.375 2.766 0.75 0.109 0.406 0.219 1.031 0.328 1.844 0.141 1.234 0.219 2.187 0.219 2.859 0 0.219-0.031 0.453-0.078 0.703l-0.187 0.047-1.313-0.094-0.219-0.031c-0.531-1.578-1.078-2.641-1.609-3.203-0.922-0.953-2.031-1.422-3.281-1.422-1.188 0-2.141 0.313-2.844 0.922s-1.047 1.375-1.047 2.281c0 0.766 0.344 1.484 1.031 2.188s2.141 1.375 4.359 2.016c0.75 0.219 1.641 0.562 2.703 1.031 0.562 0.266 1.062 0.531 1.484 0.812h-11.609zM15.469 17h6.422c0.078 0.438 0.109 0.922 0.109 1.437 0 1.125-0.203 2.234-0.641 3.313-0.234 0.578-0.594 1.109-1.109 1.625-0.375 0.359-0.938 0.781-1.703 1.266-0.781 0.469-1.563 0.828-2.391 1.031-0.828 0.219-1.875 0.328-3.172 0.328-0.859 0-1.891-0.031-3.047-0.359l-2.188-0.625c-0.609-0.172-0.969-0.313-1.125-0.438-0.063-0.063-0.125-0.172-0.125-0.344v-0.203c0-0.125 0.031-0.938-0.031-2.438-0.031-0.781 0.031-1.328 0.031-1.641v-0.688l1.594-0.031c0.578 1.328 0.844 2.125 1.016 2.406 0.375 0.609 0.797 1.094 1.25 1.469s1 0.672 1.641 0.891c0.625 0.234 1.328 0.344 2.063 0.344 0.656 0 1.391-0.141 2.172-0.422 0.797-0.266 1.437-0.719 1.906-1.344 0.484-0.625 0.734-1.297 0.734-2.016 0-0.875-0.422-1.687-1.266-2.453-0.344-0.297-1.062-0.672-2.141-1.109z"></path>
+          </symbol>
+          <symbol id="icon-header" viewBox="0 0 28 28">
+            <path d="M26.281 26c-1.375 0-2.766-0.109-4.156-0.109-1.375 0-2.75 0.109-4.125 0.109-0.531 0-0.781-0.578-0.781-1.031 0-1.391 1.563-0.797 2.375-1.328 0.516-0.328 0.516-1.641 0.516-2.188l-0.016-6.109c0-0.172 0-0.328-0.016-0.484-0.25-0.078-0.531-0.063-0.781-0.063h-10.547c-0.266 0-0.547-0.016-0.797 0.063-0.016 0.156-0.016 0.313-0.016 0.484l-0.016 5.797c0 0.594 0 2.219 0.578 2.562 0.812 0.5 2.656-0.203 2.656 1.203 0 0.469-0.219 1.094-0.766 1.094-1.453 0-2.906-0.109-4.344-0.109-1.328 0-2.656 0.109-3.984 0.109-0.516 0-0.75-0.594-0.75-1.031 0-1.359 1.437-0.797 2.203-1.328 0.5-0.344 0.516-1.687 0.516-2.234l-0.016-0.891v-12.703c0-0.75 0.109-3.156-0.594-3.578-0.781-0.484-2.453 0.266-2.453-1.141 0-0.453 0.203-1.094 0.75-1.094 1.437 0 2.891 0.109 4.328 0.109 1.313 0 2.641-0.109 3.953-0.109 0.562 0 0.781 0.625 0.781 1.094 0 1.344-1.547 0.688-2.312 1.172-0.547 0.328-0.547 1.937-0.547 2.5l0.016 5c0 0.172 0 0.328 0.016 0.5 0.203 0.047 0.406 0.047 0.609 0.047h10.922c0.187 0 0.391 0 0.594-0.047 0.016-0.172 0.016-0.328 0.016-0.5l0.016-5c0-0.578 0-2.172-0.547-2.5-0.781-0.469-2.344 0.156-2.344-1.172 0-0.469 0.219-1.094 0.781-1.094 1.375 0 2.75 0.109 4.125 0.109 1.344 0 2.688-0.109 4.031-0.109 0.562 0 0.781 0.625 0.781 1.094 0 1.359-1.609 0.672-2.391 1.156-0.531 0.344-0.547 1.953-0.547 2.516l0.016 14.734c0 0.516 0.031 1.875 0.531 2.188 0.797 0.5 2.484-0.141 2.484 1.219 0 0.453-0.203 1.094-0.75 1.094z"></path>
+          </symbol>
+          <symbol id="icon-list" viewBox="0 0 24 24">
+            <path d="M8 7h13c0.552 0 1-0.448 1-1s-0.448-1-1-1h-13c-0.552 0-1 0.448-1 1s0.448 1 1 1zM8 13h13c0.552 0 1-0.448 1-1s-0.448-1-1-1h-13c-0.552 0-1 0.448-1 1s0.448 1 1 1zM8 19h13c0.552 0 1-0.448 1-1s-0.448-1-1-1h-13c-0.552 0-1 0.448-1 1s0.448 1 1 1zM3 7c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1zM3 13c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1zM3 19c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1z"></path>
+          </symbol>
+          <symbol id="icon-italic" viewBox="0 0 24 24">
+            <path d="M13.557 5l-5.25 14h-3.307c-0.552 0-1 0.448-1 1s0.448 1 1 1h9c0.552 0 1-0.448 1-1s-0.448-1-1-1h-3.557l5.25-14h3.307c0.552 0 1-0.448 1-1s-0.448-1-1-1h-9c-0.552 0-1 0.448-1 1s0.448 1 1 1z"></path>
+          </symbol>
+          <symbol id="icon-code" viewBox="0 0 24 24">
+            <path d="M16.707 18.707l6-6c0.391-0.391 0.391-1.024 0-1.414l-6-6c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0zM7.293 5.293l-6 6c-0.391 0.391-0.391 1.024 0 1.414l6 6c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0z"></path>
+          </symbol>
+          <symbol id="icon-bold" viewBox="0 0 24 24">
+            <path d="M7 11v-6h7c0.829 0 1.577 0.335 2.121 0.879s0.879 1.292 0.879 2.121-0.335 1.577-0.879 2.121-1.292 0.879-2.121 0.879zM5 12v8c0 0.552 0.448 1 1 1h9c1.38 0 2.632-0.561 3.536-1.464s1.464-2.156 1.464-3.536-0.561-2.632-1.464-3.536c-0.325-0.325-0.695-0.606-1.1-0.832 0.034-0.032 0.067-0.064 0.1-0.097 0.903-0.903 1.464-2.155 1.464-3.535s-0.561-2.632-1.464-3.536-2.156-1.464-3.536-1.464h-8c-0.552 0-1 0.448-1 1zM7 13h8c0.829 0 1.577 0.335 2.121 0.879s0.879 1.292 0.879 2.121-0.335 1.577-0.879 2.121-1.292 0.879-2.121 0.879h-8z"></path>
+          </symbol>
+          <symbol id="icon-format_quote" viewBox="0 0 24 24">
+            <path d="M14.016 17.016l1.969-4.031h-3v-6h6v6l-1.969 4.031h-3zM6 17.016l2.016-4.031h-3v-6h6v6l-2.016 4.031h-3z"></path>
+          </symbol>
           <symbol id="icon-settings" viewBox="0 0 24 24">
             <path d="M16 12c0-1.104-0.449-2.106-1.172-2.828s-1.724-1.172-2.828-1.172-2.106 0.449-2.828 1.172-1.172 1.724-1.172 2.828 0.449 2.106 1.172 2.828 1.724 1.172 2.828 1.172 2.106-0.449 2.828-1.172 1.172-1.724 1.172-2.828zM14 12c0 0.553-0.223 1.051-0.586 1.414s-0.861 0.586-1.414 0.586-1.051-0.223-1.414-0.586-0.586-0.861-0.586-1.414 0.223-1.051 0.586-1.414 0.861-0.586 1.414-0.586 1.051 0.223 1.414 0.586 0.586 0.861 0.586 1.414zM20.315 15.404c0.046-0.105 0.112-0.191 0.192-0.257 0.112-0.092 0.251-0.146 0.403-0.147h0.090c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121-0.337-1.58-0.879-2.121-1.293-0.879-2.121-0.879h-0.159c-0.11-0.001-0.215-0.028-0.308-0.076-0.127-0.066-0.23-0.172-0.292-0.312-0.003-0.029-0.004-0.059-0.004-0.089-0.024-0.055-0.040-0.111-0.049-0.168 0.020-0.334 0.077-0.454 0.168-0.547l0.062-0.062c0.585-0.586 0.878-1.356 0.877-2.122s-0.294-1.536-0.881-2.122c-0.586-0.585-1.356-0.878-2.122-0.877s-1.536 0.294-2.12 0.879l-0.046 0.046c-0.083 0.080-0.183 0.136-0.288 0.166-0.14 0.039-0.291 0.032-0.438-0.033-0.101-0.044-0.187-0.11-0.253-0.19-0.092-0.112-0.146-0.251-0.147-0.403v-0.090c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879-1.58 0.337-2.121 0.879-0.879 1.293-0.879 2.121v0.159c-0.001 0.11-0.028 0.215-0.076 0.308-0.066 0.127-0.172 0.23-0.312 0.292-0.029 0.003-0.059 0.004-0.089 0.004-0.055 0.024-0.111 0.040-0.168 0.049-0.335-0.021-0.455-0.078-0.548-0.169l-0.062-0.062c-0.586-0.585-1.355-0.878-2.122-0.878s-1.535 0.294-2.122 0.882c-0.585 0.586-0.878 1.355-0.878 2.122s0.294 1.536 0.879 2.12l0.048 0.047c0.080 0.083 0.136 0.183 0.166 0.288 0.039 0.14 0.032 0.291-0.031 0.434-0.006 0.016-0.013 0.034-0.021 0.052-0.041 0.109-0.108 0.203-0.191 0.275-0.11 0.095-0.25 0.153-0.383 0.156h-0.090c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.294-0.879 2.122 0.337 1.58 0.879 2.121 1.293 0.879 2.121 0.879h0.159c0.11 0.001 0.215 0.028 0.308 0.076 0.128 0.067 0.233 0.174 0.296 0.321 0.024 0.055 0.040 0.111 0.049 0.168-0.020 0.334-0.077 0.454-0.168 0.547l-0.062 0.062c-0.585 0.586-0.878 1.356-0.877 2.122s0.294 1.536 0.881 2.122c0.586 0.585 1.356 0.878 2.122 0.877s1.536-0.294 2.12-0.879l0.047-0.048c0.083-0.080 0.183-0.136 0.288-0.166 0.14-0.039 0.291-0.032 0.434 0.031 0.016 0.006 0.034 0.013 0.052 0.021 0.109 0.041 0.203 0.108 0.275 0.191 0.095 0.11 0.153 0.25 0.156 0.383v0.092c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879 1.58-0.337 2.121-0.879 0.879-1.293 0.879-2.121v-0.159c0.001-0.11 0.028-0.215 0.076-0.308 0.067-0.128 0.174-0.233 0.321-0.296 0.055-0.024 0.111-0.040 0.168-0.049 0.334 0.020 0.454 0.077 0.547 0.168l0.062 0.062c0.586 0.585 1.356 0.878 2.122 0.877s1.536-0.294 2.122-0.881c0.585-0.586 0.878-1.356 0.877-2.122s-0.294-1.536-0.879-2.12l-0.048-0.047c-0.080-0.083-0.136-0.183-0.166-0.288-0.039-0.14-0.032-0.291 0.031-0.434zM18.396 9.302c-0.012-0.201-0.038-0.297-0.076-0.382v0.080c0 0.043 0.003 0.084 0.008 0.125 0.021 0.060 0.043 0.119 0.068 0.177 0.004 0.090 0.005 0.091 0.005 0.092 0.249 0.581 0.684 1.030 1.208 1.303 0.371 0.193 0.785 0.298 1.211 0.303h0.18c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707-0.111 0.525-0.293 0.707-0.431 0.293-0.707 0.293h-0.090c-0.637 0.003-1.22 0.228-1.675 0.603-0.323 0.266-0.581 0.607-0.75 0.993-0.257 0.582-0.288 1.21-0.127 1.782 0.119 0.423 0.341 0.814 0.652 1.136l0.072 0.073c0.196 0.196 0.294 0.45 0.294 0.707s-0.097 0.512-0.292 0.707c-0.197 0.197-0.451 0.295-0.709 0.295s-0.512-0.097-0.707-0.292l-0.061-0.061c-0.463-0.453-1.040-0.702-1.632-0.752-0.437-0.037-0.882 0.034-1.293 0.212-0.578 0.248-1.027 0.683-1.3 1.206-0.193 0.371-0.298 0.785-0.303 1.211v0.181c0 0.276-0.111 0.525-0.293 0.707s-0.43 0.292-0.706 0.292-0.525-0.111-0.707-0.293-0.293-0.431-0.293-0.707v-0.090c-0.015-0.66-0.255-1.242-0.644-1.692-0.284-0.328-0.646-0.585-1.058-0.744-0.575-0.247-1.193-0.274-1.756-0.116-0.423 0.119-0.814 0.341-1.136 0.652l-0.073 0.072c-0.196 0.196-0.45 0.294-0.707 0.294s-0.512-0.097-0.707-0.292c-0.197-0.197-0.295-0.451-0.295-0.709s0.097-0.512 0.292-0.707l0.061-0.061c0.453-0.463 0.702-1.040 0.752-1.632 0.037-0.437-0.034-0.882-0.212-1.293-0.248-0.578-0.683-1.027-1.206-1.3-0.371-0.193-0.785-0.298-1.211-0.303l-0.18 0.001c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707 0.111-0.525 0.293-0.707 0.431-0.293 0.707-0.293h0.090c0.66-0.015 1.242-0.255 1.692-0.644 0.328-0.284 0.585-0.646 0.744-1.058 0.247-0.575 0.274-1.193 0.116-1.756-0.119-0.423-0.341-0.814-0.652-1.136l-0.073-0.073c-0.196-0.196-0.294-0.45-0.294-0.707s0.097-0.512 0.292-0.707c0.197-0.197 0.451-0.295 0.709-0.295s0.512 0.097 0.707 0.292l0.061 0.061c0.463 0.453 1.040 0.702 1.632 0.752 0.37 0.032 0.745-0.014 1.101-0.137 0.096-0.012 0.186-0.036 0.266-0.072-0.031 0.001-0.061 0.003-0.089 0.004-0.201 0.012-0.297 0.038-0.382 0.076h0.080c0.043 0 0.084-0.003 0.125-0.008 0.060-0.021 0.119-0.043 0.177-0.068 0.090-0.004 0.091-0.005 0.092-0.005 0.581-0.249 1.030-0.684 1.303-1.208 0.193-0.37 0.298-0.785 0.303-1.21v-0.181c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293 0.525 0.111 0.707 0.293 0.293 0.431 0.293 0.707v0.090c0.003 0.637 0.228 1.22 0.603 1.675 0.266 0.323 0.607 0.581 0.996 0.751 0.578 0.255 1.206 0.286 1.778 0.125 0.423-0.119 0.814-0.341 1.136-0.652l0.073-0.072c0.196-0.196 0.45-0.294 0.707-0.294s0.512 0.097 0.707 0.292c0.197 0.197 0.295 0.451 0.295 0.709s-0.097 0.512-0.292 0.707l-0.061 0.061c-0.453 0.463-0.702 1.040-0.752 1.632-0.032 0.37 0.014 0.745 0.137 1.101 0.012 0.095 0.037 0.185 0.072 0.266-0.001-0.032-0.002-0.062-0.004-0.089z"></path>
           </symbol>
index 90c4a9959cb58a82560511ac2ebd465cbde28154..6e111c63914ad9d98c2720d61d0159c13aac4e39 100644 (file)
     "unsticky": "unsticky",
     "link": "link",
     "archive_link": "archive link",
+    "bold": "bold",
+    "italic": "italic",
+    "header": "header",
+    "strikethrough": "strikethrough",
+    "quote": "quote",
+    "spoiler": "spoiler",
+    "list": "list",
     "mod": "mod",
     "mods": "mods",
     "moderates": "Moderates",
     "email": "Email",
     "matrix_user_id": "Matrix User",
     "private_message_disclaimer":
-      "Warning: Private messages in Lemmy are not secure. Please create an account on <1>Riot.im</1> for secure messaging.",
+      "Warning: Private messages in Lemmy are not secure. Please create an account on <1>Element.io</1> for secure messaging.",
     "send_notifications_to_email": "Send notifications to Email",
     "optional": "Optional",
     "expires": "Expires",