Adding preview, image upload, and formatting help to comment and post
forms. - Fixes #253
This commit is contained in:
parent
b6f6cd5c3e
commit
c568a83adf
6 changed files with 62 additions and 18 deletions
16
README.md
vendored
16
README.md
vendored
|
@ -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
|
||||||
|
|
||||||
|
|
27
ui/src/components/comment-form.tsx
vendored
27
ui/src/components/comment-form.tsx
vendored
|
@ -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);
|
||||||
|
|
20
ui/src/components/post-form.tsx
vendored
20
ui/src/components/post-form.tsx
vendored
|
@ -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) {
|
||||||
|
|
5
ui/src/components/post-listing.tsx
vendored
5
ui/src/components/post-listing.tsx
vendored
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
3
ui/src/translations/en.ts
vendored
3
ui/src/translations/en.ts
vendored
|
@ -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
9
ui/src/utils.ts
vendored
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue