summaryrefslogtreecommitdiffstats
path: root/ui/src/components
diff options
context:
space:
mode:
authorderek <wwsage@gmail.com>2020-07-13 20:09:39 -0400
committerderek <wwsage@gmail.com>2020-07-13 20:09:39 -0400
commitd71897620ceea58b2d314242ea2f646a466d338e (patch)
tree8b32a1e5f3e06e7c2a6be03827730bb8fdb0679e /ui/src/components
parente7b9ab1b3a4926d5d32ed9e87792cc9d39d4ede5 (diff)
parent52983907c4d1b7fda1182316cb631f9b5e913f5b (diff)
Merge remote-tracking branch 'LemmyNet/master'
Diffstat (limited to 'ui/src/components')
-rw-r--r--ui/src/components/cake-day.tsx25
-rw-r--r--ui/src/components/comment-form.tsx259
-rw-r--r--ui/src/components/comment-node.tsx32
-rw-r--r--ui/src/components/communities.tsx2
-rw-r--r--ui/src/components/community.tsx2
-rw-r--r--ui/src/components/create-community.tsx7
-rw-r--r--ui/src/components/create-post.tsx7
-rw-r--r--ui/src/components/create-private-message.tsx7
-rw-r--r--ui/src/components/inbox.tsx33
-rw-r--r--ui/src/components/main.tsx12
-rw-r--r--ui/src/components/post-form.tsx85
-rw-r--r--ui/src/components/post-listing.tsx11
-rw-r--r--ui/src/components/post.tsx173
-rw-r--r--ui/src/components/search.tsx52
-rw-r--r--ui/src/components/symbols.tsx3
-rw-r--r--ui/src/components/user-listing.tsx35
-rw-r--r--ui/src/components/user.tsx57
17 files changed, 495 insertions, 307 deletions
diff --git a/ui/src/components/cake-day.tsx b/ui/src/components/cake-day.tsx
new file mode 100644
index 00000000..f28be33c
--- /dev/null
+++ b/ui/src/components/cake-day.tsx
@@ -0,0 +1,25 @@
+import { Component } from 'inferno';
+import { i18n } from '../i18next';
+
+interface CakeDayProps {
+ creatorName: string;
+}
+
+export class CakeDay extends Component<CakeDayProps, any> {
+ render() {
+ return (
+ <div
+ className={`mx-2 d-inline-block unselectable pointer`}
+ data-tippy-content={this.cakeDayTippy()}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-cake"></use>
+ </svg>
+ </div>
+ );
+ }
+
+ cakeDayTippy(): string {
+ return i18n.t('cake_day_info', { creator_name: this.props.creatorName });
+ }
+}
diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx
index 770c127c..00b4fe1e 100644
--- a/ui/src/components/comment-form.tsx
+++ b/ui/src/components/comment-form.tsx
@@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { Prompt } from 'inferno-router';
@@ -17,7 +18,6 @@ import {
toast,
setupTribute,
wsJsonToRes,
- emojiPicker,
pictrsDeleteToast,
} from '../utils';
import { WebSocketService, UserService } from '../services';
@@ -25,6 +25,7 @@ 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';
interface CommentFormProps {
postId?: number;
@@ -32,6 +33,7 @@ interface CommentFormProps {
onReplyCancel?(): any;
edit?: boolean;
disabled?: boolean;
+ focus?: boolean;
}
interface CommentFormState {
@@ -72,7 +74,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
super(props, context);
this.tribute = setupTribute();
- this.setupEmojiPicker();
this.state = this.emptyState;
@@ -98,14 +99,34 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
}
componentDidMount() {
- var textarea: any = document.getElementById(this.id);
- autosize(textarea);
- this.tribute.attach(textarea);
- textarea.addEventListener('tribute-replaced', () => {
- this.state.commentForm.content = textarea.value;
- this.setState(this.state);
- autosize.update(textarea);
- });
+ 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() {
@@ -128,133 +149,123 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
when={this.state.commentForm.content}
message={i18n.t('block_leaving')}
/>
- <form
- id={this.formId}
- onSubmit={linkEvent(this, this.handleCommentSubmit)}
- >
- <div class="form-group row">
- <div className={`col-sm-12`}>
- <textarea
- id={this.id}
- className={`form-control ${this.state.previewMode && 'd-none'}`}
- value={this.state.commentForm.content}
- onInput={linkEvent(this, this.handleCommentContentChange)}
- onPaste={linkEvent(this, this.handleImageUploadPaste)}
- required
- disabled={this.props.disabled}
- rows={2}
- maxLength={10000}
- />
- {this.state.previewMode && (
- <div
- className="card card-body md-div"
- dangerouslySetInnerHTML={mdToHtml(
- this.state.commentForm.content
- )}
+ {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}
/>
- )}
- </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>
+ {this.state.previewMode && (
+ <div
+ className="card card-body md-div"
+ dangerouslySetInnerHTML={mdToHtml(
+ this.state.commentForm.content
+ )}
+ />
)}
- </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 && (
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-sm-12">
<button
- type="button"
+ type="submit"
class="btn btn-sm btn-secondary mr-2"
- onClick={linkEvent(this, this.handleReplyCancel)}
+ disabled={this.props.disabled || this.state.loading}
>
- {i18n.t('cancel')}
+ {this.state.loading ? (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ ) : (
+ <span>{this.state.buttonTitle}</span>
+ )}
</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')}
+ {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-image"></use>
+ <use xlinkHref="#icon-help-circle"></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>
- )}
- <span
- onClick={linkEvent(this, this.handleEmojiPickerClick)}
- class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold"
- data-tippy-content={i18n.t('emoji_picker')}
- >
- <svg class="icon icon-inline">
- <use xlinkHref="#icon-smile"></use>
- </svg>
- </span>
+ </a>
+ <form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
+ <label
+ htmlFor={`file-upload-${this.id}`}
+ className={`${UserService.Instance.user && 'pointer'}`}
+ data-tippy-content={i18n.t('upload_image')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-image"></use>
+ </svg>
+ </label>
+ <input
+ id={`file-upload-${this.id}`}
+ type="file"
+ accept="image/*,video/*"
+ name="file"
+ class="d-none"
+ disabled={!UserService.Instance.user}
+ onChange={linkEvent(this, this.handleImageUpload)}
+ />
+ </form>
+ {this.state.imageLoading && (
+ <svg class="icon icon-spinner spin">
+ <use xlinkHref="#icon-spinner"></use>
+ </svg>
+ )}
+ </div>
</div>
+ </form>
+ ) : (
+ <div class="alert alert-warning" role="alert">
+ <svg class="icon icon-inline mr-2">
+ <use xlinkHref="#icon-alert-triangle"></use>
+ </svg>
+ <T i18nKey="must_login" class="d-inline">
+ #<Link to="/login">#</Link>
+ </T>
</div>
- </form>
+ )}
</div>
);
}
- setupEmojiPicker() {
- emojiPicker.on('emoji', twemojiHtmlStr => {
- if (this.state.commentForm.content == null) {
- this.state.commentForm.content = '';
- }
- var el = document.createElement('div');
- el.innerHTML = twemojiHtmlStr;
- let nativeUnicode = (el.childNodes[0] as HTMLElement).getAttribute('alt');
- let shortName = `:${emojiShortName[nativeUnicode]}:`;
- this.state.commentForm.content += shortName;
- this.setState(this.state);
- });
- }
-
handleFinished(op: UserOperation, data: CommentResponse) {
let isReply =
this.props.node !== undefined && data.comment.parent_id !== null;
@@ -302,10 +313,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
i.setState(i.state);
}
- handleEmojiPickerClick(_i: CommentForm, event: any) {
- emojiPicker.togglePicker(event.target);
- }
-
handleCommentContentChange(i: CommentForm, event: any) {
i.state.commentForm.content = event.target.value;
i.setState(i.state);
diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx
index 839a01dc..a6b9b7ba 100644
--- a/ui/src/components/comment-node.tsx
+++ b/ui/src/components/comment-node.tsx
@@ -32,6 +32,7 @@ import { MomentTime } from './moment-time';
import { CommentForm } from './comment-form';
import { CommentNodes } from './comment-nodes';
import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
import { i18n } from '../i18next';
interface CommentNodeState {
@@ -158,9 +159,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
id: node.comment.creator_id,
local: node.comment.creator_local,
actor_id: node.comment.creator_actor_id,
+ published: node.comment.creator_published,
}}
/>
</span>
+
{this.isMod && (
<div className="badge badge-light d-none d-sm-inline mr-2">
{i18n.t('mod')}
@@ -184,13 +187,22 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{this.props.showCommunity && (
<>
<span class="mx-1">{i18n.t('to')}</span>
- <Link class="mr-2" to={`/c/${node.comment.community_name}`}>
- {node.comment.community_name}
+ <CommunityLink
+ community={{
+ name: node.comment.community_name,
+ id: node.comment.community_id,
+ local: node.comment.community_local,
+ actor_id: node.comment.community_actor_id,
+ }}
+ />
+ <span class="mx-2">•</span>
+ <Link class="mr-2" to={`/post/${node.comment.post_id}`}>
+ {node.comment.post_name}
</Link>
</>
)}
- <div
- className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"
+ <button
+ class="btn btn-sm text-muted"
onClick={linkEvent(this, this.handleCommentCollapse)}
>
{this.state.collapsed ? (
@@ -202,9 +214,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<use xlinkHref="#icon-minus-square"></use>
</svg>
)}
- </div>
- <span
- className={`unselectable pointer ${this.scoreColor}`}
+ </button>
+ {/* This is an expanding spacer for mobile */}
+ <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
+ <button
+ className={`btn btn-sm p-0 unselectable pointer ${this.scoreColor}`}
onClick={linkEvent(node, this.handleCommentUpvote)}
data-tippy-content={this.pointsTippy}
>
@@ -212,7 +226,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<use xlinkHref="#icon-zap"></use>
</svg>
<span class="mr-1">{this.state.score}</span>
- </span>
+ </button>
<span className="mr-1">•</span>
<span>
<MomentTime data={node.comment} />
@@ -225,6 +239,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
edit
onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked}
+ focus
/>
)}
{!this.state.showEdit && !this.state.collapsed && (
@@ -693,6 +708,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
node={node}
onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked}
+ focus
/>
)}
{node.children && !this.state.collapsed && (
diff --git a/ui/src/components/communities.tsx b/ui/src/components/communities.tsx
index 441f7bb1..10a3ab80 100644
--- a/ui/src/components/communities.tsx
+++ b/ui/src/components/communities.tsx
@@ -160,7 +160,7 @@ export class Communities extends Component<any, CommunitiesState> {
</button>
)}
- {this.state.communities.length == communityLimit && (
+ {this.state.communities.length > 0 && (
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx
index ff50c3dc..fc999b25 100644
--- a/ui/src/components/community.tsx
+++ b/ui/src/components/community.tsx
@@ -260,7 +260,7 @@ export class Community extends Component<any, State> {
{i18n.t('prev')}
</button>
)}
- {this.state.posts.length == fetchLimit && (
+ {this.state.posts.length > 0 && (
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
diff --git a/ui/src/components/create-community.tsx b/ui/src/components/create-community.tsx
index 86929894..3a5d943d 100644
--- a/ui/src/components/create-community.tsx
+++ b/ui/src/components/create-community.tsx
@@ -9,7 +9,7 @@ import {
GetSiteResponse,
} from '../interfaces';
import { toast, wsJsonToRes } from '../utils';
-import { WebSocketService } from '../services';
+import { WebSocketService, UserService } from '../services';
import { i18n } from '../i18next';
interface CreateCommunityState {
@@ -26,6 +26,11 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
this.state = this.emptyState;
+ if (!UserService.Instance.user) {
+ toast(i18n.t('not_logged_in'), 'danger');
+ this.context.router.history.push(`/login`);
+ }
+
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
diff --git a/ui/src/components/create-post.tsx b/ui/src/components/create-post.tsx
index 348ba0cb..4554326d 100644
--- a/ui/src/components/create-post.tsx
+++ b/ui/src/components/create-post.tsx
@@ -3,7 +3,7 @@ import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { PostForm } from './post-form';
import { toast, wsJsonToRes } from '../utils';
-import { WebSocketService } from '../services';
+import { WebSocketService, UserService } from '../services';
import {
UserOperation,
PostFormParams,
@@ -41,6 +41,11 @@ export class CreatePost extends Component<any, CreatePostState> {
this.handlePostCreate = this.handlePostCreate.bind(this);
this.state = this.emptyState;
+ if (!UserService.Instance.user) {
+ toast(i18n.t('not_logged_in'), 'danger');
+ this.context.router.history.push(`/login`);
+ }
+
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
diff --git a/ui/src/components/create-private-message.tsx b/ui/src/components/create-private-message.tsx
index 21ed04c7..c309cbe3 100644
--- a/ui/src/components/create-private-message.tsx
+++ b/ui/src/components/create-private-message.tsx
@@ -2,7 +2,7 @@ import { Component } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { PrivateMessageForm } from './private-message-form';
-import { WebSocketService } from '../services';
+import { WebSocketService, UserService } from '../services';
import {
UserOperation,
WebSocketJsonResponse,
@@ -20,6 +20,11 @@ export class CreatePrivateMessage extends Component<any, any> {
this
);
+ if (!UserService.Instance.user) {
+ toast(i18n.t('not_logged_in'), 'danger');
+ this.context.router.history.push(`/login`);
+ }
+
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx
index a88d45c5..8e148921 100644
--- a/ui/src/components/inbox.tsx
+++ b/ui/src/components/inbox.tsx
@@ -267,6 +267,7 @@ export class Inbox extends Component<any, InboxState> {
nodes={[{ comment: i }]}
noIndent
markable
+ showCommunity
showContext
enableDownvotes={this.state.enableDownvotes}
/>
@@ -285,6 +286,7 @@ export class Inbox extends Component<any, InboxState> {
nodes={commentsToFlatNodes(this.state.replies)}
noIndent
markable
+ showCommunity
showContext
enableDownvotes={this.state.enableDownvotes}
/>
@@ -300,6 +302,7 @@ export class Inbox extends Component<any, InboxState> {
nodes={[{ comment: mention }]}
noIndent
markable
+ showCommunity
showContext
enableDownvotes={this.state.enableDownvotes}
/>
@@ -329,12 +332,14 @@ export class Inbox extends Component<any, InboxState> {
{i18n.t('prev')}
</button>
)}
- <button
- class="btn btn-sm btn-secondary"
- onClick={linkEvent(this, this.nextPage)}
- >
- {i18n.t('next')}
- </button>
+ {this.unreadCount() > 0 && (
+ <button
+ class="btn btn-sm btn-secondary"
+ onClick={linkEvent(this, this.nextPage)}
+ >
+ {i18n.t('next')}
+ </button>
+ )}
</div>
);
}
@@ -534,15 +539,19 @@ export class Inbox extends Component<any, InboxState> {
}
sendUnreadCount() {
- let count =
+ UserService.Instance.user.unreadCount = this.unreadCount();
+ UserService.Instance.sub.next({
+ user: UserService.Instance.user,
+ });
+ }
+
+ unreadCount(): number {
+ return (
this.state.replies.filter(r => !r.read).length +
this.state.mentions.filter(r => !r.read).length +
this.state.messages.filter(
r => !r.read && r.creator_id !== UserService.Instance.user.id
- ).length;
- UserService.Instance.user.unreadCount = count;
- UserService.Instance.sub.next({
- user: UserService.Instance.user,
- });
+ ).length
+ );
}
}
diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx
index 20735049..9063a039 100644
--- a/ui/src/components/main.tsx
+++ b/ui/src/components/main.tsx
@@ -373,17 +373,21 @@ export class Main extends Component<any, MainState> {
#
</a>
<a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
- <br></br>
+ <br class="big"></br>
<code>#</code>
<br></br>
<b>#</b>
- <br></br>
+ <br class="big"></br>
<a href={repoUrl}>#</a>
- <br></br>
+ <br class="big"></br>
<a href="https://www.rust-lang.org">#</a>
<a href="https://actix.rs/">#</a>
<a href="https://infernojs.org">#</a>
<a href="https://www.typescriptlang.org/">#</a>
+ <br class="big"></br>
+ <a href="https://github.com/LemmyNet/lemmy/graphs/contributors?type=a">
+ #
+ </a>
</T>
</p>
</div>
@@ -493,7 +497,7 @@ export class Main extends Component<any, MainState> {
{i18n.t('prev')}
</button>
)}
- {this.state.posts.length == fetchLimit && (
+ {this.state.posts.length > 0 && (
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
diff --git a/ui/src/components/post-form.tsx b/ui/src/components/post-form.tsx
index a88d38c7..e5efeaac 100644
--- a/ui/src/components/post-form.tsx
+++ b/ui/src/components/post-form.tsx
@@ -33,14 +33,14 @@ import {
randomStr,
setupTribute,
setupTippy,
- emojiPicker,
hostname,
pictrsDeleteToast,
+ validTitle,
} from '../utils';
import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js';
import emojiShortName from 'emoji-short-name';
-import Selectr from 'mobius1-selectr';
+import Choices from 'choices.js';
import { i18n } from '../i18next';
const MAX_POST_TITLE_LENGTH = 200;
@@ -70,6 +70,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
private id = `post-form-${randomStr()}`;
private tribute: Tribute;
private subscription: Subscription;
+ private choices: Choices;
private emptyState: PostFormState = {
postForm: {
name: null,
@@ -95,7 +96,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
this.tribute = setupTribute();
- this.setupEmojiPicker();
this.state = this.emptyState;
@@ -166,6 +166,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
componentWillUnmount() {
this.subscription.unsubscribe();
+ this.choices && this.choices.destroy();
window.onbeforeunload = null;
}
@@ -271,12 +272,19 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
value={this.state.postForm.name}
id="post-title"
onInput={linkEvent(this, this.handlePostNameChange)}
- class="form-control"
+ class={`form-control ${
+ !validTitle(this.state.postForm.name) && 'is-invalid'
+ }`}
required
rows={2}
minLength={3}
maxLength={MAX_POST_TITLE_LENGTH}
/>
+ {!validTitle(this.state.postForm.name) && (
+ <div class="invalid-feedback">
+ {i18n.t('invalid_post_title')}
+ </div>
+ )}
{this.state.suggestedPosts.length > 0 && (
<>
<div class="my-1 text-muted small font-weight-bold">
@@ -332,15 +340,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<use xlinkHref="#icon-help-circle"></use>
</svg>
</a>
- <span
- onClick={linkEvent(this, this.handleEmojiPickerClick)}
- class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold"
- data-tippy-content={i18n.t('emoji_picker')}
- >
- <svg class="icon icon-inline">
- <use xlinkHref="#icon-smile"></use>
- </svg>
- </span>
</div>
</div>
{!this.props.post && (
@@ -420,20 +419,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
);
}
- setupEmojiPicker() {
- emojiPicker.on('emoji', twemojiHtmlStr => {
- if (this.state.postForm.body == null) {
- this.state.postForm.body = '';
- }
- var el = document.createElement('div');
- el.innerHTML = twemojiHtmlStr;
- let nativeUnicode = (el.childNodes[0] as HTMLElement).getAttribute('alt');
- let shortName = `:${emojiShortName[nativeUnicode]}:`;
- this.state.postForm.body += shortName;
- this.setState(this.state);
- });
- }
-
handlePostSubmit(i: PostForm, event: any) {
event.preventDefault();
@@ -596,10 +581,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
});
}
- handleEmojiPickerClick(_i: PostForm, event: any) {
- emojiPicker.togglePicker(event.target);
- }
-
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
if (msg.error) {
@@ -625,11 +606,45 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
// Set up select searching
let selectId: any = document.getElementById('post-community');
if (selectId) {
- let selector = new Selectr(selectId, { nativeDropdown: false });
- selector.on('selectr.select', option => {
- this.state.postForm.community_id = Number(option.value);
- this.setState(this.state);
+ this.choices = new Choices(selectId, {
+ shouldSort: false,
+ classNames: {
+ containerOuter: 'choices',
+ containerInner: 'choices__inner bg-secondary border-0',
+ input: 'form-control',
+ inputCloned: 'choices__input--cloned',
+ list: 'choices__list',
+ listItems: 'choices__list--multiple',
+ listSingle: 'choices__list--single',
+ listDropdown: 'choices__list--dropdown',
+ item: 'choices__item bg-secondary',
+ itemSelectable: 'choices__item--selectable',
+ itemDisabled: 'choices__item--disabled',
+ itemChoice: 'choices__item--choice',
+ placeholder: 'choices__placeholder',
+ group: 'choices__group',
+ groupHeading: 'choices__heading',
+ button: 'choices__button',
+ activeState: 'is-active',
+ focusSta