From 4d329c8da508fb6c73edbcad17338ad81ccab3b1 Mon Sep 17 00:00:00 2001
From: Dessalines <tyhou13@gmx.com>
Date: Thu, 29 Aug 2019 20:11:29 -0700
Subject: Adding support for mentions via tribute.

- Adding mentions in comment textareas for users (@) and communities
(#). Fixes #129
- Removing balloon-css
---
 ui/src/components/comment-form.tsx | 111 +++++++++++++++++++++++++++++++++++--
 1 file changed, 107 insertions(+), 4 deletions(-)

(limited to 'ui/src/components/comment-form.tsx')

diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx
index 7a75d9e4..b26fe0c9 100644
--- a/ui/src/components/comment-form.tsx
+++ b/ui/src/components/comment-form.tsx
@@ -1,10 +1,12 @@
 import { Component, linkEvent } from 'inferno';
-import { CommentNode as CommentNodeI, CommentForm as CommentFormI } from '../interfaces';
-import { capitalizeFirstLetter } from '../utils';
+import { CommentNode as CommentNodeI, CommentForm as CommentFormI, SearchForm, SearchType, SortType, UserOperation, SearchResponse } from '../interfaces';
+import { Subscription } from "rxjs";
+import { capitalizeFirstLetter, fetchLimit, msgOp } from '../utils';
 import { WebSocketService, UserService } from '../services';
 import * as autosize from 'autosize';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
+import * as tributejs from 'tributejs';
 
 interface CommentFormProps {
   postId?: number;
@@ -21,6 +23,10 @@ interface CommentFormState {
 
 export class CommentForm extends Component<CommentFormProps, CommentFormState> {
 
+  private id = `comment-form-${btoa(Math.random()).substring(0,12)}`;
+  private userSub: Subscription;
+  private communitySub: Subscription;
+  private tribute: any;
   private emptyState: CommentFormState = {
     commentForm: {
       auth: null,
@@ -34,6 +40,34 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
   constructor(props: any, context: any) {
     super(props, context);
 
+    this.tribute = new tributejs({
+      collection: [
+
+        // Users
+        {
+          trigger: '@',
+          selectTemplate: (item: any) => {
+            return `[/u/${item.original.key}](${window.location.origin}/u/${item.original.key})`;
+          },
+          values: (text: string, cb: any) => {
+            this.userSearch(text, users => cb(users));
+          },
+          autocompleteMode: true,
+        },
+
+        // Communities
+        {
+          trigger: '#',
+          selectTemplate: (item: any) => {
+            return `[/c/${item.original.key}](${window.location.origin}/c/${item.original.key})`;
+          },
+          values: (text: string, cb: any) => {
+            this.communitySearch(text, communities => cb(communities));
+          },
+          autocompleteMode: true,
+        }
+      ]
+    });
 
     this.state = this.emptyState;
 
@@ -51,7 +85,14 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
   }
 
   componentDidMount() {
-    autosize(document.querySelectorAll('textarea'));
+    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);
+    });
   }
 
   render() {
@@ -60,7 +101,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
         <form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
           <div class="form-group row">
             <div class="col-sm-12">
-              <textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} required disabled={this.props.disabled} rows={2} maxLength={10000} />
+              <textarea id={this.id} class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} required disabled={this.props.disabled} rows={2} maxLength={10000} />
             </div>
           </div>
           <div class="row">
@@ -100,4 +141,66 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
   handleReplyCancel(i: CommentForm) {
     i.props.onReplyCancel();
   }
+  
+  userSearch(text: string, cb: any) {
+    if (text) {
+      let form: SearchForm = {
+        q: text,
+        type_: SearchType[SearchType.Users],
+        sort: SortType[SortType.TopAll],
+        page: 1,
+        limit: fetchLimit,
+      };
+
+      WebSocketService.Instance.search(form);
+
+      this.userSub = WebSocketService.Instance.subject
+      .subscribe(
+        (msg) => {  
+          let op: UserOperation = msgOp(msg);
+          if (op == UserOperation.Search) {
+            let res: SearchResponse = msg;
+            let users = res.users.map(u => {return {key: u.name}});
+            cb(users);
+            this.userSub.unsubscribe();
+          }
+        },
+        (err) => console.error(err),
+        () => console.log('complete')
+      );
+    } else {
+      cb([]);
+    }
+  }
+
+  communitySearch(text: string, cb: any) {
+    if (text) {
+      let form: SearchForm = {
+        q: text,
+        type_: SearchType[SearchType.Communities],
+        sort: SortType[SortType.TopAll],
+        page: 1,
+        limit: fetchLimit,
+      };
+
+      WebSocketService.Instance.search(form);
+
+      this.communitySub = WebSocketService.Instance.subject
+      .subscribe(
+        (msg) => {  
+          let op: UserOperation = msgOp(msg);
+          if (op == UserOperation.Search) {
+            let res: SearchResponse = msg;
+            let communities = res.communities.map(u => {return {key: u.name}});
+            cb(communities);
+            this.communitySub.unsubscribe();
+          }
+        },
+        (err) => console.error(err),
+        () => console.log('complete')
+      );
+    } else {
+      cb([]);
+    }
+  }
 }
-- 
cgit v1.2.3

