Adding preview, image upload, and formatting help to comment and post

forms.

- Fixes #253
This commit is contained in:
Dessalines 2019-08-31 21:10:48 -07:00
parent b6f6cd5c3e
commit c568a83adf
6 changed files with 62 additions and 18 deletions

16
README.md vendored
View file

@ -169,14 +169,14 @@ If you'd like to add translations, take a look a look at the [english translatio
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

View file

@ -1,7 +1,7 @@
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';
@ -19,11 +19,12 @@ interface CommentFormProps {
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;
@ -35,6 +36,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
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) {
@ -119,13 +121,21 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
<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>
@ -141,6 +151,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
WebSocketService.Instance.createComment(i.state.commentForm);
}
i.state.previewMode = false;
i.state.commentForm.content = undefined;
i.setState(i.state);
event.target.reset();
@ -156,6 +167,12 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
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();
}
@ -167,7 +184,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
type_: SearchType[SearchType.Users],
sort: SortType[SortType.TopAll],
page: 1,
limit: 6,
limit: mentionDropdownFetchLimit,
};
WebSocketService.Instance.search(form);
@ -198,7 +215,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
type_: SearchType[SearchType.Communities],
sort: SortType[SortType.TopAll],
page: 1,
limit: 6,
limit: mentionDropdownFetchLimit,
};
WebSocketService.Instance.search(form);

View file

@ -4,7 +4,7 @@ import { Subscription } from "rxjs";
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';
@ -21,6 +21,7 @@ interface PostFormState {
postForm: PostFormI;
communities: Array<Community>;
loading: boolean;
previewMode: boolean;
suggestedTitle: string;
suggestedPosts: Array<Post>;
crossPosts: Array<Post>;
@ -39,6 +40,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
},
communities: [],
loading: false,
previewMode: false,
suggestedTitle: undefined,
suggestedPosts: [],
crossPosts: [],
@ -107,6 +109,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
{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>
@ -130,7 +133,14 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<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 &&
@ -250,6 +260,12 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
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) {

View file

@ -49,7 +49,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<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>
)

View file

@ -26,6 +26,9 @@ export const en = {
edit: 'edit',
reply: 'reply',
cancel: 'Cancel',
preview: 'Preview',
upload_image: 'upload image',
formatting_help: 'formatting help',
unlock: 'unlock',
lock: 'lock',
link: 'link',

9
ui/src/utils.ts vendored
View file

@ -15,6 +15,13 @@ import { emoji_list } from './emoji_list';
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;
@ -110,8 +117,6 @@ export function validURL(str: string) {
return !!pattern.test(str);
}
export let fetchLimit: number = 20;
export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}