forms.
- Fixes #253
lang | done | missing
--- | --- | ---
-de | 88% | cross_posts,cross_post,users,number_of_communities,settings,subscribed,expires,recent_comments,nsfw,show_nsfw,crypto,monero,joined,by,to,transfer_community,transfer_site,are_you_sure,yes,no
-eo | 98% | number_of_communities,are_you_sure,yes,no
-es | 98% | number_of_communities,are_you_sure,yes,no
-fr | 91% | cross_posts,cross_post,users,number_of_communities,settings,recent_comments,nsfw,show_nsfw,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
-nl | 100% |
-ru | 93% | cross_posts,cross_post,number_of_communities,recent_comments,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
-sv | 100% |
-zh | 91% | cross_posts,cross_post,users,number_of_communities,settings,recent_comments,nsfw,show_nsfw,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
+de | 87% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,formatting_help,settings,subscribed,expires,recent_comments,nsfw,show_nsfw,crypto,monero,joined,by,to,transfer_community,transfer_site,are_you_sure,yes,no
+eo | 96% | number_of_communities,preview,upload_image,formatting_help,are_you_sure,yes,no
+es | 96% | number_of_communities,preview,upload_image,formatting_help,are_you_sure,yes,no
+fr | 89% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,formatting_help,settings,recent_comments,nsfw,show_nsfw,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
+nl | 98% | preview,upload_image,formatting_help
+ru | 91% | cross_posts,cross_post,number_of_communities,preview,upload_image,formatting_help,recent_comments,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
+sv | 98% | preview,upload_image,formatting_help
+zh | 89% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,formatting_help,settings,recent_comments,nsfw,show_nsfw,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
## Credits
import { Component, linkEvent } from 'inferno';
import { CommentNode as CommentNodeI, CommentForm as CommentFormI, SearchForm, SearchType, SortType, UserOperation, SearchResponse } from '../interfaces';
import { Subscription } from "rxjs";
-import { capitalizeFirstLetter, fetchLimit, msgOp, md, emojiMentionList } from '../utils';
+import { capitalizeFirstLetter, mentionDropdownFetchLimit, msgOp, md, emojiMentionList, mdToHtml, randomStr, imageUploadUrl, markdownHelpUrl } from '../utils';
import { WebSocketService, UserService } from '../services';
import * as autosize from 'autosize';
import { i18n } from '../i18next';
interface CommentFormState {
commentForm: CommentFormI;
buttonTitle: string;
+ previewMode: boolean;
}
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
- private id = `comment-form-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10)}`;
+ private id = `comment-form-${randomStr()}`;
private userSub: Subscription;
private communitySub: Subscription;
private tribute: any;
creator_id: UserService.Instance.user ? UserService.Instance.user.id : null,
},
buttonTitle: !this.props.node ? capitalizeFirstLetter(i18n.t('post')) : this.props.edit ? capitalizeFirstLetter(i18n.t('edit')) : capitalizeFirstLetter(i18n.t('reply')),
+ previewMode: false,
}
constructor(props: any, context: any) {
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
<div class="form-group row">
<div class="col-sm-12">
- <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} />
+ <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} />
+ {this.state.previewMode &&
+ <div className="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.buttonTitle}</button>
+ {this.state.commentForm.content &&
+ <button className={`btn btn-sm mr-2 btn-secondary ${this.state.previewMode && 'active'}`} onClick={linkEvent(this, this.handlePreviewToggle)}><T i18nKey="preview">#</T></button>
+ }
{this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}><T i18nKey="cancel">#</T></button>}
+ <a href={markdownHelpUrl} target="_blank" class="d-inline-block float-right text-muted small font-weight-bold"><T i18nKey="formatting_help">#</T></a>
+ <a href={imageUploadUrl} target="_blank" class="d-inline-block mr-2 float-right text-muted small font-weight-bold"><T i18nKey="upload_image">#</T></a>
</div>
</div>
</form>
WebSocketService.Instance.createComment(i.state.commentForm);
}
+ i.state.previewMode = false;
i.state.commentForm.content = undefined;
i.setState(i.state);
event.target.reset();
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();
}
type_: SearchType[SearchType.Users],
sort: SortType[SortType.TopAll],
page: 1,
- limit: 6,
+ limit: mentionDropdownFetchLimit,
};
WebSocketService.Instance.search(form);
type_: SearchType[SearchType.Communities],
sort: SortType[SortType.TopAll],
page: 1,
- limit: 6,
+ limit: mentionDropdownFetchLimit,
};
WebSocketService.Instance.search(form);
import { retryWhen, delay, take } from 'rxjs/operators';
import { PostForm as PostFormI, PostFormParams, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType, SearchForm, SearchType, SearchResponse } from '../interfaces';
import { WebSocketService, UserService } from '../services';
-import { msgOp, getPageTitle, debounce, validURL, capitalizeFirstLetter } from '../utils';
+import { msgOp, getPageTitle, debounce, validURL, capitalizeFirstLetter, imageUploadUrl, markdownHelpUrl, mdToHtml } from '../utils';
import * as autosize from 'autosize';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
postForm: PostFormI;
communities: Array<Community>;
loading: boolean;
+ previewMode: boolean;
suggestedTitle: string;
suggestedPosts: Array<Post>;
crossPosts: Array<Post>;
},
communities: [],
loading: false,
+ previewMode: false,
suggestedTitle: undefined,
suggestedPosts: [],
crossPosts: [],
{this.state.suggestedTitle &&
<div class="mt-1 text-muted small font-weight-bold pointer" onClick={linkEvent(this, this.copySuggestedTitle)}><T i18nKey="copy_suggested_title" interpolation={{title: this.state.suggestedTitle}}>#</T></div>
}
+ <a href={imageUploadUrl} target="_blank" class="d-inline-block mr-2 float-right text-muted small font-weight-bold"><T i18nKey="upload_image">#</T></a>
{this.state.crossPosts.length > 0 &&
<>
<div class="my-1 text-muted small font-weight-bold"><T i18nKey="cross_posts">#</T></div>
<div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="body">#</T></label>
<div class="col-sm-10">
- <textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} class="form-control" rows={4} maxLength={10000} />
+ <textarea value={this.state.postForm.body} onInput={linkEvent(this, this.handlePostBodyChange)} className={`form-control ${this.state.previewMode && 'd-none'}`} rows={4} maxLength={10000} />
+ {this.state.previewMode &&
+ <div className="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)}><T i18nKey="preview">#</T></button>
+ }
+ <a href={markdownHelpUrl} target="_blank" class="d-inline-block float-right text-muted small font-weight-bold"><T i18nKey="formatting_help">#</T></a>
</div>
</div>
{!this.props.post &&
i.props.onCancel();
}
+ handlePreviewToggle(i: PostForm, event: any) {
+ event.preventDefault();
+ i.state.previewMode = !i.state.previewMode;
+ i.setState(i.state);
+ }
+
parseMessage(msg: any) {
let op: UserOperation = msgOp(msg);
if (msg.error) {
<div class="row">
{!this.state.showEdit
? this.listing()
- : <PostForm post={this.props.post} onEdit={this.handleEditPost} onCancel={this.handleEditCancel}/>
+ :
+ <div class="col-12">
+ <PostForm post={this.props.post} onEdit={this.handleEditPost} onCancel={this.handleEditCancel}/>
+ </div>
}
</div>
)
edit: 'edit',
reply: 'reply',
cancel: 'Cancel',
+ preview: 'Preview',
+ upload_image: 'upload image',
+ formatting_help: 'formatting help',
unlock: 'unlock',
lock: 'lock',
link: 'link',
import * as twemoji from 'twemoji';
export const repoUrl = 'https://github.com/dessalines/lemmy';
+export const imageUploadUrl = 'https://postimages.org/';
+export const markdownHelpUrl = 'https://commonmark.org/help/';
+
+export const fetchLimit: number = 20;
+export const mentionDropdownFetchLimit = 6;
+
+export function randomStr() {return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10)}
export function msgOp(msg: any): UserOperation {
let opStr: string = msg.op;
return !!pattern.test(str);
}
-export let fetchLimit: number = 20;
-
export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}