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

forms.

- Fixes #253
pull/722/head
Dessalines 2019-08-31 21:10:48 -07:00
parent d53b105c02
commit ba55f1cefb
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 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 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 | 98% | number_of_communities,are_you_sure,yes,no eo | 96% | number_of_communities,preview,upload_image,formatting_help,are_you_sure,yes,no
es | 98% | number_of_communities,are_you_sure,yes,no es | 96% | number_of_communities,preview,upload_image,formatting_help,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 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 | 100% | nl | 98% | preview,upload_image,formatting_help
ru | 93% | cross_posts,cross_post,number_of_communities,recent_comments,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no 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 | 100% | sv | 98% | preview,upload_image,formatting_help
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 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 ## Credits

View File

@ -1,7 +1,7 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { CommentNode as CommentNodeI, CommentForm as CommentFormI, SearchForm, SearchType, SortType, UserOperation, SearchResponse } from '../interfaces'; import { CommentNode as CommentNodeI, CommentForm as CommentFormI, SearchForm, SearchType, SortType, UserOperation, SearchResponse } from '../interfaces';
import { Subscription } from "rxjs"; 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 { WebSocketService, UserService } from '../services';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
@ -19,11 +19,12 @@ interface CommentFormProps {
interface CommentFormState { interface CommentFormState {
commentForm: CommentFormI; commentForm: CommentFormI;
buttonTitle: string; buttonTitle: string;
previewMode: boolean;
} }
export class CommentForm extends Component<CommentFormProps, CommentFormState> { 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 userSub: Subscription;
private communitySub: Subscription; private communitySub: Subscription;
private tribute: any; private tribute: any;
@ -35,6 +36,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
creator_id: UserService.Instance.user ? UserService.Instance.user.id : null, 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')), 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) { constructor(props: any, context: any) {
@ -119,13 +121,21 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
<form onSubmit={linkEvent(this, this.handleCommentSubmit)}> <form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-12"> <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> </div>
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="submit" class="btn btn-sm btn-secondary mr-2" disabled={this.props.disabled}>{this.state.buttonTitle}</button> <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>} {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>
</div> </div>
</form> </form>
@ -141,6 +151,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
WebSocketService.Instance.createComment(i.state.commentForm); WebSocketService.Instance.createComment(i.state.commentForm);
} }
i.state.previewMode = false;
i.state.commentForm.content = undefined; i.state.commentForm.content = undefined;
i.setState(i.state); i.setState(i.state);
event.target.reset(); event.target.reset();
@ -156,6 +167,12 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
i.setState(i.state); i.setState(i.state);
} }
handlePreviewToggle(i: CommentForm, event: any) {
event.preventDefault();
i.state.previewMode = !i.state.previewMode;
i.setState(i.state);
}
handleReplyCancel(i: CommentForm) { handleReplyCancel(i: CommentForm) {
i.props.onReplyCancel(); i.props.onReplyCancel();
} }
@ -167,7 +184,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
type_: SearchType[SearchType.Users], type_: SearchType[SearchType.Users],
sort: SortType[SortType.TopAll], sort: SortType[SortType.TopAll],
page: 1, page: 1,
limit: 6, limit: mentionDropdownFetchLimit,
}; };
WebSocketService.Instance.search(form); WebSocketService.Instance.search(form);
@ -198,7 +215,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
type_: SearchType[SearchType.Communities], type_: SearchType[SearchType.Communities],
sort: SortType[SortType.TopAll], sort: SortType[SortType.TopAll],
page: 1, page: 1,
limit: 6, limit: mentionDropdownFetchLimit,
}; };
WebSocketService.Instance.search(form); WebSocketService.Instance.search(form);

View File

@ -4,7 +4,7 @@ import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators'; 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 { PostForm as PostFormI, PostFormParams, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType, SearchForm, SearchType, SearchResponse } from '../interfaces';
import { WebSocketService, UserService } from '../services'; 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 * as autosize from 'autosize';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
@ -21,6 +21,7 @@ interface PostFormState {
postForm: PostFormI; postForm: PostFormI;
communities: Array<Community>; communities: Array<Community>;
loading: boolean; loading: boolean;
previewMode: boolean;
suggestedTitle: string; suggestedTitle: string;
suggestedPosts: Array<Post>; suggestedPosts: Array<Post>;
crossPosts: Array<Post>; crossPosts: Array<Post>;
@ -39,6 +40,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
}, },
communities: [], communities: [],
loading: false, loading: false,
previewMode: false,
suggestedTitle: undefined, suggestedTitle: undefined,
suggestedPosts: [], suggestedPosts: [],
crossPosts: [], crossPosts: [],
@ -107,6 +109,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
{this.state.suggestedTitle && {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> <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 && {this.state.crossPosts.length > 0 &&
<> <>
<div class="my-1 text-muted small font-weight-bold"><T i18nKey="cross_posts">#</T></div> <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"> <div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="body">#</T></label> <label class="col-sm-2 col-form-label"><T i18nKey="body">#</T></label>
<div class="col-sm-10"> <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>
</div> </div>
{!this.props.post && {!this.props.post &&
@ -250,6 +260,12 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
i.props.onCancel(); i.props.onCancel();
} }
handlePreviewToggle(i: PostForm, event: any) {
event.preventDefault();
i.state.previewMode = !i.state.previewMode;
i.setState(i.state);
}
parseMessage(msg: any) { parseMessage(msg: any) {
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {

View File

@ -49,7 +49,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<div class="row"> <div class="row">
{!this.state.showEdit {!this.state.showEdit
? this.listing() ? 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> </div>
) )

View File

@ -26,6 +26,9 @@ export const en = {
edit: 'edit', edit: 'edit',
reply: 'reply', reply: 'reply',
cancel: 'Cancel', cancel: 'Cancel',
preview: 'Preview',
upload_image: 'upload image',
formatting_help: 'formatting help',
unlock: 'unlock', unlock: 'unlock',
lock: 'lock', lock: 'lock',
link: 'link', 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'; import * as twemoji from 'twemoji';
export const repoUrl = 'https://github.com/dessalines/lemmy'; 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 { export function msgOp(msg: any): UserOperation {
let opStr: string = msg.op; let opStr: string = msg.op;
@ -110,8 +117,6 @@ export function validURL(str: string) {
return !!pattern.test(str); return !!pattern.test(str);
} }
export let fetchLimit: number = 20;
export function capitalizeFirstLetter(str: string): string { export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1); return str.charAt(0).toUpperCase() + str.slice(1);
} }