1 import { Component, linkEvent } from 'inferno';
2 import { CommentNode as CommentNodeI, CommentForm as CommentFormI, SearchForm, SearchType, SortType, UserOperation, SearchResponse } from '../interfaces';
3 import { Subscription } from "rxjs";
4 import { capitalizeFirstLetter, mentionDropdownFetchLimit, msgOp, mdToHtml, randomStr, markdownHelpUrl } from '../utils';
5 import { WebSocketService, UserService } from '../services';
6 import * as autosize from 'autosize';
7 import { i18n } from '../i18next';
8 import { T } from 'inferno-i18next';
9 import Tribute from 'tributejs/src/Tribute.js';
10 import * as emojiShortName from 'emoji-short-name';
12 interface CommentFormProps {
15 onReplyCancel?(): any;
20 interface CommentFormState {
21 commentForm: CommentFormI;
24 imageLoading: boolean;
27 export class CommentForm extends Component<CommentFormProps, CommentFormState> {
29 private id = `comment-form-${randomStr()}`;
30 private userSub: Subscription;
31 private communitySub: Subscription;
33 private emptyState: CommentFormState = {
37 post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId,
38 creator_id: UserService.Instance.user ? UserService.Instance.user.id : null,
40 buttonTitle: !this.props.node ? capitalizeFirstLetter(i18n.t('post')) : this.props.edit ? capitalizeFirstLetter(i18n.t('edit')) : capitalizeFirstLetter(i18n.t('reply')),
45 constructor(props: any, context: any) {
46 super(props, context);
48 this.tribute = new Tribute({
53 menuItemTemplate: (item: any) => {
54 let emoji = `:${item.original.key}:`;
55 return `${item.original.val} ${emoji}`;
57 selectTemplate: (item: any) => {
58 return `:${item.original.key}:`;
60 values: Object.entries(emojiShortName).map(e => {return {'key': e[1], 'val': e[0]}}),
62 autocompleteMode: true,
63 menuItemLimit: mentionDropdownFetchLimit,
68 selectTemplate: (item: any) => {
69 return `[/u/${item.original.key}](/u/${item.original.key})`;
71 values: (text: string, cb: any) => {
72 this.userSearch(text, (users: any) => cb(users));
75 autocompleteMode: true,
76 menuItemLimit: mentionDropdownFetchLimit,
82 selectTemplate: (item: any) => {
83 return `[/c/${item.original.key}](/c/${item.original.key})`;
85 values: (text: string, cb: any) => {
86 this.communitySearch(text, (communities: any) => cb(communities));
89 autocompleteMode: true,
90 menuItemLimit: mentionDropdownFetchLimit,
95 this.state = this.emptyState;
97 if (this.props.node) {
98 if (this.props.edit) {
99 this.state.commentForm.edit_id = this.props.node.comment.id;
100 this.state.commentForm.parent_id = this.props.node.comment.parent_id;
101 this.state.commentForm.content = this.props.node.comment.content;
102 this.state.commentForm.creator_id = this.props.node.comment.creator_id;
104 // A reply gets a new parent id
105 this.state.commentForm.parent_id = this.props.node.comment.id;
110 componentDidMount() {
111 var textarea: any = document.getElementById(this.id);
113 this.tribute.attach(textarea);
114 textarea.addEventListener('tribute-replaced', () => {
115 this.state.commentForm.content = textarea.value;
116 this.setState(this.state);
117 autosize.update(textarea);
124 <form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
125 <div class="form-group row">
126 <div className={`col-sm-12`}>
127 <textarea id={this.id} className={`form-control ${this.state.previewMode && 'd-none'}`} value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} required disabled={this.props.disabled} rows={2} maxLength={10000} />
128 {this.state.previewMode &&
129 <div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.commentForm.content)} />
134 <div class="col-sm-12">
135 <button type="submit" class="btn btn-sm btn-secondary mr-2" disabled={this.props.disabled}>{this.state.buttonTitle}</button>
136 {this.state.commentForm.content &&
137 <button className={`btn btn-sm mr-2 btn-secondary ${this.state.previewMode && 'active'}`} onClick={linkEvent(this, this.handlePreviewToggle)}><T i18nKey="preview">#</T></button>
139 {this.props.node && <button type="button" class="btn btn-sm btn-secondary mr-2" onClick={linkEvent(this, this.handleReplyCancel)}><T i18nKey="cancel">#</T></button>}
140 <a href={markdownHelpUrl} target="_blank" class="d-inline-block float-right text-muted small font-weight-bold"><T i18nKey="formatting_help">#</T></a>
141 <form class="d-inline-block mr-2 float-right text-muted small font-weight-bold">
142 <label htmlFor={`file-upload-${this.id}`} class="pointer"><T i18nKey="upload_image">#</T></label>
143 <input id={`file-upload-${this.id}`} type="file" accept="image/*,video/*" name="file" class="d-none" onChange={linkEvent(this, this.handleImageUpload)} />
145 {this.state.imageLoading &&
146 <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg>
155 handleCommentSubmit(i: CommentForm, event: any) {
156 event.preventDefault();
158 WebSocketService.Instance.editComment(i.state.commentForm);
160 WebSocketService.Instance.createComment(i.state.commentForm);
163 i.state.previewMode = false;
164 i.state.commentForm.content = undefined;
165 event.target.reset();
168 i.props.onReplyCancel();
171 autosize.update(document.querySelector('textarea'));
174 handleCommentContentChange(i: CommentForm, event: any) {
175 i.state.commentForm.content = event.target.value;
179 handlePreviewToggle(i: CommentForm, event: any) {
180 event.preventDefault();
181 i.state.previewMode = !i.state.previewMode;
185 handleReplyCancel(i: CommentForm) {
186 i.props.onReplyCancel();
189 handleImageUpload(i: CommentForm, event: any) {
190 event.preventDefault();
191 let file = event.target.files[0];
192 const imageUploadUrl = `/pictshare/api/upload.php`;
193 const formData = new FormData();
194 formData.append('file', file);
196 i.state.imageLoading = true;
199 fetch(imageUploadUrl, {
203 .then(res => res.json())
205 let url = `${window.location.origin}/pictshare/${res.url}`;
206 let markdown = (res.filetype == 'mp4') ? `[vid](${url}/raw)` : `![](${url})`;
207 let content = i.state.commentForm.content;
208 content = (content) ? `${content} ${markdown}` : markdown;
209 i.state.commentForm.content = content;
210 i.state.imageLoading = false;
214 i.state.imageLoading = false;
220 userSearch(text: string, cb: any) {
222 let form: SearchForm = {
224 type_: SearchType[SearchType.Users],
225 sort: SortType[SortType.TopAll],
227 limit: mentionDropdownFetchLimit,
230 WebSocketService.Instance.search(form);
232 this.userSub = WebSocketService.Instance.subject
235 let op: UserOperation = msgOp(msg);
236 if (op == UserOperation.Search) {
237 let res: SearchResponse = msg;
238 let users = res.users.map(u => {return {key: u.name}});
240 this.userSub.unsubscribe();
243 (err) => console.error(err),
244 () => console.log('complete')
251 communitySearch(text: string, cb: any) {
253 let form: SearchForm = {
255 type_: SearchType[SearchType.Communities],
256 sort: SortType[SortType.TopAll],
258 limit: mentionDropdownFetchLimit,
261 WebSocketService.Instance.search(form);
263 this.communitySub = WebSocketService.Instance.subject
266 let op: UserOperation = msgOp(msg);
267 if (op == UserOperation.Search) {
268 let res: SearchResponse = msg;
269 let communities = res.communities.map(u => {return {key: u.name}});
271 this.communitySub.unsubscribe();
274 (err) => console.error(err),
275 () => console.log('complete')