-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,
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;
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: {
: 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;
);
}
- 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">
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) {
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
CommunityFormState
> {
private id = `community-form-${randomStr()}`;
- private tribute: Tribute;
private subscription: Subscription;
private emptyState: CommunityFormState = {
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,
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 &&
{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>
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) {
--- /dev/null
+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);
+ }
+ }
+}
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 {
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';
export class PostForm extends Component<PostFormProps, PostFormState> {
private id = `post-form-${randomStr()}`;
- private tribute: Tribute;
private subscription: Subscription;
private choices: Choices;
private emptyState: 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;
}
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();
}
{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 && (
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) {
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';
PrivateMessageFormProps,
PrivateMessageFormState
> {
- private id = `message-form-${randomStr()}`;
- private tribute: Tribute;
private subscription: Subscription;
private emptyState: PrivateMessageFormState = {
privateMessageForm: {
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,
}
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();
}
</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>
class="alert-link"
target="_blank"
rel="noopener"
- href="https://about.riot.im/"
+ href="https://element.io/get-started"
>
#
</a>
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"
</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>
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) {
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 {
export class SiteForm extends Component<SiteFormProps, SiteFormState> {
private id = `site-form-${randomStr()}`;
- private tribute: Tribute;
private emptyState: SiteFormState = {
siteForm: {
enable_downvotes: true,
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 = {
}
}
- 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;
{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>
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) {
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>
"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",