diff options
author | Dessalines <tyhou13@gmx.com> | 2020-07-11 19:12:56 -0400 |
---|---|---|
committer | Dessalines <tyhou13@gmx.com> | 2020-07-11 19:12:56 -0400 |
commit | 60288b2d060ba930fe6cae22c4824d88fe7a00c9 (patch) | |
tree | 8fed01325853240c69f3688ee621be29bedfd382 /ui/src | |
parent | 1710844a1bc6a4f46eceaa12f2fb428cb794c694 (diff) | |
parent | 1b9f2fa5f7f7831f59b24cb36a5607a769a0d92e (diff) |
Merge branch 'master' into jmarthernandez-remove-karma-from-search
Diffstat (limited to 'ui/src')
34 files changed, 975 insertions, 381 deletions
diff --git a/ui/src/components/cake-day.tsx b/ui/src/components/cake-day.tsx new file mode 100644 index 00000000..f28be33c --- /dev/null +++ b/ui/src/components/cake-day.tsx @@ -0,0 +1,25 @@ +import { Component } from 'inferno'; +import { i18n } from '../i18next'; + +interface CakeDayProps { + creatorName: string; +} + +export class CakeDay extends Component<CakeDayProps, any> { + render() { + return ( + <div + className={`mx-2 d-inline-block unselectable pointer`} + data-tippy-content={this.cakeDayTippy()} + > + <svg class="icon icon-inline"> + <use xlinkHref="#icon-cake"></use> + </svg> + </div> + ); + } + + cakeDayTippy(): string { + return i18n.t('cake_day_info', { creator_name: this.props.creatorName }); + } +} diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx index 61ee3d77..04720cbb 100644 --- a/ui/src/components/comment-form.tsx +++ b/ui/src/components/comment-form.tsx @@ -1,4 +1,5 @@ import { Component, linkEvent } from 'inferno'; +import { Link } from 'inferno-router'; import { Subscription } from 'rxjs'; import { retryWhen, delay, take } from 'rxjs/operators'; import { Prompt } from 'inferno-router'; @@ -17,7 +18,6 @@ import { toast, setupTribute, wsJsonToRes, - emojiPicker, pictrsDeleteToast, } from '../utils'; import { WebSocketService, UserService } from '../services'; @@ -25,6 +25,7 @@ import autosize from 'autosize'; import Tribute from 'tributejs/src/Tribute.js'; import emojiShortName from 'emoji-short-name'; import { i18n } from '../i18next'; +import { T } from 'inferno-i18next'; interface CommentFormProps { postId?: number; @@ -72,7 +73,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> { super(props, context); this.tribute = setupTribute(); - this.setupEmojiPicker(); this.state = this.emptyState; @@ -98,18 +98,45 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> { } componentDidMount() { - var textarea: any = document.getElementById(this.id); - autosize(textarea); - this.tribute.attach(textarea); - textarea.addEventListener('tribute-replaced', () => { - this.state.commentForm.content = textarea.value; - this.setState(this.state); - autosize.update(textarea); - }); + let textarea: any = document.getElementById(this.id); + if (textarea) { + autosize(textarea); + this.tribute.attach(textarea); + textarea.addEventListener('tribute-replaced', () => { + this.state.commentForm.content = textarea.value; + this.setState(this.state); + autosize.update(textarea); + }); + + // Quoting of selected text + let selectedText = window.getSelection().toString(); + if (selectedText) { + let quotedText = + selectedText + .split('\n') + .map(t => `> ${t}`) + .join('\n') + '\n\n'; + this.state.commentForm.content = quotedText; + this.setState(this.state); + // Not sure why this needs a delay + setTimeout(() => autosize.update(textarea), 10); + } + + textarea.focus(); + } + } + + componentDidUpdate() { + if (this.state.commentForm.content) { + window.onbeforeunload = () => true; + } else { + window.onbeforeunload = undefined; + } } componentWillUnmount() { this.subscription.unsubscribe(); + window.onbeforeunload = null; } render() { @@ -119,133 +146,123 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> { when={this.state.commentForm.content} message={i18n.t('block_leaving')} /> - <form - id={this.formId} - onSubmit={linkEvent(this, this.handleCommentSubmit)} - > - <div class="form-group row"> - <div className={`col-sm-12`}> - <textarea - id={this.id} - className={`form-control ${this.state.previewMode && 'd-none'}`} - value={this.state.commentForm.content} - onInput={linkEvent(this, this.handleCommentContentChange)} - onPaste={linkEvent(this, this.handleImageUploadPaste)} - required - disabled={this.props.disabled} - rows={2} - maxLength={10000} - /> - {this.state.previewMode && ( - <div - className="card card-body md-div" - dangerouslySetInnerHTML={mdToHtml( - this.state.commentForm.content - )} + {UserService.Instance.user ? ( + <form + id={this.formId} + onSubmit={linkEvent(this, this.handleCommentSubmit)} + > + <div class="form-group row"> + <div className={`col-sm-12`}> + <textarea + id={this.id} + className={`form-control ${ + this.state.previewMode && 'd-none' + }`} + value={this.state.commentForm.content} + onInput={linkEvent(this, this.handleCommentContentChange)} + onPaste={linkEvent(this, this.handleImageUploadPaste)} + required + disabled={this.props.disabled} + rows={2} + maxLength={10000} /> - )} - </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.loading} - > - {this.state.loading ? ( - <svg class="icon icon-spinner spin"> - <use xlinkHref="#icon-spinner"></use> - </svg> - ) : ( - <span>{this.state.buttonTitle}</span> + {this.state.previewMode && ( + <div + className="card card-body md-div" + dangerouslySetInnerHTML={mdToHtml( + this.state.commentForm.content + )} + /> )} - </button> - {this.state.commentForm.content && ( - <button - className={`btn btn-sm mr-2 btn-secondary ${ - this.state.previewMode && 'active' - }`} - onClick={linkEvent(this, this.handlePreviewToggle)} - > - {i18n.t('preview')} - </button> - )} - {this.props.node && ( + </div> + </div> + <div class="row"> + <div class="col-sm-12"> <button - type="button" + type="submit" class="btn btn-sm btn-secondary mr-2" - onClick={linkEvent(this, this.handleReplyCancel)} + disabled={this.props.disabled || this.state.loading} > - {i18n.t('cancel')} + {this.state.loading ? ( + <svg class="icon icon-spinner spin"> + <use xlinkHref="#icon-spinner"></use> + </svg> + ) : ( + <span>{this.state.buttonTitle}</span> + )} </button> - )} - <a - href={markdownHelpUrl} - target="_blank" - class="d-inline-block float-right text-muted font-weight-bold" - title={i18n.t('formatting_help')} - rel="noopener" - > - <svg class="icon icon-inline"> - <use xlinkHref="#icon-help-circle"></use> - </svg> - </a> - <form class="d-inline-block mr-3 float-right text-muted font-weight-bold"> - <label - htmlFor={`file-upload-${this.id}`} - className={`${UserService.Instance.user && 'pointer'}`} - data-tippy-content={i18n.t('upload_image')} + {this.state.commentForm.content && ( + <button + className={`btn btn-sm mr-2 btn-secondary ${ + this.state.previewMode && 'active' + }`} + onClick={linkEvent(this, this.handlePreviewToggle)} + > + {i18n.t('preview')} + </button> + )} + {this.props.node && ( + <button + type="button" + class="btn btn-sm btn-secondary mr-2" + onClick={linkEvent(this, this.handleReplyCancel)} + > + {i18n.t('cancel')} + </button> + )} + <a + href={markdownHelpUrl} + target="_blank" + class="d-inline-block float-right text-muted font-weight-bold" + title={i18n.t('formatting_help')} + rel="noopener" > <svg class="icon icon-inline"> - <use xlinkHref="#icon-image"></use> + <use xlinkHref="#icon-help-circle"></use> </svg> - </label> - <input - id={`file-upload-${this.id}`} - type="file" - accept="image/*,video/*" - name="file" - class="d-none" - disabled={!UserService.Instance.user} - onChange={linkEvent(this, this.handleImageUpload)} - /> - </form> - {this.state.imageLoading && ( - <svg class="icon icon-spinner spin"> - <use xlinkHref="#icon-spinner"></use> - </svg> - )} - <span - onClick={linkEvent(this, this.handleEmojiPickerClick)} - class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold" - data-tippy-content={i18n.t('emoji_picker')} - > - <svg class="icon icon-inline"> - <use xlinkHref="#icon-smile"></use> - </svg> - </span> + </a> + <form class="d-inline-block mr-3 float-right text-muted font-weight-bold"> + <label + htmlFor={`file-upload-${this.id}`} + className={`${UserService.Instance.user && 'pointer'}`} + data-tippy-content={i18n.t('upload_image')} + > + <svg class="icon icon-inline"> + <use xlinkHref="#icon-image"></use> + </svg> + </label> + <input + id={`file-upload-${this.id}`} + type="file" + accept="image/*,video/*" + name="file" + class="d-none" + disabled={!UserService.Instance.user} + onChange={linkEvent(this, this.handleImageUpload)} + /> + </form> + {this.state.imageLoading && ( + <svg class="icon icon-spinner spin"> + <use xlinkHref="#icon-spinner"></use> + </svg> + )} + </div> </div> + </form> + ) : ( + <div class="alert alert-warning" role="alert"> + <svg class="icon icon-inline mr-2"> + <use xlinkHref="#icon-alert-triangle"></use> + </svg> + <T i18nKey="must_login" class="d-inline"> + #<Link to="/login">#</Link> + </T> </div> - </form> + )} </div> ); } - setupEmojiPicker() { - emojiPicker.on('emoji', twemojiHtmlStr => { - if (this.state.commentForm.content == null) { - this.state.commentForm.content = ''; - } - var el = document.createElement('div'); - el.innerHTML = twemojiHtmlStr; - let nativeUnicode = (el.childNodes[0] as HTMLElement).getAttribute('alt'); - let shortName = `:${emojiShortName[nativeUnicode]}:`; - this.state.commentForm.content += shortName; - this.setState(this.state); - }); - } - handleFinished(op: UserOperation, data: CommentResponse) { let isReply = this.props.node !== undefined && data.comment.parent_id !== null; @@ -293,10 +310,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> { i.setState(i.state); } - handleEmojiPickerClick(_i: CommentForm, event: any) { - emojiPicker.togglePicker(event.target); - } - handleCommentContentChange(i: CommentForm, event: any) { i.state.commentForm.content = event.target.value; i.setState(i.state); diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index 155efe8e..8e976e7c 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -73,6 +73,7 @@ interface CommentNodeProps { showCommunity?: boolean; sort?: CommentSortType; sortType?: SortType; + enableDownvotes: boolean; } export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { @@ -157,9 +158,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { id: node.comment.creator_id, local: node.comment.creator_local, actor_id: node.comment.creator_actor_id, + published: node.comment.creator_published, }} /> </span> + {this.isMod && ( <div className="badge badge-light d-none d-sm-inline mr-2"> {i18n.t('mod')} @@ -188,8 +191,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { </Link> </> )} - <div - className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2" + <button + class="btn btn-sm text-muted" onClick={linkEvent(this, this.handleCommentCollapse)} > {this.state.collapsed ? ( @@ -201,9 +204,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { <use xlinkHref="#icon-minus-square"></use> </svg> )} - </div> - <span - className={`unselectable pointer ${this.scoreColor}`} + </button> + {/* This is an expanding spacer for mobile */} + <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div> + <button + className={`btn btn-sm p-0 unselectable pointer ${this.scoreColor}`} onClick={linkEvent(node, this.handleCommentUpvote)} data-tippy-content={this.pointsTippy} > @@ -211,7 +216,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { <use xlinkHref="#icon-zap"></use> </svg> <span class="mr-1">{this.state.score}</span> - </span> + </button> <span className="mr-1">•</span> <span> <MomentTime data={node.comment} /> @@ -279,7 +284,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { <span class="ml-1">{this.state.upvotes}</span> )} </button> - {WebSocketService.Instance.site.enable_downvotes && ( + {this.props.enableDownvotes && ( <button className={`btn btn-link btn-animate ${ this.state.my_vote == -1 @@ -703,6 +708,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { postCreatorId={this.props.postCreatorId} sort={this.props.sort} sortType={this.props.sortType} + enableDownvotes={this.props.enableDownvotes} /> )} {/* A collapsed clearfix */} diff --git a/ui/src/components/comment-nodes.tsx b/ui/src/components/comment-nodes.tsx index 875db1f2..bd5ec20b 100644 --- a/ui/src/components/comment-nodes.tsx +++ b/ui/src/components/comment-nodes.tsx @@ -24,6 +24,7 @@ interface CommentNodesProps { showCommunity?: boolean; sort?: CommentSortType; sortType?: SortType; + enableDownvotes: boolean; } export class CommentNodes extends Component< @@ -52,6 +53,7 @@ export class CommentNodes extends Component< showCommunity={this.props.showCommunity} sort={this.props.sort} sortType={this.props.sortType} + enableDownvotes={this.props.enableDownvotes} /> ))} </div> diff --git a/ui/src/components/communities.tsx b/ui/src/components/communities.tsx index a3e340ff..10a3ab80 100644 --- a/ui/src/components/communities.tsx +++ b/ui/src/components/communities.tsx @@ -1,5 +1,4 @@ import { Component, linkEvent } from 'inferno'; -import { Link } from 'inferno-router'; import { Subscription } from 'rxjs'; import { retryWhen, delay, take } from 'rxjs/operators'; import { @@ -11,6 +10,7 @@ import { ListCommunitiesForm, SortType, WebSocketJsonResponse, + GetSiteResponse, } from '../interfaces'; import { WebSocketService } from '../services'; import { wsJsonToRes, toast } from '../utils'; @@ -47,6 +47,7 @@ export class Communities extends Component<any, CommunitiesState> { ); this.refetch(); + WebSocketService.Instance.getSite(); } getPageFromProps(props: any): number { @@ -57,12 +58,6 @@ export class Communities extends Component<any, CommunitiesState> { this.subscription.unsubscribe(); } - componentDidMount() { - document.title = `${i18n.t('communities')} - ${ - WebSocketService.Instance.site.name - }`; - } - // Necessary for back button for some reason componentWillReceiveProps(nextProps: any) { if (nextProps.history.action == 'POP') { @@ -165,7 +160,7 @@ export class Communities extends Component<any, CommunitiesState> { </button> )} - {this.state.communities.length == communityLimit && ( + {this.state.communities.length > 0 && ( <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)} @@ -244,6 +239,9 @@ export class Communities extends Component<any, CommunitiesState> { found.subscribed = data.community.subscribed; found.number_of_subscribers = data.community.number_of_subscribers; this.setState(this.state); + } else if (res.op == UserOperation.GetSite) { + let data = res.data as GetSiteResponse; + document.title = `${i18n.t('communities')} - ${data.site.name}`; } } } diff --git a/ui/src/components/community-form.tsx b/ui/src/components/community-form.tsx index 90e12738..95d9c1f7 100644 --- a/ui/src/components/community-form.tsx +++ b/ui/src/components/community-form.tsx @@ -8,7 +8,6 @@ import { Category, ListCategoriesResponse, CommunityResponse, - GetSiteResponse, WebSocketJsonResponse, } from '../interfaces'; import { WebSocketService } from '../services'; @@ -30,13 +29,13 @@ interface CommunityFormProps { onCancel?(): any; onCreate?(community: Community): any; onEdit?(community: Community): any; + enableNsfw: boolean; } interface CommunityFormState { communityForm: CommunityFormI; categories: Array<Category>; loading: boolean; - enable_nsfw: boolean; } export class CommunityForm extends Component< @@ -56,7 +55,6 @@ export class CommunityForm extends Component< }, categories: [], loading: false, - enable_nsfw: null, }; constructor(props: any, context: any) { @@ -86,7 +84,6 @@ export class CommunityForm extends Component< ); WebSocketService.Instance.listCategories(); - WebSocketService.Instance.getSite(); } componentDidMount() { @@ -100,8 +97,22 @@ export class CommunityForm extends Component< }); } + componentDidUpdate() { + if ( + !this.state.loading && + (this.state.communityForm.name || + this.state.communityForm.title || + this.state.communityForm.description) + ) { + window.onbeforeunload = () => true; + } else { + window.onbeforeunload = undefined; + } + } + componentWillUnmount() { this.subscription.unsubscribe(); + window.onbeforeunload = null; } render() { @@ -187,7 +198,7 @@ export class CommunityForm extends Component< </div> </div> - {this.state.enable_nsfw && ( + {this.props.enableNsfw && ( <div class="form-group row"> <div class="col-12"> <div class="form-check"> @@ -303,10 +314,6 @@ export class CommunityForm extends Component< let data = res.data as CommunityResponse; this.state.loading = false; this.props.onEdit(data.community); - } else if (res.op == UserOperation.GetSite) { - let data = res.data as GetSiteResponse; - this.state.enable_nsfw = data.site.enable_nsfw; - this.setState(this.state); } } } diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index c193532b..fc999b25 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -23,6 +23,8 @@ import { GetCommentsResponse, CommentResponse, WebSocketJsonResponse, + GetSiteResponse, + Site, } from '../interfaces'; import { WebSocketService } from '../services'; import { PostListings } from './post-listings'; @@ -60,6 +62,7 @@ interface State { dataType: DataType; sort: SortType; page: number; + site: Site; } export class Community extends Component<any, State> { @@ -97,6 +100,20 @@ export class Community extends Component<any, State> { dataType: getDataTypeFromProps(this.props), sort: getSortTypeFromProps(this.props), page: getPageFromProps(this.props), + site: { + id: undefined, + name: undefined, + creator_id: undefined, + published: undefined, + creator_name: undefined, + number_of_users: undefined, + number_of_posts: undefined, + number_of_comments: undefined, + number_of_communities: undefined, + enable_downvotes: undefined, + open_registration: undefined, + enable_nsfw: undefined, + }, }; constructor(props: any, context: any) { @@ -119,6 +136,7 @@ export class Community extends Component<any, State> { name: this.state.communityName ? this.state.communityName : null, }; WebSocketService.Instance.getCommunity(form); + WebSocketService.Instance.getSite(); } componentWillUnmount() { @@ -174,6 +192,7 @@ export class Community extends Component<any, State> { moderators={this.state.moderators} admins={this.state.admins} online={this.state.online} + enableNsfw={this.state.site.enable_nsfw} /> </div> </div> @@ -188,6 +207,8 @@ export class Community extends Component<any, State> { posts={this.state.posts} removeDuplicates sort={this.state.sort} + enableDownvotes={this.state.site.enable_downvotes} + enableNsfw={this.state.site.enable_nsfw} /> ) : ( <CommentNodes @@ -195,6 +216,7 @@ export class Community extends Component<any, State> { noIndent sortType={this.state.sort} showContext + enableDownvotes={this.state.site.enable_downvotes} /> ); } @@ -238,7 +260,7 @@ export class Community extends Component<any, State> { {i18n.t('prev')} </button> )} - {this.state.posts.length == fetchLimit && ( + {this.state.posts.length > 0 && ( <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)} @@ -331,7 +353,7 @@ export class Community extends Component<any, State> { this.state.moderators = data.moderators; this.state.admins = data.admins; this.state.online = data.online; - document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`; + document.title = `/c/${this.state.community.name} - ${this.state.site.name}`; this.setState(this.state); this.fetchData(); } else if (res.op == UserOperation.EditCommunity) { @@ -399,6 +421,10 @@ export class Community extends Component<any, State> { let data = res.data as CommentResponse; createCommentLikeRes(data, this.state.comments); this.setState(this.state); + } else if (res.op == UserOperation.GetSite) { + let data = res.data as GetSiteResponse; + this.state.site = data.site; + this.setState(this.state); } } } diff --git a/ui/src/components/create-community.tsx b/ui/src/components/create-community.tsx index 5c7a0a9b..3a5d943d 100644 --- a/ui/src/components/create-community.tsx +++ b/ui/src/components/create-community.tsx @@ -1,19 +1,49 @@ import { Component } from 'inferno'; +import { Subscription } from 'rxjs'; +import { retryWhen, delay, take } from 'rxjs/operators'; import { CommunityForm } from './community-form'; -import { Community } from '../interfaces'; -import { WebSocketService } from '../services'; +import { + Community, + UserOperation, + WebSocketJsonResponse, + GetSiteResponse, +} from '../interfaces'; +import { toast, wsJsonToRes } from '../utils'; +import { WebSocketService, UserService } from '../services'; import { i18n } from '../i18next'; -export class CreateCommunity extends Component<any, any> { +interface CreateCommunityState { + enableNsfw: boolean; +} |