Running prettier on code.

- #305 , #309
This commit is contained in:
Dessalines 2019-10-18 17:20:27 -07:00
parent 82ea5ae918
commit 4e55612833
43 changed files with 4523 additions and 2059 deletions

View file

@ -1,7 +1,22 @@
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 {
import { Subscription } from "rxjs"; CommentNode as CommentNodeI,
import { capitalizeFirstLetter, mentionDropdownFetchLimit, msgOp, mdToHtml, randomStr, markdownHelpUrl } from '../utils'; CommentForm as CommentFormI,
SearchForm,
SearchType,
SortType,
UserOperation,
SearchResponse,
} from '../interfaces';
import { Subscription } from 'rxjs';
import {
capitalizeFirstLetter,
mentionDropdownFetchLimit,
msgOp,
mdToHtml,
randomStr,
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';
@ -25,7 +40,6 @@ interface CommentFormState {
} }
export class CommentForm extends Component<CommentFormProps, CommentFormState> { export class CommentForm extends Component<CommentFormProps, CommentFormState> {
private id = `comment-form-${randomStr()}`; private id = `comment-form-${randomStr()}`;
private userSub: Subscription; private userSub: Subscription;
private communitySub: Subscription; private communitySub: Subscription;
@ -34,13 +48,21 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
commentForm: { commentForm: {
auth: null, auth: null,
content: null, content: null,
post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId, post_id: this.props.node
creator_id: UserService.Instance.user ? UserService.Instance.user.id : null, ? this.props.node.comment.post_id
: this.props.postId,
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, previewMode: false,
imageLoading: false, imageLoading: false,
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -57,7 +79,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
selectTemplate: (item: any) => { selectTemplate: (item: any) => {
return `:${item.original.key}:`; return `:${item.original.key}:`;
}, },
values: Object.entries(emojiShortName).map(e => {return {'key': e[1], 'val': e[0]}}), values: Object.entries(emojiShortName).map(e => {
return { key: e[1], val: e[0] };
}),
allowSpaces: false, allowSpaces: false,
autocompleteMode: true, autocompleteMode: true,
menuItemLimit: mentionDropdownFetchLimit, menuItemLimit: mentionDropdownFetchLimit,
@ -88,8 +112,8 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
allowSpaces: false, allowSpaces: false,
autocompleteMode: true, autocompleteMode: true,
menuItemLimit: mentionDropdownFetchLimit, menuItemLimit: mentionDropdownFetchLimit,
} },
] ],
}); });
this.state = this.emptyState; this.state = this.emptyState;
@ -124,27 +148,82 @@ 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 className={`col-sm-12`}> <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)} required disabled={this.props.disabled} rows={2} maxLength={10000} /> <textarea
{this.state.previewMode && id={this.id}
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.commentForm.content)} /> 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
{this.state.commentForm.content && type="submit"
<button className={`btn btn-sm mr-2 btn-secondary ${this.state.previewMode && 'active'}`} onClick={linkEvent(this, this.handlePreviewToggle)}><T i18nKey="preview">#</T></button> class="btn btn-sm btn-secondary mr-2"
} disabled={this.props.disabled}
{this.props.node && <button type="button" class="btn btn-sm btn-secondary mr-2" 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> {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 mr-2"
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>
<form class="d-inline-block mr-2 float-right text-muted small font-weight-bold"> <form class="d-inline-block mr-2 float-right text-muted small font-weight-bold">
<label htmlFor={`file-upload-${this.id}`} className={`${UserService.Instance.user && 'pointer'}`}><T i18nKey="upload_image">#</T></label> <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)} /> htmlFor={`file-upload-${this.id}`}
className={`${UserService.Instance.user && 'pointer'}`}
>
<T i18nKey="upload_image">#</T>
</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> </form>
{this.state.imageLoading && {this.state.imageLoading && (
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> <svg class="icon icon-spinner spin">
} <use xlinkHref="#icon-spinner"></use>
</svg>
)}
</div> </div>
</div> </div>
</form> </form>
@ -203,18 +282,19 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
let url = `${window.location.origin}/pictshare/${res.url}`; let url = `${window.location.origin}/pictshare/${res.url}`;
let markdown = (res.filetype == 'mp4') ? `[vid](${url}/raw)` : `![](${url})`; let markdown =
res.filetype == 'mp4' ? `[vid](${url}/raw)` : `![](${url})`;
let content = i.state.commentForm.content; let content = i.state.commentForm.content;
content = (content) ? `${content} ${markdown}` : markdown; content = content ? `${content} ${markdown}` : markdown;
i.state.commentForm.content = content; i.state.commentForm.content = content;
i.state.imageLoading = false; i.state.imageLoading = false;
i.setState(i.state); i.setState(i.state);
}) })
.catch((error) => { .catch(error => {
i.state.imageLoading = false; i.state.imageLoading = false;
i.setState(i.state); i.setState(i.state);
alert(error); alert(error);
}) });
} }
userSearch(text: string, cb: any) { userSearch(text: string, cb: any) {
@ -229,18 +309,19 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
WebSocketService.Instance.search(form); WebSocketService.Instance.search(form);
this.userSub = WebSocketService.Instance.subject this.userSub = WebSocketService.Instance.subject.subscribe(
.subscribe( msg => {
(msg) => {
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (op == UserOperation.Search) { if (op == UserOperation.Search) {
let res: SearchResponse = msg; let res: SearchResponse = msg;
let users = res.users.map(u => {return {key: u.name}}); let users = res.users.map(u => {
return { key: u.name };
});
cb(users); cb(users);
this.userSub.unsubscribe(); this.userSub.unsubscribe();
} }
}, },
(err) => console.error(err), err => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
} else { } else {
@ -260,18 +341,19 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
WebSocketService.Instance.search(form); WebSocketService.Instance.search(form);
this.communitySub = WebSocketService.Instance.subject this.communitySub = WebSocketService.Instance.subject.subscribe(
.subscribe( msg => {
(msg) => {
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (op == UserOperation.Search) { if (op == UserOperation.Search) {
let res: SearchResponse = msg; let res: SearchResponse = msg;
let communities = res.communities.map(u => {return {key: u.name}}); let communities = res.communities.map(u => {
return { key: u.name };
});
cb(communities); cb(communities);
this.communitySub.unsubscribe(); this.communitySub.unsubscribe();
} }
}, },
(err) => console.error(err), err => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
} else { } else {

View file

@ -1,6 +1,20 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, SaveCommentForm, BanFromCommunityForm, BanUserForm, CommunityUser, UserView, AddModToCommunityForm, AddAdminForm, TransferCommunityForm, TransferSiteForm, BanType } from '../interfaces'; import {
CommentNode as CommentNodeI,
CommentLikeForm,
CommentForm as CommentFormI,
SaveCommentForm,
BanFromCommunityForm,
BanUserForm,
CommunityUser,
UserView,
AddModToCommunityForm,
AddAdminForm,
TransferCommunityForm,
TransferSiteForm,
BanType,
} from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { mdToHtml, getUnixTime, canMod, isMod } from '../utils'; import { mdToHtml, getUnixTime, canMod, isMod } from '../utils';
import * as moment from 'moment'; import * as moment from 'moment';
@ -37,7 +51,6 @@ interface CommentNodeProps {
} }
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
private emptyState: CommentNodeState = { private emptyState: CommentNodeState = {
showReply: false, showReply: false,
showEdit: false, showEdit: false,
@ -51,7 +64,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
viewSource: false, viewSource: false,
showConfirmTransferSite: false, showConfirmTransferSite: false,
showConfirmTransferCommunity: false, showConfirmTransferCommunity: false,
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -65,176 +78,405 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
render() { render() {
let node = this.props.node; let node = this.props.node;
return ( return (
<div className={`comment ${node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''}`}> <div
{!this.state.collapsed && className={`comment ${
<div className={`vote-bar mr-2 float-left small text-center ${this.props.viewOnly && 'no-click'}`}> node.comment.parent_id && !this.props.noIndent ? 'ml-4' : ''
<button className={`btn p-0 ${node.comment.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentLike)}> }`}
<svg class="icon upvote"><use xlinkHref="#icon-arrow-up"></use></svg> >
{!this.state.collapsed && (
<div
className={`vote-bar mr-2 float-left small text-center ${this.props
.viewOnly && 'no-click'}`}
>
<button
className={`btn p-0 ${
node.comment.my_vote == 1 ? 'text-info' : 'text-muted'
}`}
onClick={linkEvent(node, this.handleCommentLike)}
>
<svg class="icon upvote">
<use xlinkHref="#icon-arrow-up"></use>
</svg>
</button> </button>
<div class={`font-weight-bold text-muted`}>{node.comment.score}</div> <div class={`font-weight-bold text-muted`}>
<button className={`btn p-0 ${node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'}`} onClick={linkEvent(node, this.handleCommentDisLike)}> {node.comment.score}
<svg class="icon downvote"><use xlinkHref="#icon-arrow-down"></use></svg> </div>
<button
className={`btn p-0 ${
node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'
}`}
onClick={linkEvent(node, this.handleCommentDisLike)}
>
<svg class="icon downvote">
<use xlinkHref="#icon-arrow-down"></use>
</svg>
</button> </button>
</div> </div>
} )}
<div id={`comment-${node.comment.id}`} className={`details comment-node ml-4 ${this.isCommentNew ? 'mark' : ''}`}> <div
id={`comment-${node.comment.id}`}
className={`details comment-node ml-4 ${
this.isCommentNew ? 'mark' : ''
}`}
>
<ul class="list-inline mb-0 text-muted small"> <ul class="list-inline mb-0 text-muted small">
<li className="list-inline-item"> <li className="list-inline-item">
<Link className="text-info" to={`/u/${node.comment.creator_name}`}>{node.comment.creator_name}</Link> <Link
className="text-info"
to={`/u/${node.comment.creator_name}`}
>
{node.comment.creator_name}
</Link>
</li> </li>
{this.isMod && {this.isMod && (
<li className="list-inline-item badge badge-light"><T i18nKey="mod">#</T></li> <li className="list-inline-item badge badge-light">
} <T i18nKey="mod">#</T>
{this.isAdmin && </li>
<li className="list-inline-item badge badge-light"><T i18nKey="admin">#</T></li> )}
} {this.isAdmin && (
{this.isPostCreator && <li className="list-inline-item badge badge-light">
<li className="list-inline-item badge badge-light"><T i18nKey="creator">#</T></li> <T i18nKey="admin">#</T>
} </li>
{(node.comment.banned_from_community || node.comment.banned) && )}
<li className="list-inline-item badge badge-danger"><T i18nKey="banned">#</T></li> {this.isPostCreator && (
} <li className="list-inline-item badge badge-light">
<T i18nKey="creator">#</T>
</li>
)}
{(node.comment.banned_from_community || node.comment.banned) && (
<li className="list-inline-item badge badge-danger">
<T i18nKey="banned">#</T>
</li>
)}
<li className="list-inline-item"> <li className="list-inline-item">
<span>( <span>
<span className="text-info">+{node.comment.upvotes}</span> (<span className="text-info">+{node.comment.upvotes}</span>
<span> | </span> <span> | </span>
<span className="text-danger">-{node.comment.downvotes}</span> <span className="text-danger">-{node.comment.downvotes}</span>
<span>) </span> <span>) </span>
</span> </span>
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<span><MomentTime data={node.comment} /></span> <span>
<MomentTime data={node.comment} />
</span>
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<div className="pointer text-monospace" onClick={linkEvent(this, this.handleCommentCollapse)}>{this.state.collapsed ? '[+]' : '[-]'}</div> <div
className="pointer text-monospace"
onClick={linkEvent(this, this.handleCommentCollapse)}
>
{this.state.collapsed ? '[+]' : '[-]'}
</div>
</li> </li>
</ul> </ul>
{this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />} {this.state.showEdit && (
{!this.state.showEdit && !this.state.collapsed && <CommentForm
node={node}
edit
onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked}
/>
)}
{!this.state.showEdit && !this.state.collapsed && (
<div> <div>
{this.state.viewSource ? <pre>{this.commentUnlessRemoved}</pre> : {this.state.viewSource ? (
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.commentUnlessRemoved)} /> <pre>{this.commentUnlessRemoved}</pre>
} ) : (
<div
className="md-div"
dangerouslySetInnerHTML={mdToHtml(this.commentUnlessRemoved)}
/>
)}
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
{UserService.Instance.user && !this.props.viewOnly && {UserService.Instance.user && !this.props.viewOnly && (
<> <>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}><T i18nKey="reply">#</T></span> <span
class="pointer"
onClick={linkEvent(this, this.handleReplyClick)}
>
<T i18nKey="reply">#</T>
</span>
</li> </li>
<li className="list-inline-item mr-2"> <li className="list-inline-item mr-2">
<span class="pointer" onClick={linkEvent(this, this.handleSaveCommentClick)}>{node.comment.saved ? i18n.t('unsave') : i18n.t('save')}</span> <span
class="pointer"
onClick={linkEvent(this, this.handleSaveCommentClick)}
>
{node.comment.saved ? i18n.t('unsave') : i18n.t('save')}
</span>
</li> </li>
{this.myComment && {this.myComment && (
<> <>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span> <span
class="pointer"
onClick={linkEvent(this, this.handleEditClick)}
>
<T i18nKey="edit">#</T>
</span>
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}> <span
{!node.comment.deleted ? i18n.t('delete') : i18n.t('restore')} class="pointer"
onClick={linkEvent(this, this.handleDeleteClick)}
>
{!node.comment.deleted
? i18n.t('delete')
: i18n.t('restore')}
</span> </span>
</li> </li>
</> </>
} )}
{/* Admins and mods can remove comments */} {/* Admins and mods can remove comments */}
{(this.canMod || this.canAdmin) && {(this.canMod || this.canAdmin) && (
<li className="list-inline-item"> <li className="list-inline-item">
{!node.comment.removed ? {!node.comment.removed ? (
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> : <span
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span> class="pointer"
} onClick={linkEvent(this, this.handleModRemoveShow)}
>
<T i18nKey="remove">#</T>
</span>
) : (
<span
class="pointer"
onClick={linkEvent(
this,
this.handleModRemoveSubmit
)}
>
<T i18nKey="restore">#</T>
</span>
)}
</li> </li>
} )}
{/* Mods can ban from community, and appoint as mods to community */} {/* Mods can ban from community, and appoint as mods to community */}
{this.canMod && {this.canMod && (
<> <>
{!this.isMod && {!this.isMod && (
<li className="list-inline-item"> <li className="list-inline-item">
{!node.comment.banned_from_community ? {!node.comment.banned_from_community ? (
<span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}><T i18nKey="ban">#</T></span> : <span
<span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}><T i18nKey="unban">#</T></span> class="pointer"
} onClick={linkEvent(
this,
this.handleModBanFromCommunityShow
)}
>
<T i18nKey="ban">#</T>
</span>
) : (
<span
class="pointer"
onClick={linkEvent(
this,
this.handleModBanFromCommunitySubmit
)}
>
<T i18nKey="unban">#</T>
</span>
)}
</li> </li>
} )}
{!node.comment.banned_from_community && {!node.comment.banned_from_community && (
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{this.isMod ? i18n.t('remove_as_mod') : i18n.t('appoint_as_mod')}</span> <span
class="pointer"
onClick={linkEvent(
this,
this.handleAddModToCommunity
)}
>
{this.isMod
? i18n.t('remove_as_mod')
: i18n.t('appoint_as_mod')}
</span>
</li> </li>
} )}
</> </>
} )}
{/* Community creators and admins can transfer community to another mod */} {/* Community creators and admins can transfer community to another mod */}
{(this.amCommunityCreator || this.canAdmin) && this.isMod && {(this.amCommunityCreator || this.canAdmin) && this.isMod && (
<li className="list-inline-item"> <li className="list-inline-item">
{!this.state.showConfirmTransferCommunity ? {!this.state.showConfirmTransferCommunity ? (
<span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferCommunity)}><T i18nKey="transfer_community">#</T> <span
</span> : <> class="pointer"
<span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span> onClick={linkEvent(
<span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferCommunity)}><T i18nKey="yes">#</T></span> this,
<span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferCommunity)}><T i18nKey="no">#</T></span> this.handleShowConfirmTransferCommunity
</> )}
} >
</li> <T i18nKey="transfer_community">#</T>
} </span>
{/* Admins can ban from all, and appoint other admins */} ) : (
{this.canAdmin &&
<> <>
{!this.isAdmin && <span class="d-inline-block mr-1">
<li className="list-inline-item"> <T i18nKey="are_you_sure">#</T>
{!node.comment.banned ? </span>
<span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}><T i18nKey="ban_from_site">#</T></span> : <span
<span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}><T i18nKey="unban_from_site">#</T></span> class="pointer d-inline-block mr-1"
} onClick={linkEvent(
</li> this,
} this.handleTransferCommunity
{!node.comment.banned && )}
<li className="list-inline-item"> >
<span class="pointer" onClick={linkEvent(this, this.handleAddAdmin)}>{this.isAdmin ? i18n.t('remove_as_admin') : i18n.t('appoint_as_admin')}</span> <T i18nKey="yes">#</T>
</li> </span>
} <span
class="pointer d-inline-block"
onClick={linkEvent(
this,
this.handleCancelShowConfirmTransferCommunity
)}
>
<T i18nKey="no">#</T>
</span>
</> </>
} )}
</li>
)}
{/* Admins can ban from all, and appoint other admins */}
{this.canAdmin && (
<>
{!this.isAdmin && (
<li className="list-inline-item">
{!node.comment.banned ? (
<span
class="pointer"
onClick={linkEvent(this, this.handleModBanShow)}
>
<T i18nKey="ban_from_site">#</T>
</span>
) : (
<span
class="pointer"
onClick={linkEvent(
this,
this.handleModBanSubmit
)}
>
<T i18nKey="unban_from_site">#</T>
</span>
)}
</li>
)}
{!node.comment.banned && (
<li className="list-inline-item">
<span
class="pointer"
onClick={linkEvent(this, this.handleAddAdmin)}
>
{this.isAdmin
? i18n.t('remove_as_admin')
: i18n.t('appoint_as_admin')}
</span>
</li>
)}
</>
)}
{/* Site Creator can transfer to another admin */} {/* Site Creator can transfer to another admin */}
{this.amSiteCreator && this.isAdmin && {this.amSiteCreator && this.isAdmin && (
<li className="list-inline-item"> <li className="list-inline-item">
{!this.state.showConfirmTransferSite ? {!this.state.showConfirmTransferSite ? (
<span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferSite)}><T i18nKey="transfer_site">#</T> <span
</span> : <> class="pointer"
<span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span> onClick={linkEvent(
<span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferSite)}><T i18nKey="yes">#</T></span> this,
<span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferSite)}><T i18nKey="no">#</T></span> this.handleShowConfirmTransferSite
)}
>
<T i18nKey="transfer_site">#</T>
</span>
) : (
<>
<span class="d-inline-block mr-1">
<T i18nKey="are_you_sure">#</T>
</span>
<span
class="pointer d-inline-block mr-1"
onClick={linkEvent(this, this.handleTransferSite)}
>
<T i18nKey="yes">#</T>
</span>
<span
class="pointer d-inline-block"
onClick={linkEvent(
this,
this.handleCancelShowConfirmTransferSite
)}
>
<T i18nKey="no">#</T>
</span>
</> </>
} )}
</li> </li>
} )}
</> </>
} )}
<li className="list-inline-item"> <li className="list-inline-item">
<span className="pointer" onClick={linkEvent(this, this.handleViewSource)}><T i18nKey="view_source">#</T></span> <span
className="pointer"
onClick={linkEvent(this, this.handleViewSource)}
>
<T i18nKey="view_source">#</T>
</span>
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<Link className="text-muted" to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}><T i18nKey="link">#</T></Link> <Link
className="text-muted"
to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
>
<T i18nKey="link">#</T>
</Link>
</li> </li>
{this.props.markable && {this.props.markable && (
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleMarkRead)}>{node.comment.read ? i18n.t('mark_as_unread') : i18n.t('mark_as_read')}</span> <span
class="pointer"
onClick={linkEvent(this, this.handleMarkRead)}
>
{node.comment.read
? i18n.t('mark_as_unread')
: i18n.t('mark_as_read')}
</span>
</li> </li>
} )}
</ul> </ul>
</div> </div>
} )}
</div> </div>
{this.state.showRemoveDialog && {this.state.showRemoveDialog && (
<form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}> <form
<input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} /> class="form-inline"
<button type="submit" class="btn btn-secondary"><T i18nKey="remove_comment">#</T></button> onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
>
<input
type="text"
class="form-control mr-2"
placeholder={i18n.t('reason')}
value={this.state.removeReason}
onInput={linkEvent(this, this.handleModRemoveReasonChange)}
/>
<button type="submit" class="btn btn-secondary">
<T i18nKey="remove_comment">#</T>
</button>
</form> </form>
} )}
{this.state.showBanDialog && {this.state.showBanDialog && (
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}> <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
<div class="form-group row"> <div class="form-group row">
<label class="col-form-label"><T i18nKey="reason">#</T></label> <label class="col-form-label">
<input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} /> <T i18nKey="reason">#</T>
</label>
<input
type="text"
class="form-control mr-2"
placeholder={i18n.t('reason')}
value={this.state.banReason}
onInput={linkEvent(this, this.handleModBanReasonChange)}
/>
</div> </div>
{/* TODO hold off on expires until later */} {/* TODO hold off on expires until later */}
{/* <div class="form-group row"> */} {/* <div class="form-group row"> */}
@ -242,18 +484,20 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */} {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
{/* </div> */} {/* </div> */}
<div class="form-group row"> <div class="form-group row">
<button type="submit" class="btn btn-secondary">{i18n.t('ban')} {node.comment.creator_name}</button> <button type="submit" class="btn btn-secondary">
{i18n.t('ban')} {node.comment.creator_name}
</button>
</div> </div>
</form> </form>
} )}
{this.state.showReply && {this.state.showReply && (
<CommentForm <CommentForm
node={node} node={node}
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked} disabled={this.props.locked}
/> />
} )}
{node.children && !this.state.collapsed && {node.children && !this.state.collapsed && (
<CommentNodes <CommentNodes
nodes={node.children} nodes={node.children}
locked={this.props.locked} locked={this.props.locked}
@ -261,23 +505,38 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
admins={this.props.admins} admins={this.props.admins}
postCreatorId={this.props.postCreatorId} postCreatorId={this.props.postCreatorId}
/> />
} )}
{/* A collapsed clearfix */} {/* A collapsed clearfix */}
{this.state.collapsed && <div class="row col-12"></div>} {this.state.collapsed && <div class="row col-12"></div>}
</div> </div>
) );
} }
get myComment(): boolean { get myComment(): boolean {
return UserService.Instance.user && this.props.node.comment.creator_id == UserService.Instance.user.id; return (
UserService.Instance.user &&
this.props.node.comment.creator_id == UserService.Instance.user.id
);
} }
get isMod(): boolean { get isMod(): boolean {
return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.node.comment.creator_id); return (
this.props.moderators &&
isMod(
this.props.moderators.map(m => m.user_id),
this.props.node.comment.creator_id
)
);
} }
get isAdmin(): boolean { get isAdmin(): boolean {
return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.node.comment.creator_id); return (
this.props.admins &&
isMod(
this.props.admins.map(a => a.id),
this.props.node.comment.creator_id
)
);
} }
get isPostCreator(): boolean { get isPostCreator(): boolean {
@ -285,38 +544,57 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
get canMod(): boolean { get canMod(): boolean {
if (this.props.admins && this.props.moderators) { if (this.props.admins && this.props.moderators) {
let adminsThenMods = this.props.admins.map(a => a.id) let adminsThenMods = this.props.admins
.map(a => a.id)
.concat(this.props.moderators.map(m => m.user_id)); .concat(this.props.moderators.map(m => m.user_id));
return canMod(UserService.Instance.user, adminsThenMods, this.props.node.comment.creator_id); return canMod(
UserService.Instance.user,
adminsThenMods,
this.props.node.comment.creator_id
);
} else { } else {
return false; return false;
} }
} }
get canAdmin(): boolean { get canAdmin(): boolean {
return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.node.comment.creator_id); return (
this.props.admins &&
canMod(
UserService.Instance.user,
this.props.admins.map(a => a.id),
this.props.node.comment.creator_id
)
);
} }
get amCommunityCreator(): boolean { get amCommunityCreator(): boolean {
return this.props.moderators && return (
this.props.moderators &&
UserService.Instance.user && UserService.Instance.user &&
(this.props.node.comment.creator_id != UserService.Instance.user.id) && this.props.node.comment.creator_id != UserService.Instance.user.id &&
(UserService.Instance.user.id == this.props.moderators[0].user_id); UserService.Instance.user.id == this.props.moderators[0].user_id
);
} }
get amSiteCreator(): boolean { get amSiteCreator(): boolean {
return this.props.admins && return (
this.props.admins &&
UserService.Instance.user && UserService.Instance.user &&
(this.props.node.comment.creator_id != UserService.Instance.user.id) && this.props.node.comment.creator_id != UserService.Instance.user.id &&
(UserService.Instance.user.id == this.props.admins[0].id); UserService.Instance.user.id == this.props.admins[0].id
);
} }
get commentUnlessRemoved(): string { get commentUnlessRemoved(): string {
let node = this.props.node; let node = this.props.node;
return node.comment.removed ? `*${i18n.t('removed')}*` : node.comment.deleted ? `*${i18n.t('deleted')}*` : node.comment.content; return node.comment.removed
? `*${i18n.t('removed')}*`
: node.comment.deleted
? `*${i18n.t('deleted')}*`
: node.comment.content;
} }
handleReplyClick(i: CommentNode) { handleReplyClick(i: CommentNode) {
@ -337,16 +615,19 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
post_id: i.props.node.comment.post_id, post_id: i.props.node.comment.post_id,
parent_id: i.props.node.comment.parent_id, parent_id: i.props.node.comment.parent_id,
deleted: !i.props.node.comment.deleted, deleted: !i.props.node.comment.deleted,
auth: null auth: null,
}; };
WebSocketService.Instance.editComment(deleteForm); WebSocketService.Instance.editComment(deleteForm);
} }
handleSaveCommentClick(i: CommentNode) { handleSaveCommentClick(i: CommentNode) {
let saved = (i.props.node.comment.saved == undefined) ? true : !i.props.node.comment.saved; let saved =
i.props.node.comment.saved == undefined
? true
: !i.props.node.comment.saved;
let form: SaveCommentForm = { let form: SaveCommentForm = {
comment_id: i.props.node.comment.id, comment_id: i.props.node.comment.id,
save: saved save: saved,
}; };
WebSocketService.Instance.saveComment(form); WebSocketService.Instance.saveComment(form);
@ -358,13 +639,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
this.setState(this.state); this.setState(this.state);
} }
handleCommentLike(i: CommentNodeI) { handleCommentLike(i: CommentNodeI) {
let form: CommentLikeForm = { let form: CommentLikeForm = {
comment_id: i.comment.id, comment_id: i.comment.id,
post_id: i.comment.post_id, post_id: i.comment.post_id,
score: (i.comment.my_vote == 1) ? 0 : 1 score: i.comment.my_vote == 1 ? 0 : 1,
}; };
WebSocketService.Instance.likeComment(form); WebSocketService.Instance.likeComment(form);
} }
@ -373,7 +652,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
let form: CommentLikeForm = { let form: CommentLikeForm = {
comment_id: i.comment.id, comment_id: i.comment.id,
post_id: i.comment.post_id, post_id: i.comment.post_id,
score: (i.comment.my_vote == -1) ? 0 : -1 score: i.comment.my_vote == -1 ? 0 : -1,
}; };
WebSocketService.Instance.likeComment(form); WebSocketService.Instance.likeComment(form);
} }
@ -398,7 +677,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
parent_id: i.props.node.comment.parent_id, parent_id: i.props.node.comment.parent_id,
removed: !i.props.node.comment.removed, removed: !i.props.node.comment.removed,
reason: i.state.removeReason, reason: i.state.removeReason,
auth: null auth: null,
}; };
WebSocketService.Instance.editComment(form); WebSocketService.Instance.editComment(form);
@ -414,12 +693,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
post_id: i.props.node.comment.post_id, post_id: i.props.node.comment.post_id,
parent_id: i.props.node.comment.parent_id, parent_id: i.props.node.comment.parent_id,
read: !i.props.node.comment.read, read: !i.props.node.comment.read,
auth: null auth: null,
}; };
WebSocketService.Instance.editComment(form); WebSocketService.Instance.editComment(form);
} }
handleModBanFromCommunityShow(i: CommentNode) { handleModBanFromCommunityShow(i: CommentNode) {
i.state.showBanDialog = true; i.state.showBanDialog = true;
i.state.banType = BanType.Community; i.state.banType = BanType.Community;

View file

@ -1,9 +1,12 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { CommentNode as CommentNodeI, CommunityUser, UserView } from '../interfaces'; import {
CommentNode as CommentNodeI,
CommunityUser,
UserView,
} from '../interfaces';
import { CommentNode } from './comment-node'; import { CommentNode } from './comment-node';
interface CommentNodesState { interface CommentNodesState {}
}
interface CommentNodesProps { interface CommentNodesProps {
nodes: Array<CommentNodeI>; nodes: Array<CommentNodeI>;
@ -16,8 +19,10 @@ interface CommentNodesProps {
markable?: boolean; markable?: boolean;
} }
export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> { export class CommentNodes extends Component<
CommentNodesProps,
CommentNodesState
> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
} }
@ -25,8 +30,9 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
render() { render() {
return ( return (
<div className="comments"> <div className="comments">
{this.props.nodes.map(node => {this.props.nodes.map(node => (
<CommentNode node={node} <CommentNode
node={node}
noIndent={this.props.noIndent} noIndent={this.props.noIndent}
viewOnly={this.props.viewOnly} viewOnly={this.props.viewOnly}
locked={this.props.locked} locked={this.props.locked}
@ -35,10 +41,8 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
postCreatorId={this.props.postCreatorId} postCreatorId={this.props.postCreatorId}
markable={this.props.markable} markable={this.props.markable}
/> />
)} ))}
</div> </div>
) );
} }
} }

View file

@ -1,8 +1,16 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Subscription } from "rxjs"; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Community, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm, ListCommunitiesForm, SortType } from '../interfaces'; import {
UserOperation,
Community,
ListCommunitiesResponse,
CommunityResponse,
FollowCommunityForm,
ListCommunitiesForm,
SortType,
} from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { msgOp } from '../utils'; import { msgOp } from '../utils';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
@ -22,25 +30,31 @@ export class Communities extends Component<any, CommunitiesState> {
communities: [], communities: [],
loading: true, loading: true,
page: this.getPageFromProps(this.props), page: this.getPageFromProps(this.props),
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState; this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), msg => this.parseMessage(msg),
(err) => console.error(err), err => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
this.refetch(); this.refetch();
} }
getPageFromProps(props: any): number { getPageFromProps(props: any): number {
return (props.match.params.page) ? Number(props.match.params.page) : 1; return props.match.params.page ? Number(props.match.params.page) : 1;
} }
componentWillUnmount() { componentWillUnmount() {
@ -48,7 +62,9 @@ export class Communities extends Component<any, CommunitiesState> {
} }
componentDidMount() { componentDidMount() {
document.title = `${i18n.t('communities')} - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('communities')} - ${
WebSocketService.Instance.site.name
}`;
} }
// Necessary for back button for some reason // Necessary for back button for some reason
@ -63,46 +79,92 @@ export class Communities extends Component<any, CommunitiesState> {
render() { render() {
return ( return (
<div class="container"> <div class="container">
{this.state.loading ? {this.state.loading ? (
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : <h5 class="">
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
</h5>
) : (
<div> <div>
<h5><T i18nKey="list_of_communities">#</T></h5> <h5>
<T i18nKey="list_of_communities">#</T>
</h5>
<div class="table-responsive"> <div class="table-responsive">
<table id="community_table" class="table table-sm table-hover"> <table id="community_table" class="table table-sm table-hover">
<thead class="pointer"> <thead class="pointer">
<tr> <tr>
<th><T i18nKey="name">#</T></th> <th>
<th class="d-none d-lg-table-cell"><T i18nKey="title">#</T></th> <T i18nKey="name">#</T>
<th><T i18nKey="category">#</T></th> </th>
<th class="text-right"><T i18nKey="subscribers">#</T></th> <th class="d-none d-lg-table-cell">
<th class="text-right d-none d-lg-table-cell"><T i18nKey="posts">#</T></th> <T i18nKey="title">#</T>
<th class="text-right d-none d-lg-table-cell"><T i18nKey="comments">#</T></th> </th>
<th>
<T i18nKey="category">#</T>
</th>
<th class="text-right">
<T i18nKey="subscribers">#</T>
</th>
<th class="text-right d-none d-lg-table-cell">
<T i18nKey="posts">#</T>
</th>
<th class="text-right d-none d-lg-table-cell">
<T i18nKey="comments">#</T>
</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{this.state.communities.map(community => {this.state.communities.map(community => (
<tr> <tr>
<td><Link to={`/c/${community.name}`}>{community.name}</Link></td> <td>
<Link to={`/c/${community.name}`}>
{community.name}
</Link>
</td>
<td class="d-none d-lg-table-cell">{community.title}</td> <td class="d-none d-lg-table-cell">{community.title}</td>
<td>{community.category_name}</td> <td>{community.category_name}</td>
<td class="text-right">{community.number_of_subscribers}</td>
<td class="text-right d-none d-lg-table-cell">{community.number_of_posts}</td>
<td class="text-right d-none d-lg-table-cell">{community.number_of_comments}</td>
<td class="text-right"> <td class="text-right">
{community.subscribed ? {community.number_of_subscribers}
<span class="pointer btn-link" onClick={linkEvent(community.id, this.handleUnsubscribe)}><T i18nKey="unsubscribe">#</T></span> : </td>
<span class="pointer btn-link" onClick={linkEvent(community.id, this.handleSubscribe)}><T i18nKey="subscribe">#</T></span> <td class="text-right d-none d-lg-table-cell">
} {community.number_of_posts}
</td>
<td class="text-right d-none d-lg-table-cell">
{community.number_of_comments}
</td>
<td class="text-right">
{community.subscribed ? (
<span
class="pointer btn-link"
onClick={linkEvent(
community.id,
this.handleUnsubscribe
)}
>
<T i18nKey="unsubscribe">#</T>
</span>
) : (
<span
class="pointer btn-link"
onClick={linkEvent(
community.id,
this.handleSubscribe
)}
>
<T i18nKey="subscribe">#</T>
</span>
)}
</td> </td>
</tr> </tr>
)} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
{this.paginator()} {this.paginator()}
</div> </div>
} )}
</div> </div>
); );
} }
@ -110,10 +172,20 @@ export class Communities extends Component<any, CommunitiesState> {
paginator() { paginator() {
return ( return (
<div class="mt-2"> <div class="mt-2">
{this.state.page > 1 && {this.state.page > 1 && (
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button> <button
} class="btn btn-sm btn-secondary mr-1"
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button> onClick={linkEvent(this, this.prevPage)}
>
<T i18nKey="prev">#</T>
</button>
)}
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
<T i18nKey="next">#</T>
</button>
</div> </div>
); );
} }
@ -139,7 +211,7 @@ export class Communities extends Component<any, CommunitiesState> {
handleUnsubscribe(communityId: number) { handleUnsubscribe(communityId: number) {
let form: FollowCommunityForm = { let form: FollowCommunityForm = {
community_id: communityId, community_id: communityId,
follow: false follow: false,
}; };
WebSocketService.Instance.followCommunity(form); WebSocketService.Instance.followCommunity(form);
} }
@ -147,7 +219,7 @@ export class Communities extends Component<any, CommunitiesState> {
handleSubscribe(communityId: number) { handleSubscribe(communityId: number) {
let form: FollowCommunityForm = { let form: FollowCommunityForm = {
community_id: communityId, community_id: communityId,
follow: true follow: true,
}; };
WebSocketService.Instance.followCommunity(form); WebSocketService.Instance.followCommunity(form);
} }
@ -157,10 +229,9 @@ export class Communities extends Component<any, CommunitiesState> {
sort: SortType[SortType.TopAll], sort: SortType[SortType.TopAll],
limit: 100, limit: 100,
page: this.state.page, page: this.state.page,
} };
WebSocketService.Instance.listCommunities(listCommunitiesForm); WebSocketService.Instance.listCommunities(listCommunitiesForm);
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -172,7 +243,9 @@ export class Communities extends Component<any, CommunitiesState> {
} else if (op == UserOperation.ListCommunities) { } else if (op == UserOperation.ListCommunities) {
let res: ListCommunitiesResponse = msg; let res: ListCommunitiesResponse = msg;
this.state.communities = res.communities; this.state.communities = res.communities;
this.state.communities.sort((a, b) => b.number_of_subscribers - a.number_of_subscribers); this.state.communities.sort(
(a, b) => b.number_of_subscribers - a.number_of_subscribers
);
this.state.loading = false; this.state.loading = false;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state); this.setState(this.state);

View file

@ -1,7 +1,13 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs"; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { CommunityForm as CommunityFormI, UserOperation, Category, ListCategoriesResponse, CommunityResponse } from '../interfaces'; import {
CommunityForm as CommunityFormI,
UserOperation,
Category,
ListCategoriesResponse,
CommunityResponse,
} from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { msgOp, capitalizeFirstLetter } from '../utils'; import { msgOp, capitalizeFirstLetter } from '../utils';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
@ -23,7 +29,10 @@ interface CommunityFormState {
loading: boolean; loading: boolean;
} }
export class CommunityForm extends Component<CommunityFormProps, CommunityFormState> { export class CommunityForm extends Component<
CommunityFormProps,
CommunityFormState
> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: CommunityFormState = { private emptyState: CommunityFormState = {
@ -34,8 +43,8 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
nsfw: false, nsfw: false,
}, },
categories: [], categories: [],
loading: false loading: false,
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -50,16 +59,23 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
description: this.props.community.description, description: this.props.community.description,
edit_id: this.props.community.id, edit_id: this.props.community.id,
nsfw: this.props.community.nsfw, nsfw: this.props.community.nsfw,
auth: null auth: null,
} };
} }
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), msg => this.parseMessage(msg),
(err) => console.error(err), err => console.error(err),
() => console.log("complete") () => console.log('complete')
); );
WebSocketService.Instance.listCategories(); WebSocketService.Instance.listCategories();
@ -73,53 +89,110 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
render() { render() {
return ( return (
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}> <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label"><T i18nKey="name">#</T></label> <label class="col-12 col-form-label">
<T i18nKey="name">#</T>
</label>
<div class="col-12"> <div class="col-12">
<input type="text" class="form-control" value={this.state.communityForm.name} onInput={linkEvent(this, this.handleCommunityNameChange)} required minLength={3} maxLength={20} pattern="[a-z0-9_]+" title={i18n.t('community_reqs')}/> <input
type="text"
class="form-control"
value={this.state.communityForm.name}
onInput={linkEvent(this, this.handleCommunityNameChange)}
required
minLength={3}
maxLength={20}
pattern="[a-z0-9_]+"
title={i18n.t('community_reqs')}
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label"><T i18nKey="title">#</T></label> <label class="col-12 col-form-label">
<T i18nKey="title">#</T>
</label>
<div class="col-12"> <div class="col-12">
<input type="text" value={this.state.communityForm.title} onInput={linkEvent(this, this.handleCommunityTitleChange)} class="form-control" required minLength={3} maxLength={100} /> <input
type="text"
value={this.state.communityForm.title}
onInput={linkEvent(this, this.handleCommunityTitleChange)}
class="form-control"
required
minLength={3}
maxLength={100}
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label"><T i18nKey="sidebar">#</T></label> <label class="col-12 col-form-label">
<T i18nKey="sidebar">#</T>
</label>
<div class="col-12"> <div class="col-12">
<textarea value={this.state.communityForm.description} onInput={linkEvent(this, this.handleCommunityDescriptionChange)} class="form-control" rows={3} maxLength={10000} /> <textarea
value={this.state.communityForm.description}
onInput={linkEvent(this, this.handleCommunityDescriptionChange)}
class="form-control"
rows={3}
maxLength={10000}
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label"><T i18nKey="category">#</T></label> <label class="col-12 col-form-label">
<T i18nKey="category">#</T>
</label>
<div class="col-12"> <div class="col-12">
<select class="form-control" value={this.state.communityForm.category_id} onInput={linkEvent(this, this.handleCommunityCategoryChange)}> <select
{this.state.categories.map(category => class="form-control"
value={this.state.communityForm.category_id}
onInput={linkEvent(this, this.handleCommunityCategoryChange)}
>
{this.state.categories.map(category => (
<option value={category.id}>{category.name}</option> <option value={category.id}>{category.name}</option>
)} ))}
</select> </select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-12"> <div class="col-12">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" checked={this.state.communityForm.nsfw} onChange={linkEvent(this, this.handleCommunityNsfwChange)}/> <input
<label class="form-check-label"><T i18nKey="nsfw">#</T></label> class="form-check-input"
type="checkbox"
checked={this.state.communityForm.nsfw}
onChange={linkEvent(this, this.handleCommunityNsfwChange)}
/>
<label class="form-check-label">
<T i18nKey="nsfw">#</T>
</label>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-12"> <div class="col-12">
<button type="submit" class="btn btn-secondary mr-2"> <button type="submit" class="btn btn-secondary mr-2">
{this.state.loading ? {this.state.loading ? (
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : <svg class="icon icon-spinner spin">
this.props.community ? capitalizeFirstLetter(i18n.t('save')) : capitalizeFirstLetter(i18n.t('create'))}</button> <use xlinkHref="#icon-spinner"></use>
{this.props.community && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}><T i18nKey="cancel">#</T></button>} </svg>
) : this.props.community ? (
capitalizeFirstLetter(i18n.t('save'))
) : (
capitalizeFirstLetter(i18n.t('create'))
)}
</button>
{this.props.community && (
<button
type="button"
class="btn btn-secondary"
onClick={linkEvent(this, this.handleCancel)}
>
<T i18nKey="cancel">#</T>
</button>
)}
</div> </div>
</div> </div>
</form> </form>
@ -193,5 +266,4 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
this.props.onEdit(res.community); this.props.onEdit(res.community);
} }
} }
} }

View file

@ -1,11 +1,29 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs"; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, CommunityUser, UserView, SortType, Post, GetPostsForm, ListingType, GetPostsResponse, CreatePostLikeResponse } from '../interfaces'; import {
UserOperation,
Community as CommunityI,
GetCommunityResponse,
CommunityResponse,
CommunityUser,
UserView,
SortType,
Post,
GetPostsForm,
ListingType,
GetPostsResponse,
CreatePostLikeResponse,
} from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
import { Sidebar } from './sidebar'; import { Sidebar } from './sidebar';
import { msgOp, routeSortTypeToEnum, fetchLimit, postRefetchSeconds } from '../utils'; import {
msgOp,
routeSortTypeToEnum,
fetchLimit,
postRefetchSeconds,
} from '../utils';
import { T, i18n } from 'inferno-i18next'; import { T, i18n } from 'inferno-i18next';
interface State { interface State {
@ -21,7 +39,6 @@ interface State {
} }
export class Community extends Component<any, State> { export class Community extends Component<any, State> {
private subscription: Subscription; private subscription: Subscription;
private postFetcher: any; private postFetcher: any;
private emptyState: State = { private emptyState: State = {
@ -49,16 +66,16 @@ export class Community extends Component<any, State> {
posts: [], posts: [],
sort: this.getSortTypeFromProps(this.props), sort: this.getSortTypeFromProps(this.props),
page: this.getPageFromProps(this.props), page: this.getPageFromProps(this.props),
} };
getSortTypeFromProps(props: any): SortType { getSortTypeFromProps(props: any): SortType {
return (props.match.params.sort) ? return props.match.params.sort
routeSortTypeToEnum(props.match.params.sort) : ? routeSortTypeToEnum(props.match.params.sort)
SortType.Hot; : SortType.Hot;
} }
getPageFromProps(props: any): number { getPageFromProps(props: any): number {
return (props.match.params.page) ? Number(props.match.params.page) : 1; return props.match.params.page ? Number(props.match.params.page) : 1;
} }
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -67,10 +84,17 @@ export class Community extends Component<any, State> {
this.state = this.emptyState; this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), msg => this.parseMessage(msg),
(err) => console.error(err), err => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
@ -79,7 +103,6 @@ export class Community extends Component<any, State> {
} else if (this.state.communityName) { } else if (this.state.communityName) {
WebSocketService.Instance.getCommunityByName(this.state.communityName); WebSocketService.Instance.getCommunityByName(this.state.communityName);
} }
} }
componentWillUnmount() { componentWillUnmount() {
@ -100,17 +123,27 @@ export class Community extends Component<any, State> {
render() { render() {
return ( return (
<div class="container"> <div class="container">
{this.state.loading ? {this.state.loading ? (
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : <h5>
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
</h5>
) : (
<div class="row"> <div class="row">
<div class="col-12 col-md-8"> <div class="col-12 col-md-8">
<h5>{this.state.community.title} <h5>
{this.state.community.removed && {this.state.community.title}
<small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small> {this.state.community.removed && (
} <small className="ml-2 text-muted font-italic">
{this.state.community.nsfw && <T i18nKey="removed">#</T>
<small className="ml-2 text-muted font-italic"><T i18nKey="nsfw">#</T></small> </small>
} )}
{this.state.community.nsfw && (
<small className="ml-2 text-muted font-italic">
<T i18nKey="nsfw">#</T>
</small>
)}
</h5> </h5>
{this.selects()} {this.selects()}
<PostListings posts={this.state.posts} /> <PostListings posts={this.state.posts} />
@ -124,36 +157,66 @@ export class Community extends Component<any, State> {
/> />
</div> </div>
</div> </div>
} )}
</div> </div>
) );
} }
selects() { selects() {
return ( return (
<div className="mb-2"> <div className="mb-2">
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto"> <select
<option disabled><T i18nKey="sort_type">#</T></option> value={this.state.sort}
<option value={SortType.Hot}><T i18nKey="hot">#</T></option> onChange={linkEvent(this, this.handleSortChange)}
<option value={SortType.New}><T i18nKey="new">#</T></option> class="custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="sort_type">#</T>
</option>
<option value={SortType.Hot}>
<T i18nKey="hot">#</T>
</option>
<option value={SortType.New}>
<T i18nKey="new">#</T>
</option>
<option disabled></option> <option disabled></option>
<option value={SortType.TopDay}><T i18nKey="top_day">#</T></option> <option value={SortType.TopDay}>
<option value={SortType.TopWeek}><T i18nKey="week">#</T></option> <T i18nKey="top_day">#</T>
<option value={SortType.TopMonth}><T i18nKey="month">#</T></option> </option>
<option value={SortType.TopYear}><T i18nKey="year">#</T></option> <option value={SortType.TopWeek}>
<option value={SortType.TopAll}><T i18nKey="all">#</T></option> <T i18nKey="week">#</T>
</option>
<option value={SortType.TopMonth}>
<T i18nKey="month">#</T>
</option>
<option value={SortType.TopYear}>
<T i18nKey="year">#</T>
</option>
<option value={SortType.TopAll}>
<T i18nKey="all">#</T>
</option>
</select> </select>
</div> </div>
) );
} }
paginator() { paginator() {
return ( return (
<div class="my-2"> <div class="my-2">
{this.state.page > 1 && {this.state.page > 1 && (
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button> <button
} class="btn btn-sm btn-secondary mr-1"
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button> onClick={linkEvent(this, this.prevPage)}
>
<T i18nKey="prev">#</T>
</button>
)}
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
<T i18nKey="next">#</T>
</button>
</div> </div>
); );
} }
@ -185,7 +248,9 @@ export class Community extends Component<any, State> {
updateUrl() { updateUrl() {
let sortStr = SortType[this.state.sort].toLowerCase(); let sortStr = SortType[this.state.sort].toLowerCase();
this.props.history.push(`/c/${this.state.community.name}/sort/${sortStr}/page/${this.state.page}`); this.props.history.push(
`/c/${this.state.community.name}/sort/${sortStr}/page/${this.state.page}`
);
} }
keepFetchingPosts() { keepFetchingPosts() {
@ -200,7 +265,7 @@ export class Community extends Component<any, State> {
sort: SortType[this.state.sort], sort: SortType[this.state.sort],
type_: ListingType[ListingType.Community], type_: ListingType[ListingType.Community],
community_id: this.state.community.id, community_id: this.state.community.id,
} };
WebSocketService.Instance.getPosts(getPostsForm); WebSocketService.Instance.getPosts(getPostsForm);
} }
@ -225,7 +290,8 @@ export class Community extends Component<any, State> {
} else if (op == UserOperation.FollowCommunity) { } else if (op == UserOperation.FollowCommunity) {
let res: CommunityResponse = msg; let res: CommunityResponse = msg;
this.state.community.subscribed = res.community.subscribed; this.state.community.subscribed = res.community.subscribed;
this.state.community.number_of_subscribers = res.community.number_of_subscribers; this.state.community.number_of_subscribers =
res.community.number_of_subscribers;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetPosts) { } else if (op == UserOperation.GetPosts) {
let res: GetPostsResponse = msg; let res: GetPostsResponse = msg;
@ -243,4 +309,3 @@ export class Community extends Component<any, State> {
} }
} }
} }

View file

@ -6,14 +6,15 @@ import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
export class CreateCommunity extends Component<any, any> { export class CreateCommunity extends Component<any, any> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.handleCommunityCreate = this.handleCommunityCreate.bind(this); this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
} }
componentDidMount() { componentDidMount() {
document.title = `${i18n.t('create_community')} - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('create_community')} - ${
WebSocketService.Instance.site.name
}`;
} }
render() { render() {
@ -21,17 +22,17 @@ export class CreateCommunity extends Component<any, any> {
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5><T i18nKey="create_community">#</T></h5> <h5>
<T i18nKey="create_community">#</T>
</h5>
<CommunityForm onCreate={this.handleCommunityCreate} /> <CommunityForm onCreate={this.handleCommunityCreate} />
</div> </div>
</div> </div>
</div> </div>
) );
} }
handleCommunityCreate(community: Community) { handleCommunityCreate(community: Community) {
this.props.history.push(`/c/${community.name}`); this.props.history.push(`/c/${community.name}`);
} }
} }

View file

@ -6,14 +6,15 @@ import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
export class CreatePost extends Component<any, any> { export class CreatePost extends Component<any, any> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.handlePostCreate = this.handlePostCreate.bind(this); this.handlePostCreate = this.handlePostCreate.bind(this);
} }
componentDidMount() { componentDidMount() {
document.title = `${i18n.t('create_post')} - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('create_post')} - ${
WebSocketService.Instance.site.name
}`;
} }
render() { render() {
@ -21,21 +22,23 @@ export class CreatePost extends Component<any, any> {
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5><T i18nKey="create_post">#</T></h5> <h5>
<T i18nKey="create_post">#</T>
</h5>
<PostForm onCreate={this.handlePostCreate} params={this.params} /> <PostForm onCreate={this.handlePostCreate} params={this.params} />
</div> </div>
</div> </div>
</div> </div>
) );
} }
get params(): PostFormParams { get params(): PostFormParams {
let urlParams = new URLSearchParams(this.props.location.search); let urlParams = new URLSearchParams(this.props.location.search);
let params: PostFormParams = { let params: PostFormParams = {
name: urlParams.get("name"), name: urlParams.get('name'),
community: urlParams.get("community") || this.prevCommunityName, community: urlParams.get('community') || this.prevCommunityName,
body: urlParams.get("body"), body: urlParams.get('body'),
url: urlParams.get("url"), url: urlParams.get('url'),
}; };
return params; return params;
@ -46,8 +49,8 @@ export class CreatePost extends Component<any, any> {
return this.props.match.params.name; return this.props.match.params.name;
} else if (this.props.location.state) { } else if (this.props.location.state) {
let lastLocation = this.props.location.state.prevPath; let lastLocation = this.props.location.state.prevPath;
if (lastLocation.includes("/c/")) { if (lastLocation.includes('/c/')) {
return lastLocation.split("/c/")[1]; return lastLocation.split('/c/')[1];
} }
} }
return undefined; return undefined;
@ -57,5 +60,3 @@ export class CreatePost extends Component<any, any> {
this.props.history.push(`/post/${id}`); this.props.history.push(`/post/${id}`);
} }
} }

View file

@ -5,8 +5,6 @@ import { version } from '../version';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
export class Footer extends Component<any, any> { export class Footer extends Component<any, any> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
} }
@ -20,16 +18,24 @@ export class Footer extends Component<any, any> {
<span class="navbar-text">{version}</span> <span class="navbar-text">{version}</span>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/modlog"><T i18nKey="modlog">#</T></Link> <Link class="nav-link" to="/modlog">
<T i18nKey="modlog">#</T>
</Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href={`${repoUrl}/blob/master/docs/api.md`}><T i18nKey="api">#</T></a> <a class="nav-link" href={`${repoUrl}/blob/master/docs/api.md`}>
<T i18nKey="api">#</T>
</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/sponsors"><T i18nKey="sponsors">#</T></Link> <Link class="nav-link" to="/sponsors">
<T i18nKey="sponsors">#</T>
</Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href={repoUrl}><T i18nKey="code">#</T></a> <a class="nav-link" href={repoUrl}>
<T i18nKey="code">#</T>
</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -37,4 +43,3 @@ export class Footer extends Component<any, any> {
); );
} }
} }

View file

@ -1,8 +1,15 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Subscription } from "rxjs"; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Comment, SortType, GetRepliesForm, GetRepliesResponse, CommentResponse } from '../interfaces'; import {
UserOperation,
Comment,
SortType,
GetRepliesForm,
GetRepliesResponse,
CommentResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils'; import { msgOp } from '../utils';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
@ -10,7 +17,8 @@ import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
enum UnreadType { enum UnreadType {
Unread, All Unread,
All,
} }
interface InboxState { interface InboxState {
@ -21,14 +29,13 @@ interface InboxState {
} }
export class Inbox extends Component<any, InboxState> { export class Inbox extends Component<any, InboxState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: InboxState = { private emptyState: InboxState = {
unreadType: UnreadType.Unread, unreadType: UnreadType.Unread,
replies: [], replies: [],
sort: SortType.New, sort: SortType.New,
page: 1, page: 1,
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -36,10 +43,17 @@ export class Inbox extends Component<any, InboxState> {
this.state = this.emptyState; this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), msg => this.parseMessage(msg),
(err) => console.error(err), err => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
@ -51,7 +65,9 @@ export class Inbox extends Component<any, InboxState> {
} }
componentDidMount() { componentDidMount() {
document.title = `/u/${UserService.Instance.user.username} ${i18n.t('inbox')} - ${WebSocketService.Instance.site.name}`; document.title = `/u/${UserService.Instance.user.username} ${i18n.t(
'inbox'
)} - ${WebSocketService.Instance.site.name}`;
} }
render() { render() {
@ -61,52 +77,86 @@ export class Inbox extends Component<any, InboxState> {
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<h5 class="mb-0"> <h5 class="mb-0">
<span><T i18nKey="inbox_for" interpolation={{user: user.username}}>#<Link to={`/u/${user.username}`}>#</Link></T></span> <span>
<T i18nKey="inbox_for" interpolation={{ user: user.username }}>
#<Link to={`/u/${user.username}`}>#</Link>
</T>
</span>
</h5> </h5>
{this.state.replies.length > 0 && this.state.unreadType == UnreadType.Unread && {this.state.replies.length > 0 &&
this.state.unreadType == UnreadType.Unread && (
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={this.markAllAsRead}><T i18nKey="mark_all_as_read">#</T></span> <span class="pointer" onClick={this.markAllAsRead}>
<T i18nKey="mark_all_as_read">#</T>
</span>
</li> </li>
</ul> </ul>
} )}
{this.selects()} {this.selects()}
{this.replies()} {this.replies()}
{this.paginator()} {this.paginator()}
</div> </div>
</div> </div>
</div> </div>
) );
} }
selects() { selects() {
return ( return (
<div className="mb-2"> <div className="mb-2">
<select value={this.state.unreadType} onChange={linkEvent(this, this.handleUnreadTypeChange)} class="custom-select custom-select-sm w-auto"> <select
<option disabled><T i18nKey="type">#</T></option> value={this.state.unreadType}
<option value={UnreadType.Unread}><T i18nKey="unread">#</T></option> onChange={linkEvent(this, this.handleUnreadTypeChange)}
<option value={UnreadType.All}><T i18nKey="all">#</T></option> class="custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="type">#</T>
</option>
<option value={UnreadType.Unread}>
<T i18nKey="unread">#</T>
</option>
<option value={UnreadType.All}>
<T i18nKey="all">#</T>
</option>
</select> </select>
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2"> <select
<option disabled><T i18nKey="sort_type">#</T></option> value={this.state.sort}
<option value={SortType.New}><T i18nKey="new">#</T></option> onChange={linkEvent(this, this.handleSortChange)}
<option value={SortType.TopDay}><T i18nKey="top_day">#</T></option> class="custom-select custom-select-sm w-auto ml-2"
<option value={SortType.TopWeek}><T i18nKey="week">#</T></option> >
<option value={SortType.TopMonth}><T i18nKey="month">#</T></option> <option disabled>
<option value={SortType.TopYear}><T i18nKey="year">#</T></option> <T i18nKey="sort_type">#</T>
<option value={SortType.TopAll}><T i18nKey="all">#</T></option> </option>
<option value={SortType.New}>
<T i18nKey="new">#</T>
</option>
<option value={SortType.TopDay}>
<T i18nKey="top_day">#</T>
</option>
<option value={SortType.TopWeek}>
<T i18nKey="week">#</T>
</option>
<option value={SortType.TopMonth}>
<T i18nKey="month">#</T>
</option>
<option value={SortType.TopYear}>
<T i18nKey="year">#</T>
</option>
<option value={SortType.TopAll}>
<T i18nKey="all">#</T>
</option>
</select> </select>
</div> </div>
) );
} }
replies() { replies() {
return ( return (
<div> <div>
{this.state.replies.map(reply => {this.state.replies.map(reply => (
<CommentNodes nodes={[{ comment: reply }]} noIndent markable /> <CommentNodes nodes={[{ comment: reply }]} noIndent markable />
)} ))}
</div> </div>
); );
} }
@ -114,10 +164,20 @@ export class Inbox extends Component<any, InboxState> {
paginator() { paginator() {
return ( return (
<div class="mt-2"> <div class="mt-2">
{this.state.page > 1 && {this.state.page > 1 && (
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button> <button
} class="btn btn-sm btn-secondary mr-1"
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button> onClick={linkEvent(this, this.prevPage)}
>
<T i18nKey="prev">#</T>
</button>
)}
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
<T i18nKey="next">#</T>
</button>
</div> </div>
); );
} }
@ -144,7 +204,7 @@ export class Inbox extends Component<any, InboxState> {
refetch() { refetch() {
let form: GetRepliesForm = { let form: GetRepliesForm = {
sort: SortType[this.state.sort], sort: SortType[this.state.sort],
unread_only: (this.state.unreadType == UnreadType.Unread), unread_only: this.state.unreadType == UnreadType.Unread,
page: this.state.page, page: this.state.page,
limit: 9999, limit: 9999,
}; };
@ -168,7 +228,10 @@ export class Inbox extends Component<any, InboxState> {
if (msg.error) { if (msg.error) {
alert(i18n.t(msg.error)); alert(i18n.t(msg.error));
return; return;
} else if (op == UserOperation.GetReplies || op == UserOperation.MarkAllAsRead) { } else if (
op == UserOperation.GetReplies ||
op == UserOperation.MarkAllAsRead
) {
let res: GetRepliesResponse = msg; let res: GetRepliesResponse = msg;
this.state.replies = res.replies; this.state.replies = res.replies;
this.sendRepliesCount(); this.sendRepliesCount();
@ -188,7 +251,9 @@ export class Inbox extends Component<any, InboxState> {
// If youre in the unread view, just remove it from the list // If youre in the unread view, just remove it from the list
if (this.state.unreadType == UnreadType.Unread && res.comment.read) { if (this.state.unreadType == UnreadType.Unread && res.comment.read) {
this.state.replies = this.state.replies.filter(r => r.id !== res.comment.id); this.state.replies = this.state.replies.filter(
r => r.id !== res.comment.id
);
} else { } else {
let found = this.state.replies.find(c => c.id == res.comment.id); let found = this.state.replies.find(c => c.id == res.comment.id);
found.read = res.comment.read; found.read = res.comment.read;
@ -208,18 +273,21 @@ export class Inbox extends Component<any, InboxState> {
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let res: CommentResponse = msg; let res: CommentResponse = msg;
let found: Comment = this.state.replies.find(c => c.id === res.comment.id); let found: Comment = this.state.replies.find(
c => c.id === res.comment.id
);
found.score = res.comment.score; found.score = res.comment.score;
found.upvotes = res.comment.upvotes; found.upvotes = res.comment.upvotes;
found.downvotes = res.comment.downvotes; found.downvotes = res.comment.downvotes;
if (res.comment.my_vote !== null) if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
found.my_vote = res.comment.my_vote;
this.setState(this.state); this.setState(this.state);
} }
} }
sendRepliesCount() { sendRepliesCount() {
UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: this.state.replies.filter(r => !r.read).length}); UserService.Instance.sub.next({
user: UserService.Instance.user,
unreadCount: this.state.replies.filter(r => !r.read).length,
});
} }
} }

View file

@ -1,7 +1,12 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs"; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { LoginForm, RegisterForm, LoginResponse, UserOperation } from '../interfaces'; import {
LoginForm,
RegisterForm,
LoginResponse,
UserOperation,
} from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils'; import { msgOp } from '../utils';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
@ -14,14 +19,13 @@ interface State {
registerLoading: boolean; registerLoading: boolean;
} }
export class Login extends Component<any, State> { export class Login extends Component<any, State> {
private subscription: Subscription; private subscription: Subscription;
emptyState: State = { emptyState: State = {
loginForm: { loginForm: {
username_or_email: undefined, username_or_email: undefined,
password: undefined password: undefined,
}, },
registerForm: { registerForm: {
username: undefined, username: undefined,
@ -32,7 +36,7 @@ export class Login extends Component<any, State> {
}, },
loginLoading: false, loginLoading: false,
registerLoading: false, registerLoading: false,
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -40,11 +44,18 @@ export class Login extends Component<any, State> {
this.state = this.emptyState; this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), msg => this.parseMessage(msg),
(err) => console.error(err), err => console.error(err),
() => console.log("complete") () => console.log('complete')
); );
} }
@ -53,22 +64,20 @@ export class Login extends Component<any, State> {
} }
componentDidMount() { componentDidMount() {
document.title = `${i18n.t('login')} - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('login')} - ${
WebSocketService.Instance.site.name
}`;
} }
render() { render() {
return ( return (
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 mb-4"> <div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
{this.loginForm()} <div class="col-12 col-lg-6">{this.registerForm()}</div>
</div>
<div class="col-12 col-lg-6">
{this.registerForm()}
</div> </div>
</div> </div>
</div> );
)
} }
loginForm() { loginForm() {
@ -77,21 +86,45 @@ export class Login extends Component<any, State> {
<form onSubmit={linkEvent(this, this.handleLoginSubmit)}> <form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
<h5>Login</h5> <h5>Login</h5>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="email_or_username">#</T></label> <label class="col-sm-2 col-form-label">
<T i18nKey="email_or_username">#</T>
</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" class="form-control" value={this.state.loginForm.username_or_email} onInput={linkEvent(this, this.handleLoginUsernameChange)} required minLength={3} /> <input
type="text"
class="form-control"
value={this.state.loginForm.username_or_email}
onInput={linkEvent(this, this.handleLoginUsernameChange)}
required
minLength={3}
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="password">#</T></label> <label class="col-sm-2 col-form-label">
<T i18nKey="password">#</T>
</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="password" value={this.state.loginForm.password} onInput={linkEvent(this, this.handleLoginPasswordChange)} class="form-control" required /> <input
type="password"
value={this.state.loginForm.password}
onInput={linkEvent(this, this.handleLoginPasswordChange)}
class="form-control"
required
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<button type="submit" class="btn btn-secondary">{this.state.loginLoading ? <button type="submit" class="btn btn-secondary">
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('login')}</button> {this.state.loginLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
i18n.t('login')
)}
</button>
</div> </div>
</div> </div>
</form> </form>
@ -101,43 +134,95 @@ export class Login extends Component<any, State> {
registerForm() { registerForm() {
return ( return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}> <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h5><T i18nKey="sign_up">#</T></h5> <h5>
<T i18nKey="sign_up">#</T>
</h5>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="username">#</T></label> <label class="col-sm-2 col-form-label">
<T i18nKey="username">#</T>
</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" class="form-control" value={this.state.registerForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} maxLength={20} pattern="[a-zA-Z0-9_]+" /> <input
type="text"
class="form-control"
value={this.state.registerForm.username}
onInput={linkEvent(this, this.handleRegisterUsernameChange)}
required
minLength={3}
maxLength={20}
pattern="[a-zA-Z0-9_]+"
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="email">#</T></label> <label class="col-sm-2 col-form-label">
<T i18nKey="email">#</T>
</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="email" class="form-control" placeholder={i18n.t('optional')} value={this.state.registerForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} /> <input
type="email"
class="form-control"
placeholder={i18n.t('optional')}
value={this.state.registerForm.email}
onInput={linkEvent(this, this.handleRegisterEmailChange)}
minLength={3}
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="password">#</T></label> <label class="col-sm-2 col-form-label">
<T i18nKey="password">#</T>
</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="password" value={this.state.registerForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required /> <input
type="password"
value={this.state.registerForm.password}
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
class="form-control"
required
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="verify_password">#</T></label> <label class="col-sm-2 col-form-label">
<T i18nKey="verify_password">#</T>
</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="password" value={this.state.registerForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required /> <input
type="password"
value={this.state.registerForm.password_verify}
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
class="form-control"
required
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" checked={this.state.registerForm.show_nsfw} onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}/> <input
<label class="form-check-label"><T i18nKey="show_nsfw">#</T></label> class="form-check-input"
type="checkbox"
checked={this.state.registerForm.show_nsfw}
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
/>
<label class="form-check-label">
<T i18nKey="show_nsfw">#</T>
</label>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<button type="submit" class="btn btn-secondary">{this.state.registerLoading ? <button type="submit" class="btn btn-secondary">
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('sign_up')}</button> {this.state.registerLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
i18n.t('sign_up')
)}
</button>
</div> </div>
</div> </div>
</form> </form>
@ -217,5 +302,4 @@ export class Login extends Component<any, State> {
} }
} }
} }
} }

View file

@ -1,12 +1,35 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Subscription } from "rxjs"; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, CommunityUser, GetFollowedCommunitiesResponse, ListCommunitiesForm, ListCommunitiesResponse, Community, SortType, GetSiteResponse, ListingType, SiteResponse, GetPostsResponse, CreatePostLikeResponse, Post, GetPostsForm } from '../interfaces'; import {
UserOperation,
CommunityUser,
GetFollowedCommunitiesResponse,
ListCommunitiesForm,
ListCommunitiesResponse,
Community,
SortType,
GetSiteResponse,
ListingType,
SiteResponse,
GetPostsResponse,
CreatePostLikeResponse,
Post,
GetPostsForm,
} from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
import { SiteForm } from './site-form'; import { SiteForm } from './site-form';
import { msgOp, repoUrl, mdToHtml, fetchLimit, routeSortTypeToEnum, routeListingTypeToEnum, postRefetchSeconds } from '../utils'; import {
msgOp,
repoUrl,
mdToHtml,
fetchLimit,
routeSortTypeToEnum,
routeListingTypeToEnum,
postRefetchSeconds,
} from '../utils';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
@ -23,7 +46,6 @@ interface MainState {
} }
export class Main extends Component<any, MainState> { export class Main extends Component<any, MainState> {
private subscription: Subscription; private subscription: Subscription;
private postFetcher: any; private postFetcher: any;
private emptyState: MainState = { private emptyState: MainState = {
@ -52,24 +74,24 @@ export class Main extends Component<any, MainState> {
type_: this.getListingTypeFromProps(this.props), type_: this.getListingTypeFromProps(this.props),
sort: this.getSortTypeFromProps(this.props), sort: this.getSortTypeFromProps(this.props),
page: this.getPageFromProps(this.props), page: this.getPageFromProps(this.props),
} };
getListingTypeFromProps(props: any): ListingType { getListingTypeFromProps(props: any): ListingType {
return (props.match.params.type) ? return props.match.params.type
routeListingTypeToEnum(props.match.params.type) : ? routeListingTypeToEnum(props.match.params.type)
UserService.Instance.user ? : UserService.Instance.user
ListingType.Subscribed : ? ListingType.Subscribed
ListingType.All; : ListingType.All;
} }
getSortTypeFromProps(props: any): SortType { getSortTypeFromProps(props: any): SortType {
return (props.match.params.sort) ? return props.match.params.sort
routeSortTypeToEnum(props.match.params.sort) : ? routeSortTypeToEnum(props.match.params.sort)
SortType.Hot; : SortType.Hot;
} }
getPageFromProps(props: any): number { getPageFromProps(props: any): number {
return (props.match.params.page) ? Number(props.match.params.page) : 1; return props.match.params.page ? Number(props.match.params.page) : 1;
} }
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -79,10 +101,17 @@ export class Main extends Component<any, MainState> {
this.handleEditCancel = this.handleEditCancel.bind(this); this.handleEditCancel = this.handleEditCancel.bind(this);
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), msg => this.parseMessage(msg),
(err) => console.error(err), err => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
@ -94,8 +123,8 @@ export class Main extends Component<any, MainState> {
let listCommunitiesForm: ListCommunitiesForm = { let listCommunitiesForm: ListCommunitiesForm = {
sort: SortType[SortType.Hot], sort: SortType[SortType.Hot],
limit: 6 limit: 6,
} };
WebSocketService.Instance.listCommunities(listCommunitiesForm); WebSocketService.Instance.listCommunities(listCommunitiesForm);
@ -109,7 +138,10 @@ export class Main extends Component<any, MainState> {
// Necessary for back button for some reason // Necessary for back button for some reason
componentWillReceiveProps(nextProps: any) { componentWillReceiveProps(nextProps: any) {
if (nextProps.history.action == 'POP' || nextProps.history.action == 'PUSH') { if (
nextProps.history.action == 'POP' ||
nextProps.history.action == 'PUSH'
) {
this.state.type_ = this.getListingTypeFromProps(nextProps); this.state.type_ = this.getListingTypeFromProps(nextProps);
this.state.sort = this.getSortTypeFromProps(nextProps); this.state.sort = this.getSortTypeFromProps(nextProps);
this.state.page = this.getPageFromProps(nextProps); this.state.page = this.getPageFromProps(nextProps);
@ -122,39 +154,47 @@ export class Main extends Component<any, MainState> {
return ( return (
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-md-8"> <div class="col-12 col-md-8">{this.posts()}</div>
{this.posts()} <div class="col-12 col-md-4">{this.my_sidebar()}</div>
</div>
<div class="col-12 col-md-4">
{this.my_sidebar()}
</div> </div>
</div> </div>
</div> );
)
} }
my_sidebar() { my_sidebar() {
return ( return (
<div> <div>
{!this.state.loading && {!this.state.loading && (
<div> <div>
<div class="card border-secondary mb-3"> <div class="card border-secondary mb-3">
<div class="card-body"> <div class="card-body">
{this.trendingCommunities()} {this.trendingCommunities()}
{UserService.Instance.user && this.state.subscribedCommunities.length > 0 && {UserService.Instance.user &&
this.state.subscribedCommunities.length > 0 && (
<div> <div>
<h5> <h5>
<T i18nKey="subscribed_to_communities">#<Link class="text-white" to="/communities">#</Link></T> <T i18nKey="subscribed_to_communities">
#
<Link class="text-white" to="/communities">
#
</Link>
</T>
</h5> </h5>
<ul class="list-inline"> <ul class="list-inline">
{this.state.subscribedCommunities.map(community => {this.state.subscribedCommunities.map(community => (
<li class="list-inline-item"><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li> <li class="list-inline-item">
)} <Link to={`/c/${community.community_name}`}>
{community.community_name}
</Link>
</li>
))}
</ul> </ul>
</div> </div>
} )}
<Link class="btn btn-sm btn-secondary btn-block" <Link
to="/create_community"> class="btn btn-sm btn-secondary btn-block"
to="/create_community"
>
<T i18nKey="create_a_community">#</T> <T i18nKey="create_a_community">#</T>
</Link> </Link>
</div> </div>
@ -162,44 +202,54 @@ export class Main extends Component<any, MainState> {
{this.sidebar()} {this.sidebar()}
{this.landing()} {this.landing()}
</div> </div>
} )}
</div> </div>
) );
} }
trendingCommunities() { trendingCommunities() {
return ( return (
<div> <div>
<h5> <h5>
<T i18nKey="trending_communities">#<Link class="text-white" to="/communities">#</Link></T> <T i18nKey="trending_communities">
#
<Link class="text-white" to="/communities">
#
</Link>
</T>
</h5> </h5>
<ul class="list-inline"> <ul class="list-inline">
{this.state.trendingCommunities.map(community => {this.state.trendingCommunities.map(community => (
<li class="list-inline-item"><Link to={`/c/${community.name}`}>{community.name}</Link></li> <li class="list-inline-item">
)} <Link to={`/c/${community.name}`}>{community.name}</Link>
</li>
))}
</ul> </ul>
</div> </div>
) );
} }
sidebar() { sidebar() {
return ( return (
<div> <div>
{!this.state.showEditSite ? {!this.state.showEditSite ? (
this.siteInfo() : this.siteInfo()
) : (
<SiteForm <SiteForm
site={this.state.site.site} site={this.state.site.site}
onCancel={this.handleEditCancel} onCancel={this.handleEditCancel}
/> />
} )}
</div> </div>
) );
} }
updateUrl() { updateUrl() {
let typeStr = ListingType[this.state.type_].toLowerCase(); let typeStr = ListingType[this.state.type_].toLowerCase();
let sortStr = SortType[this.state.sort].toLowerCase(); let sortStr = SortType[this.state.sort].toLowerCase();
this.props.history.push(`/home/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`); this.props.history.push(
`/home/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
);
} }
siteInfo() { siteInfo() {
@ -208,30 +258,66 @@ export class Main extends Component<any, MainState> {
<div class="card border-secondary mb-3"> <div class="card border-secondary mb-3">
<div class="card-body"> <div class="card-body">
<h5 class="mb-0">{`${this.state.site.site.name}`}</h5> <h5 class="mb-0">{`${this.state.site.site.name}`}</h5>
{this.canAdmin && {this.canAdmin && (
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}> <span
class="pointer"
onClick={linkEvent(this, this.handleEditClick)}
>
<T i18nKey="edit">#</T> <T i18nKey="edit">#</T>
</span> </span>
</li> </li>
</ul> </ul>
} )}
<ul class="my-2 list-inline"> <ul class="my-2 list-inline">
<li className="list-inline-item badge badge-secondary"> <li className="list-inline-item badge badge-secondary">
<T i18nKey="number_online" interpolation={{count: this.state.site.online}}>#</T> <T
i18nKey="number_online"
interpolation={{ count: this.state.site.online }}
>
#
</T>
</li> </li>
<li className="list-inline-item badge badge-secondary"> <li className="list-inline-item badge badge-secondary">
<T i18nKey="number_of_users" interpolation={{count: this.state.site.site.number_of_users}}>#</T> <T
i18nKey="number_of_users"
interpolation={{
count: this.state.site.site.number_of_users,
}}
>
#
</T>
</li> </li>
<li className="list-inline-item badge badge-secondary"> <li className="list-inline-item badge badge-secondary">
<T i18nKey="number_of_communities" interpolation={{count: this.state.site.site.number_of_communities}}>#</T> <T
i18nKey="number_of_communities"
interpolation={{
count: this.state.site.site.number_of_communities,
}}
>
#
</T>
</li> </li>
<li className="list-inline-item badge badge-secondary"> <li className="list-inline-item badge badge-secondary">
<T i18nKey="number_of_posts" interpolation={{count: this.state.site.site.number_of_posts}}>#</T> <T
i18nKey="number_of_posts"
interpolation={{
count: this.state.site.site.number_of_posts,
}}
>
#
</T>
</li> </li>
<li className="list-inline-item badge badge-secondary"> <li className="list-inline-item badge badge-secondary">
<T i18nKey="number_of_comments" interpolation={{count: this.state.site.site.number_of_comments}}>#</T> <T
i18nKey="number_of_comments"
interpolation={{
count: this.state.site.site.number_of_comments,
}}
>
#
</T>
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<Link className="badge badge-secondary" to="/modlog"> <Link className="badge badge-secondary" to="/modlog">
@ -241,23 +327,35 @@ export class Main extends Component<any, MainState> {
</ul> </ul>
<ul class="mt-1 list-inline small mb-0"> <ul class="mt-1 list-inline small mb-0">
<li class="list-inline-item"> <li class="list-inline-item">
<T i18nKey="admins" class="d-inline">#</T>: <T i18nKey="admins" class="d-inline">
#
</T>
:
</li> </li>
{this.state.site.admins.map(admin => {this.state.site.admins.map(admin => (
<li class="list-inline-item"><Link class="text-info" to={`/u/${admin.name}`}>{admin.name}</Link></li> <li class="list-inline-item">
)} <Link class="text-info" to={`/u/${admin.name}`}>
{admin.name}
</Link>
</li>
))}
</ul> </ul>
</div> </div>
</div> </div>
{this.state.site.site.description && {this.state.site.site.description && (
<div class="card border-secondary mb-3"> <div class="card border-secondary mb-3">
<div class="card-body"> <div class="card-body">
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(this.state.site.site.description)} /> <div
className="md-div"
dangerouslySetInnerHTML={mdToHtml(
this.state.site.site.description
)}
/>
</div> </div>
</div> </div>
} )}
</div> </div>
) );
} }
landing() { landing() {
@ -265,43 +363,73 @@ export class Main extends Component<any, MainState> {
<div class="card border-secondary"> <div class="card border-secondary">
<div class="card-body"> <div class="card-body">
<h5> <h5>
<T i18nKey="powered_by" class="d-inline">#</T> <T i18nKey="powered_by" class="d-inline">
<svg class="icon mx-2"><use xlinkHref="#icon-mouse">#</use></svg> #
<a href={repoUrl}>Lemmy<sup>beta</sup></a> </T>
<svg class="icon mx-2">
<use xlinkHref="#icon-mouse">#</use>
</svg>
<a href={repoUrl}>
Lemmy<sup>beta</sup>
</a>
</h5> </h5>
<p class="mb-0"> <p class="mb-0">
<T i18nKey="landing_0">#<a href="https://en.wikipedia.org/wiki/Social_network_aggregation">#</a><a href="https://en.wikipedia.org/wiki/Fediverse">#</a><br></br><code>#</code><br></br><b>#</b><br></br><a href={repoUrl}>#</a><br></br><a href="https://www.rust-lang.org">#</a><a href="https://actix.rs/">#</a><a href="https://infernojs.org">#</a><a href="https://www.typescriptlang.org/">#</a> <T i18nKey="landing_0">
#
<a href="https://en.wikipedia.org/wiki/Social_network_aggregation">
#
</a>
<a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
<br></br>
<code>#</code>
<br></br>
<b>#</b>
<br></br>
<a href={repoUrl}>#</a>
<br></br>
<a href="https://www.rust-lang.org">#</a>
<a href="https://actix.rs/">#</a>
<a href="https://infernojs.org">#</a>
<a href="https://www.typescriptlang.org/">#</a>
</T> </T>
</p> </p>
</div> </div>
</div> </div>
) );
} }
posts() { posts() {
return ( return (
<div> <div>
{this.state.loading ? {this.state.loading ? (
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : <h5>
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
</h5>
) : (
<div> <div>
{this.selects()} {this.selects()}
<PostListings posts={this.state.posts} showCommunity /> <PostListings posts={this.state.posts} showCommunity />
{this.paginator()} {this.paginator()}
</div> </div>
} )}
</div> </div>
) );
} }
selects() { selects() {
return ( return (
<div className="mb-3"> <div className="mb-3">
<div class="btn-group btn-group-toggle"> <div class="btn-group btn-group-toggle">
<label className={`btn btn-sm btn-secondary <label
className={`btn btn-sm btn-secondary
${this.state.type_ == ListingType.Subscribed && 'active'} ${this.state.type_ == ListingType.Subscribed && 'active'}
${UserService.Instance.user == undefined ? 'disabled' : 'pointer'} ${UserService.Instance.user == undefined ? 'disabled' : 'pointer'}
`}> `}
<input type="radio" >
<input
type="radio"
value={ListingType.Subscribed} value={ListingType.Subscribed}
checked={this.state.type_ == ListingType.Subscribed} checked={this.state.type_ == ListingType.Subscribed}
onChange={linkEvent(this, this.handleTypeChange)} onChange={linkEvent(this, this.handleTypeChange)}
@ -309,8 +437,12 @@ export class Main extends Component<any, MainState> {
/> />
{i18n.t('subscribed')} {i18n.t('subscribed')}
</label> </label>
<label className={`pointer btn btn-sm btn-secondary ${this.state.type_ == ListingType.All && 'active'}`}> <label
<input type="radio" className={`pointer btn btn-sm btn-secondary ${this.state.type_ ==
ListingType.All && 'active'}`}
>
<input
type="radio"
value={ListingType.All} value={ListingType.All}
checked={this.state.type_ == ListingType.All} checked={this.state.type_ == ListingType.All}
onChange={linkEvent(this, this.handleTypeChange)} onChange={linkEvent(this, this.handleTypeChange)}
@ -318,34 +450,69 @@ export class Main extends Component<any, MainState> {
{i18n.t('all')} {i18n.t('all')}
</label> </label>
</div> </div>
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="ml-2 custom-select custom-select-sm w-auto"> <select
<option disabled><T i18nKey="sort_type">#</T></option> value={this.state.sort}
<option value={SortType.Hot}><T i18nKey="hot">#</T></option> onChange={linkEvent(this, this.handleSortChange)}
<option value={SortType.New}><T i18nKey="new">#</T></option> class="ml-2 custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="sort_type">#</T>
</option>
<option value={SortType.Hot}>
<T i18nKey="hot">#</T>
</option>
<option value={SortType.New}>
<T i18nKey="new">#</T>
</option>
<option disabled></option> <option disabled></option>
<option value={SortType.TopDay}><T i18nKey="top_day">#</T></option> <option value={SortType.TopDay}>
<option value={SortType.TopWeek}><T i18nKey="week">#</T></option> <T i18nKey="top_day">#</T>
<option value={SortType.TopMonth}><T i18nKey="month">#</T></option> </option>
<option value={SortType.TopYear}><T i18nKey="year">#</T></option> <option value={SortType.TopWeek}>
<option value={SortType.TopAll}><T i18nKey="all">#</T></option> <T i18nKey="week">#</T>
</option>
<option value={SortType.TopMonth}>
<T i18nKey="month">#</T>
</option>
<option value={SortType.TopYear}>
<T i18nKey="year">#</T>
</option>
<option value={SortType.TopAll}>
<T i18nKey="all">#</T>
</option>
</select> </select>
</div> </div>
) );
} }
paginator() { paginator() {
return ( return (
<div class="my-2"> <div class="my-2">
{this.state.page > 1 && {this.state.page > 1 && (
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button> <button
} class="btn btn-sm btn-secondary mr-1"
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button> onClick={linkEvent(this, this.prevPage)}
>
<T i18nKey="prev">#</T>
</button>
)}
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
<T i18nKey="next">#</T>
</button>
</div> </div>
); );
} }
get canAdmin(): boolean { get canAdmin(): boolean {
return UserService.Instance.user && this.state.site.admins.map(a => a.id).includes(UserService.Instance.user.id); return (
UserService.Instance.user &&
this.state.site.admins
.map(a => a.id)
.includes(UserService.Instance.user.id)
);
} }
handleEditClick(i: Main) { handleEditClick(i: Main) {
@ -406,8 +573,8 @@ export class Main extends Component<any, MainState> {
page: this.state.page, page: this.state.page,
limit: fetchLimit, limit: fetchLimit,
sort: SortType[this.state.sort], sort: SortType[this.state.sort],
type_: ListingType[this.state.type_] type_: ListingType[this.state.type_],
} };
WebSocketService.Instance.getPosts(getPostsForm); WebSocketService.Instance.getPosts(getPostsForm);
} }
@ -430,7 +597,7 @@ export class Main extends Component<any, MainState> {
// This means it hasn't been set up yet // This means it hasn't been set up yet
if (!res.site) { if (!res.site) {
this.context.router.history.push("/setup"); this.context.router.history.push('/setup');
} }
this.state.site.admins = res.admins; this.state.site.admins = res.admins;
this.state.site.site = res.site; this.state.site.site = res.site;
@ -438,7 +605,6 @@ export class Main extends Component<any, MainState> {
this.state.site.online = res.online; this.state.site.online = res.online;
this.setState(this.state); this.setState(this.state);
document.title = `${WebSocketService.Instance.site.name}`; document.title = `${WebSocketService.Instance.site.name}`;
} else if (op == UserOperation.EditSite) { } else if (op == UserOperation.EditSite) {
let res: SiteResponse = msg; let res: SiteResponse = msg;
this.state.site.site = res.site; this.state.site.site = res.site;
@ -460,4 +626,3 @@ export class Main extends Component<any, MainState> {
} }
} }
} }

View file

@ -1,8 +1,21 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Subscription } from "rxjs"; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, GetModlogForm, GetModlogResponse, ModRemovePost, ModLockPost, ModStickyPost, ModRemoveComment, ModRemoveCommunity, ModBanFromCommunity, ModBan, ModAddCommunity, ModAdd } from '../interfaces'; import {
UserOperation,
GetModlogForm,
GetModlogResponse,
ModRemovePost,
ModLockPost,
ModStickyPost,
ModRemoveComment,
ModRemoveCommunity,
ModBanFromCommunity,
ModBan,
ModAddCommunity,
ModAdd,
} from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { msgOp, addTypeInfo, fetchLimit } from '../utils'; import { msgOp, addTypeInfo, fetchLimit } from '../utils';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
@ -10,9 +23,18 @@ import * as moment from 'moment';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
interface ModlogState { interface ModlogState {
combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModStickyPost | ModRemoveCommunity | ModAdd | ModBan}>, combined: Array<{
communityId?: number, type_: string;
communityName?: string, data:
| ModRemovePost
| ModLockPost
| ModStickyPost
| ModRemoveCommunity
| ModAdd
| ModBan;
}>;
communityId?: number;
communityName?: string;
page: number; page: number;
loading: boolean; loading: boolean;
} }
@ -23,18 +45,27 @@ export class Modlog extends Component<any, ModlogState> {
combined: [], combined: [],
page: 1, page: 1,
loading: true, loading: true,
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState; this.state = this.emptyState;
this.state.communityId = this.props.match.params.community_id ? Number(this.props.match.params.community_id) : undefined; this.state.communityId = this.props.match.params.community_id
? Number(this.props.match.params.community_id)
: undefined;
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), msg => this.parseMessage(msg),
(err) => console.error(err), err => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
@ -50,15 +81,27 @@ export class Modlog extends Component<any, ModlogState> {
} }
setCombined(res: GetModlogResponse) { setCombined(res: GetModlogResponse) {
let removed_posts = addTypeInfo(res.removed_posts, "removed_posts"); let removed_posts = addTypeInfo(res.removed_posts, 'removed_posts');
let locked_posts = addTypeInfo(res.locked_posts, "locked_posts"); let locked_posts = addTypeInfo(res.locked_posts, 'locked_posts');
let stickied_posts = addTypeInfo(res.stickied_posts, "stickied_posts"); let stickied_posts = addTypeInfo(res.stickied_posts, 'stickied_posts');
let removed_comments = addTypeInfo(res.removed_comments, "removed_comments"); let removed_comments = addTypeInfo(
let removed_communities = addTypeInfo(res.removed_communities, "removed_communities"); res.removed_comments,
let banned_from_community = addTypeInfo(res.banned_from_community, "banned_from_community"); 'removed_comments'
let added_to_community = addTypeInfo(res.added_to_community, "added_to_community"); );
let added = addTypeInfo(res.added, "added"); let removed_communities = addTypeInfo(
let banned = addTypeInfo(res.banned, "banned"); res.removed_communities,
'removed_communities'
);
let banned_from_community = addTypeInfo(
res.banned_from_community,
'banned_from_community'
);
let added_to_community = addTypeInfo(
res.added_to_community,
'added_to_community'
);
let added = addTypeInfo(res.added, 'added');
let banned = addTypeInfo(res.banned, 'banned');
this.state.combined = []; this.state.combined = [];
this.state.combined.push(...removed_posts); this.state.combined.push(...removed_posts);
@ -72,11 +115,14 @@ export class Modlog extends Component<any, ModlogState> {
this.state.combined.push(...banned); this.state.combined.push(...banned);
if (this.state.communityId && this.state.combined.length > 0) { if (this.state.communityId && this.state.combined.length > 0) {
this.state.communityName = (this.state.combined[0].data as ModRemovePost).community_name; this.state.communityName = (this.state.combined[0]
.data as ModRemovePost).community_name;
} }
// Sort them by time // Sort them by time
this.state.combined.sort((a, b) => b.data.when_.localeCompare(a.data.when_)); this.state.combined.sort((a, b) =>
b.data.when_.localeCompare(a.data.when_)
);
this.setState(this.state); this.setState(this.state);
} }
@ -84,97 +130,242 @@ export class Modlog extends Component<any, ModlogState> {
combined() { combined() {
return ( return (
<tbody> <tbody>
{this.state.combined.map(i => {this.state.combined.map(i => (
<tr> <tr>
<td><MomentTime data={i.data} /></td>
<td><Link to={`/u/${i.data.mod_user_name}`}>{i.data.mod_user_name}</Link></td>
<td> <td>
{i.type_ == 'removed_posts' && <MomentTime data={i.data} />
</td>
<td>
<Link to={`/u/${i.data.mod_user_name}`}>
{i.data.mod_user_name}
</Link>
</td>
<td>
{i.type_ == 'removed_posts' && (
<> <>
{(i.data as ModRemovePost).removed ? 'Removed' : 'Restored'} {(i.data as ModRemovePost).removed ? 'Removed' : 'Restored'}
<span> Post <Link to={`/post/${(i.data as ModRemovePost).post_id}`}>{(i.data as ModRemovePost).post_name}</Link></span> <span>
<div>{(i.data as ModRemovePost).reason && ` reason: ${(i.data as ModRemovePost).reason}`}</div> {' '}
Post{' '}
<Link to={`/post/${(i.data as ModRemovePost).post_id}`}>
{(i.data as ModRemovePost).post_name}
</Link>
</span>
<div>
{(i.data as ModRemovePost).reason &&
` reason: ${(i.data as ModRemovePost).reason}`}
</div>
</> </>
} )}
{i.type_ == 'locked_posts' && {i.type_ == 'locked_posts' && (
<> <>
{(i.data as ModLockPost).locked ? 'Locked' : 'Unlocked'} {(i.data as ModLockPost).locked ? 'Locked' : 'Unlocked'}
<span> Post <Link to={`/post/${(i.data as ModLockPost).post_id}`}>{(i.data as ModLockPost).post_name}</Link></span> <span>
{' '}
Post{' '}
<Link to={`/post/${(i.data as ModLockPost).post_id}`}>
{(i.data as ModLockPost).post_name}
</Link>
</span>
</> </>
} )}
{i.type_ == 'stickied_posts' && {i.type_ == 'stickied_posts' && (
<> <>
{(i.data as ModStickyPost).stickied? 'Stickied' : 'Unstickied'} {(i.data as ModStickyPost).stickied
<span> Post <Link to={`/post/${(i.data as ModStickyPost).post_id}`}>{(i.data as ModStickyPost).post_name}</Link></span> ? 'Stickied'
: 'Unstickied'}
<span>
{' '}
Post{' '}
<Link to={`/post/${(i.data as ModStickyPost).post_id}`}>
{(i.data as ModStickyPost).post_name}
</Link>
</span>
</> </>
} )}
{i.type_ == 'removed_comments' && {i.type_ == 'removed_comments' && (
<> <>
{(i.data as ModRemoveComment).removed? 'Removed' : 'Restored'} {(i.data as ModRemoveComment).removed
<span> Comment <Link to={`/post/${(i.data as ModRemoveComment).post_id}/comment/${(i.data as ModRemoveComment).comment_id}`}>{(i.data as ModRemoveComment).comment_content}</Link></span> ? 'Removed'
<span> by <Link to={`/u/${(i.data as ModRemoveComment).comment_user_name}`}>{(i.data as ModRemoveComment).comment_user_name}</Link></span> : 'Restored'}
<div>{(i.data as ModRemoveComment).reason && ` reason: ${(i.data as ModRemoveComment).reason}`}</div> <span>
{' '}
Comment{' '}
<Link
to={`/post/${
(i.data as ModRemoveComment).post_id
}/comment/${(i.data as ModRemoveComment).comment_id}`}
>
{(i.data as ModRemoveComment).comment_content}
</Link>
</span>
<span>
{' '}
by{' '}
<Link
to={`/u/${
(i.data as ModRemoveComment).comment_user_name
}`}
>
{(i.data as ModRemoveComment).comment_user_name}
</Link>
</span>
<div>
{(i.data as ModRemoveComment).reason &&
` reason: ${(i.data as ModRemoveComment).reason}`}
</div>
</> </>
} )}
{i.type_ == 'removed_communities' && {i.type_ == 'removed_communities' && (
<> <>
{(i.data as ModRemoveCommunity).removed ? 'Removed' : 'Restored'} {(i.data as ModRemoveCommunity).removed
<span> Community <Link to={`/c/${(i.data as ModRemoveCommunity).community_name}`}>{(i.data as ModRemoveCommunity).community_name}</Link></span> ? 'Removed'
<div>{(i.data as ModRemoveCommunity).reason && ` reason: ${(i.data as ModRemoveCommunity).reason}`}</div> : 'Restored'}
<div>{(i.data as ModRemoveCommunity).expires && ` expires: ${moment.utc((i.data as ModRemoveCommunity).expires).fromNow()}`}</div> <span>
{' '}
Community{' '}
<Link
to={`/c/${(i.data as ModRemoveCommunity).community_name}`}
>
{(i.data as ModRemoveCommunity).community_name}
</Link>
</span>
<div>
{(i.data as ModRemoveCommunity).reason &&
` reason: ${(i.data as ModRemoveCommunity).reason}`}
</div>
<div>
{(i.data as ModRemoveCommunity).expires &&
` expires: ${moment
.utc((i.data as ModRemoveCommunity).expires)
.fromNow()}`}
</div>
</> </>
} )}
{i.type_ == 'banned_from_community' && {i.type_ == 'banned_from_community' && (
<> <>
<span>{(i.data as ModBanFromCommunity).banned ? 'Banned ' : 'Unbanned '} </span> <span>
<span><Link to={`/u/${(i.data as ModBanFromCommunity).other_user_name}`}>{(i.data as ModBanFromCommunity).other_user_name}</Link></span> {(i.data as ModBanFromCommunity).banned
? 'Banned '
: 'Unbanned '}{' '}
</span>
<span>
<Link
to={`/u/${
(i.data as ModBanFromCommunity).other_user_name
}`}
>
{(i.data as ModBanFromCommunity).other_user_name}
</Link>
</span>
<span> from the community </span> <span> from the community </span>
<span><Link to={`/c/${(i.data as ModBanFromCommunity).community_name}`}>{(i.data as ModBanFromCommunity).community_name}</Link></span> <span>
<div>{(i.data as ModBanFromCommunity).reason && ` reason: ${(i.data as ModBanFromCommunity).reason}`}</div> <Link
<div>{(i.data as ModBanFromCommunity).expires && ` expires: ${moment.utc((i.data as ModBanFromCommunity).expires).fromNow()}`}</div> to={`/c/${
(i.data as ModBanFromCommunity).community_name
}`}
>
{(i.data as ModBanFromCommunity).community_name}
</Link>
</span>
<div>
{(i.data as ModBanFromCommunity).reason &&
` reason: ${(i.data as ModBanFromCommunity).reason}`}
</div>
<div>
{(i.data as ModBanFromCommunity).expires &&
` expires: ${moment
.utc((i.data as ModBanFromCommunity).expires)
.fromNow()}`}
</div>
</> </>
} )}
{i.type_ == 'added_to_community' && {i.type_ == 'added_to_community' && (
<> <>
<span>{(i.data as ModAddCommunity).removed ? 'Removed ' : 'Appointed '} </span> <span>
<span><Link to={`/u/${(i.data as ModAddCommunity).other_user_name}`}>{(i.data as ModAddCommunity).other_user_name}</Link></span> {(i.data as ModAddCommunity).removed
? 'Removed '
: 'Appointed '}{' '}
</span>
<span>
<Link
to={`/u/${(i.data as ModAddCommunity).other_user_name}`}
>
{(i.data as ModAddCommunity).other_user_name}
</Link>
</span>
<span> as a mod to the community </span> <span> as a mod to the community </span>
<span><Link to={`/c/${(i.data as ModAddCommunity).community_name}`}>{(i.data as ModAddCommunity).community_name}</Link></span> <span>
<Link
to={`/c/${(i.data as ModAddCommunity).community_name}`}
>
{(i.data as ModAddCommunity).community_name}
</Link>
</span>
</> </>
} )}
{i.type_ == 'banned' && {i.type_ == 'banned' && (
<> <>
<span>{(i.data as ModBan).banned ? 'Banned ' : 'Unbanned '} </span> <span>
<span><Link to={`/u/${(i.data as ModBan).other_user_name}`}>{(i.data as ModBan).other_user_name}</Link></span> {(i.data as ModBan).banned ? 'Banned ' : 'Unbanned '}{' '}
<div>{(i.data as ModBan).reason && ` reason: ${(i.data as ModBan).reason}`}</div> </span>
<div>{(i.data as ModBan).expires && ` expires: ${moment.utc((i.data as ModBan).expires).fromNow()}`}</div> <span>
<Link to={`/u/${(i.data as ModBan).other_user_name}`}>
{(i.data as ModBan).other_user_name}
</Link>
</span>
<div>
{(i.data as ModBan).reason &&
` reason: ${(i.data as ModBan).reason}`}
</div>
<div>
{(i.data as ModBan).expires &&
` expires: ${moment
.utc((i.data as ModBan).expires)
.fromNow()}`}
</div>
</> </>
} )}
{i.type_ == 'added' && {i.type_ == 'added' && (
<> <>
<span>{(i.data as ModAdd).removed ? 'Removed ' : 'Appointed '} </span> <span>
<span><Link to={`/u/${(i.data as ModAdd).other_user_name}`}>{(i.data as ModAdd).other_user_name}</Link></span> {(i.data as ModAdd).removed ? 'Removed ' : 'Appointed '}{' '}
</span>
<span>
<Link to={`/u/${(i.data as ModAdd).other_user_name}`}>
{(i.data as ModAdd).other_user_name}
</Link>
</span>
<span> as an admin </span> <span> as an admin </span>
</> </>
} )}
</td> </td>
</tr> </tr>
) ))}
}
</tbody> </tbody>
); );
} }
render() { render() {
return ( return (
<div class="container"> <div class="container">
{this.state.loading ? {this.state.loading ? (
<h5 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : <h5 class="">
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
</h5>
) : (
<div> <div>
<h5> <h5>
{this.state.communityName && <Link className="text-white" to={`/c/${this.state.communityName}`}>/c/{this.state.communityName} </Link>} {this.state.communityName && (
<Link
className="text-white"
to={`/c/${this.state.communityName}`}
>
/c/{this.state.communityName}{' '}
</Link>
)}
<span>Modlog</span> <span>Modlog</span>
</h5> </h5>
<div class="table-responsive"> <div class="table-responsive">
@ -191,7 +382,7 @@ export class Modlog extends Component<any, ModlogState> {
{this.paginator()} {this.paginator()}
</div> </div>
</div> </div>
} )}
</div> </div>
); );
} }
@ -199,10 +390,20 @@ export class Modlog extends Component<any, ModlogState> {
paginator() { paginator() {
return ( return (
<div class="mt-2"> <div class="mt-2">
{this.state.page > 1 && {this.state.page > 1 && (
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}>Prev</button> <button
} class="btn btn-sm btn-secondary mr-1"
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}>Next</button> onClick={linkEvent(this, this.prevPage)}
>
Prev
</button>
)}
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
Next
</button>
</div> </div>
); );
} }

View file

@ -8,11 +8,10 @@ interface MomentTimeProps {
published?: string; published?: string;
when_?: string; when_?: string;
updated?: string; updated?: string;
} };
} }
export class MomentTime extends Component<MomentTimeProps, any> { export class MomentTime extends Component<MomentTimeProps, any> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -24,13 +23,13 @@ export class MomentTime extends Component<MomentTimeProps, any> {
render() { render() {
if (this.props.data.updated) { if (this.props.data.updated) {
return ( return (
<span title={this.props.data.updated} className="font-italics">{i18n.t('modified')} {moment.utc(this.props.data.updated).fromNow()}</span> <span title={this.props.data.updated} className="font-italics">
) {i18n.t('modified')} {moment.utc(this.props.data.updated).fromNow()}
</span>
);
} else { } else {
let str = this.props.data.published || this.props.data.when_; let str = this.props.data.published || this.props.data.when_;
return ( return <span title={str}>{moment.utc(str).fromNow()}</span>;
<span title={str}>{moment.utc(str).fromNow()}</span>
)
} }
} }
} }

View file

@ -1,9 +1,16 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Subscription } from "rxjs"; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { UserOperation, GetRepliesForm, GetRepliesResponse, SortType, GetSiteResponse, Comment} from '../interfaces'; import {
UserOperation,
GetRepliesForm,
GetRepliesResponse,
SortType,
GetSiteResponse,
Comment,
} from '../interfaces';
import { msgOp } from '../utils'; import { msgOp } from '../utils';
import { version } from '../version'; import { version } from '../version';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
@ -13,8 +20,8 @@ interface NavbarState {
isLoggedIn: boolean; isLoggedIn: boolean;
expanded: boolean; expanded: boolean;
expandUserDropdown: boolean; expandUserDropdown: boolean;
replies: Array<Comment>, replies: Array<Comment>;
fetchCount: number, fetchCount: number;
unreadCount: number; unreadCount: number;
siteName: string; siteName: string;
} }
@ -23,14 +30,14 @@ export class Navbar extends Component<any, NavbarState> {
private wsSub: Subscription; private wsSub: Subscription;
private userSub: Subscription; private userSub: Subscription;
emptyState: NavbarState = { emptyState: NavbarState = {
isLoggedIn: (UserService.Instance.user !== undefined), isLoggedIn: UserService.Instance.user !== undefined,
unreadCount: 0, unreadCount: 0,
fetchCount: 0, fetchCount: 0,
replies: [], replies: [],
expanded: false, expanded: false,
expandUserDropdown: false, expandUserDropdown: false,
siteName: undefined siteName: undefined,
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -48,10 +55,17 @@ export class Navbar extends Component<any, NavbarState> {
}); });
this.wsSub = WebSocketService.Instance.subject this.wsSub = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), msg => this.parseMessage(msg),
(err) => console.error(err), err => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
@ -63,9 +77,7 @@ export class Navbar extends Component<any, NavbarState> {
} }
render() { render() {
return ( return <div>{this.navbar()}</div>;
<div>{this.navbar()}</div>
)
} }
componentWillUnmount() { componentWillUnmount() {
@ -80,48 +92,98 @@ export class Navbar extends Component<any, NavbarState> {
<Link title={version} class="navbar-brand" to="/"> <Link title={version} class="navbar-brand" to="/">
{this.state.siteName} {this.state.siteName}
</Link> </Link>
<button class="navbar-toggler" type="button" onClick={linkEvent(this, this.expandNavbar)}> <button
class="navbar-toggler"
type="button"
onClick={linkEvent(this, this.expandNavbar)}
>
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div className={`${!this.state.expanded && 'collapse'} navbar-collapse`}> <div
className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
>
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/communities"><T i18nKey="communities">#</T></Link> <Link class="nav-link" to="/communities">
<T i18nKey="communities">#</T>
</Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/search"><T i18nKey="search">#</T></Link> <Link class="nav-link" to="/search">
<T i18nKey="search">#</T>
</Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to={{pathname: '/create_post', state: { prevPath: this.currentLocation }}}><T i18nKey="create_post">#</T></Link> <Link
class="nav-link"
to={{
pathname: '/create_post',
state: { prevPath: this.currentLocation },
}}
>
<T i18nKey="create_post">#</T>
</Link>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/create_community"><T i18nKey="create_community">#</T></Link> <Link class="nav-link" to="/create_community">
<T i18nKey="create_community">#</T>
</Link>
</li> </li>
</ul> </ul>
<ul class="navbar-nav ml-auto mr-2"> <ul class="navbar-nav ml-auto mr-2">
{this.state.isLoggedIn ? {this.state.isLoggedIn ? (
<> <>
{ {
<li className="nav-item"> <li className="nav-item">
<Link class="nav-link" to="/inbox"> <Link class="nav-link" to="/inbox">
<svg class="icon"><use xlinkHref="#icon-mail"></use></svg> <svg class="icon">
{this.state.unreadCount> 0 && <span class="ml-1 badge badge-light">{this.state.unreadCount}</span>} <use xlinkHref="#icon-mail"></use>
</svg>
{this.state.unreadCount > 0 && (
<span class="ml-1 badge badge-light">
{this.state.unreadCount}
</span>
)}
</Link> </Link>
</li> </li>
} }
<li className={`nav-item dropdown ${this.state.expandUserDropdown && 'show'}`}> <li
<a class="pointer nav-link dropdown-toggle" onClick={linkEvent(this, this.expandUserDropdown)} role="button"> className={`nav-item dropdown ${this.state
.expandUserDropdown && 'show'}`}
>
<a
class="pointer nav-link dropdown-toggle"
onClick={linkEvent(this, this.expandUserDropdown)}
role="button"
>
{UserService.Instance.user.username} {UserService.Instance.user.username}
</a> </a>
<div className={`dropdown-menu dropdown-menu-right ${this.state.expandUserDropdown && 'show'}`}> <div
<a role="button" class="dropdown-item pointer" onClick={linkEvent(this, this.handleOverviewClick)}><T i18nKey="overview">#</T></a> className={`dropdown-menu dropdown-menu-right ${this.state
<a role="button" class="dropdown-item pointer" onClick={ linkEvent(this, this.handleLogoutClick) }><T i18nKey="logout">#</T></a> .expandUserDropdown && 'show'}`}
>
<a
role="button"
class="dropdown-item pointer"
onClick={linkEvent(this, this.handleOverviewClick)}
>
<T i18nKey="overview">#</T>
</a>
<a
role="button"
class="dropdown-item pointer"
onClick={linkEvent(this, this.handleLogoutClick)}
>
<T i18nKey="logout">#</T>
</a>
</div> </div>
</li> </li>
</> </>
: ) : (
<Link class="nav-link" to="/login"><T i18nKey="login_sign_up">#</T></Link> <Link class="nav-link" to="/login">
} <T i18nKey="login_sign_up">#</T>
</Link>
)}
</ul> </ul>
</div> </div>
</nav> </nav>
@ -154,7 +216,7 @@ export class Navbar extends Component<any, NavbarState> {
parseMessage(msg: any) { parseMessage(msg: any) {
let op: UserOperation = msgOp(msg); let op: UserOperation = msgOp(msg);
if (msg.error) { if (msg.error) {
if (msg.error == "not_logged_in") { if (msg.error == 'not_logged_in') {
UserService.Instance.logout(); UserService.Instance.logout();
location.reload(); location.reload();
} }
@ -162,8 +224,11 @@ export class Navbar extends Component<any, NavbarState> {
} else if (op == UserOperation.GetReplies) { } else if (op == UserOperation.GetReplies) {
let res: GetRepliesResponse = msg; let res: GetRepliesResponse = msg;
let unreadReplies = res.replies.filter(r => !r.read); let unreadReplies = res.replies.filter(r => !r.read);
if (unreadReplies.length > 0 && this.state.fetchCount > 1 && if (
(JSON.stringify(this.state.replies) !== JSON.stringify(unreadReplies))) { unreadReplies.length > 0 &&
this.state.fetchCount > 1 &&
JSON.stringify(this.state.replies) !== JSON.stringify(unreadReplies)
) {
this.notify(unreadReplies); this.notify(unreadReplies);
} }
@ -205,7 +270,10 @@ export class Navbar extends Component<any, NavbarState> {
} }
sendRepliesCount(res: GetRepliesResponse) { sendRepliesCount(res: GetRepliesResponse) {
UserService.Instance.sub.next({user: UserService.Instance.user, unreadCount: res.replies.filter(r => !r.read).length}); UserService.Instance.sub.next({
user: UserService.Instance.user,
unreadCount: res.replies.filter(r => !r.read).length,
});
} }
requestNotificationPermission() { requestNotificationPermission() {
@ -224,18 +292,21 @@ export class Navbar extends Component<any, NavbarState> {
notify(replies: Array<Comment>) { notify(replies: Array<Comment>) {
let recentReply = replies[0]; let recentReply = replies[0];
if (Notification.permission !== 'granted') if (Notification.permission !== 'granted') Notification.requestPermission();
Notification.requestPermission();
else { else {
var notification = new Notification(`${replies.length} ${i18n.t('unread_messages')}`, { var notification = new Notification(
`${replies.length} ${i18n.t('unread_messages')}`,
{
icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`, icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
body: `${recentReply.creator_name}: ${recentReply.content}` body: `${recentReply.creator_name}: ${recentReply.content}`,
}); }
);
notification.onclick = () => { notification.onclick = () => {
this.context.router.history.push(`/post/${recentReply.post_id}/comment/${recentReply.id}`); this.context.router.history.push(
`/post/${recentReply.post_id}/comment/${recentReply.id}`
);
}; };
} }
} }
} }

View file

@ -1,10 +1,31 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
import { Subscription } from "rxjs"; 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, markdownHelpUrl, mdToHtml } from '../utils'; import {
msgOp,
getPageTitle,
debounce,
validURL,
capitalizeFirstLetter,
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';
@ -29,7 +50,6 @@ interface PostFormState {
} }
export class PostForm extends Component<PostFormProps, PostFormState> { export class PostForm extends Component<PostFormProps, PostFormState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: PostFormState = { private emptyState: PostFormState = {
postForm: { postForm: {
@ -37,7 +57,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
nsfw: false, nsfw: false,
auth: null, auth: null,
community_id: null, community_id: null,
creator_id: (UserService.Instance.user) ? UserService.Instance.user.id : null, creator_id: UserService.Instance.user
? UserService.Instance.user.id
: null,
}, },
communities: [], communities: [],
loading: false, loading: false,
@ -46,7 +68,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
suggestedTitle: undefined, suggestedTitle: undefined,
suggestedPosts: [], suggestedPosts: [],
crossPosts: [], crossPosts: [],
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -62,8 +84,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
creator_id: this.props.post.creator_id, creator_id: this.props.post.creator_id,
url: this.props.post.url, url: this.props.post.url,
nsfw: this.props.post.nsfw, nsfw: this.props.post.nsfw,
auth: null auth: null,
} };
} }
if (this.props.params) { if (this.props.params) {
@ -77,17 +99,24 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} }
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), msg => this.parseMessage(msg),
(err) => console.error(err), err => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
let listCommunitiesForm: ListCommunitiesForm = { let listCommunitiesForm: ListCommunitiesForm = {
sort: SortType[SortType.TopAll], sort: SortType[SortType.TopAll],
limit: 9999, limit: 9999,
} };
WebSocketService.Instance.listCommunities(listCommunitiesForm); WebSocketService.Instance.listCommunities(listCommunitiesForm);
} }
@ -105,79 +134,177 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<div> <div>
<form onSubmit={linkEvent(this, this.handlePostSubmit)}> <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="url">#</T></label> <label class="col-sm-2 col-form-label">
<T i18nKey="url">#</T>
</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="url" class="form-control" value={this.state.postForm.url} onInput={linkEvent(this, this.handlePostUrlChange)} /> <input
{this.state.suggestedTitle && type="url"
<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> class="form-control"
} value={this.state.postForm.url}
onInput={linkEvent(this, this.handlePostUrlChange)}
/>
{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>
)}
<form> <form>
<label htmlFor="file-upload" className={`${UserService.Instance.user && 'pointer'} d-inline-block mr-2 float-right text-muted small font-weight-bold`}><T i18nKey="upload_image">#</T></label> <label
<input id="file-upload" type="file" accept="image/*,video/*" name="file" class="d-none" disabled={!UserService.Instance.user} onChange={linkEvent(this, this.handleImageUpload)} /> htmlFor="file-upload"
className={`${UserService.Instance.user &&
'pointer'} d-inline-block mr-2 float-right text-muted small font-weight-bold`}
>
<T i18nKey="upload_image">#</T>
</label>
<input
id="file-upload"
type="file"
accept="image/*,video/*"
name="file"
class="d-none"
disabled={!UserService.Instance.user}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form> </form>
{this.state.imageLoading && {this.state.imageLoading && (
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> <svg class="icon icon-spinner spin">
} <use xlinkHref="#icon-spinner"></use>
{this.state.crossPosts.length > 0 && </svg>
)}
{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>
<PostListings showCommunity posts={this.state.crossPosts} /> <PostListings showCommunity posts={this.state.crossPosts} />
</> </>
} )}
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="title">#</T></label> <label class="col-sm-2 col-form-label">
<T i18nKey="title">#</T>
</label>
<div class="col-sm-10"> <div class="col-sm-10">
<textarea value={this.state.postForm.name} onInput={linkEvent(this, this.handlePostNameChange)} class="form-control" required rows={2} minLength={3} maxLength={100} /> <textarea
{this.state.suggestedPosts.length > 0 && value={this.state.postForm.name}
onInput={linkEvent(this, this.handlePostNameChange)}
class="form-control"
required
rows={2}
minLength={3}
maxLength={100}
/>
{this.state.suggestedPosts.length > 0 && (
<> <>
<div class="my-1 text-muted small font-weight-bold"><T i18nKey="related_posts">#</T></div> <div class="my-1 text-muted small font-weight-bold">
<T i18nKey="related_posts">#</T>
</div>
<PostListings posts={this.state.suggestedPosts} /> <PostListings posts={this.state.suggestedPosts} />
</> </>
}
</div>
</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)} 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 &&
<div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="community">#</T></label>
<div class="col-sm-10">
<select class="form-control" value={this.state.postForm.community_id} onInput={linkEvent(this, this.handlePostCommunityChange)}>
{this.state.communities.map(community =>
<option value={community.id}>{community.name}</option>
)} )}
</div>
</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)}
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 && (
<div class="form-group row">
<label class="col-sm-2 col-form-label">
<T i18nKey="community">#</T>
</label>
<div class="col-sm-10">
<select
class="form-control"
value={this.state.postForm.community_id}
onInput={linkEvent(this, this.handlePostCommunityChange)}
>
{this.state.communities.map(community => (
<option value={community.id}>{community.name}</option>
))}
</select> </select>
</div> </div>
</div> </div>
} )}
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" checked={this.state.postForm.nsfw} onChange={linkEvent(this, this.handlePostNsfwChange)}/> <input
<label class="form-check-label"><T i18nKey="nsfw">#</T></label> class="form-check-input"
type="checkbox"
checked={this.state.postForm.nsfw}
onChange={linkEvent(this, this.handlePostNsfwChange)}
/>
<label class="form-check-label">
<T i18nKey="nsfw">#</T>
</label>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<button type="submit" class="btn btn-secondary mr-2"> <button type="submit" class="btn btn-secondary mr-2">
{this.state.loading ? {this.state.loading ? (
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : <svg class="icon icon-spinner spin">
this.props.post ? capitalizeFirstLetter(i18n.t('save')) : capitalizeFirstLetter(i18n.t('create'))}</button> <use xlinkHref="#icon-spinner"></use>
{this.props.post && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}><T i18nKey="cancel">#</T></button>} </svg>
) : this.props.post ? (
capitalizeFirstLetter(i18n.t('save'))
) : (
capitalizeFirstLetter(i18n.t('create'))
)}
</button>
{this.props.post && (
<button
type="button"
class="btn btn-secondary"
onClick={linkEvent(this, this.handleCancel)}
>
<T i18nKey="cancel">#</T>
</button>
)}
</div> </div>
</div> </div>
</form> </form>
@ -205,7 +332,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
handlePostUrlChange(i: PostForm, event: any) { handlePostUrlChange(i: PostForm, event: any) {
i.state.postForm.url = event.target.value; i.state.postForm.url = event.target.value;
if (validURL(i.state.postForm.url)) { if (validURL(i.state.postForm.url)) {
let form: SearchForm = { let form: SearchForm = {
q: i.state.postForm.url, q: i.state.postForm.url,
type_: SearchType[SearchType.Url], type_: SearchType[SearchType.Url],
@ -298,11 +424,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
i.state.imageLoading = false; i.state.imageLoading = false;
i.setState(i.state); i.setState(i.state);
}) })
.catch((error) => { .catch(error => {
i.state.imageLoading = false; i.state.imageLoading = false;
i.setState(i.state); i.setState(i.state);
alert(error); alert(error);
}) });
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -318,7 +444,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
if (this.props.post) { if (this.props.post) {
this.state.postForm.community_id = this.props.post.community_id; this.state.postForm.community_id = this.props.post.community_id;
} else if (this.props.params && this.props.params.community) { } else if (this.props.params && this.props.params.community) {
let foundCommunityId = res.communities.find(r => r.name == this.props.params.community).id; let foundCommunityId = res.communities.find(
r => r.name == this.props.params.community
).id;
this.state.postForm.community_id = foundCommunityId; this.state.postForm.community_id = foundCommunityId;
} else { } else {
this.state.postForm.community_id = res.communities[0].id; this.state.postForm.community_id = res.communities[0].id;
@ -343,7 +471,4 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.setState(this.state); this.setState(this.state);
} }
} }
} }

View file

@ -1,10 +1,31 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { Post, CreatePostLikeForm, PostForm as PostFormI, SavePostForm, CommunityUser, UserView, BanType, BanFromCommunityForm, BanUserForm, AddModToCommunityForm, AddAdminForm, TransferSiteForm, TransferCommunityForm } from '../interfaces'; import {
Post,
CreatePostLikeForm,
PostForm as PostFormI,
SavePostForm,
CommunityUser,
UserView,
BanType,
BanFromCommunityForm,
BanUserForm,
AddModToCommunityForm,
AddAdminForm,
TransferSiteForm,
TransferCommunityForm,
} from '../interfaces';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
import { PostForm } from './post-form'; import { PostForm } from './post-form';
import { mdToHtml, canMod, isMod, isImage, isVideo, getUnixTime } from '../utils'; import {
mdToHtml,
canMod,
isMod,
isImage,
isVideo,
getUnixTime,
} from '../utils';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
@ -32,7 +53,6 @@ interface PostListingProps {
} }
export class PostListing extends Component<PostListingProps, PostListingState> { export class PostListing extends Component<PostListingProps, PostListingState> {
private emptyState: PostListingState = { private emptyState: PostListingState = {
showEdit: false, showEdit: false,
showRemoveDialog: false, showRemoveDialog: false,
@ -45,7 +65,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
showConfirmTransferCommunity: false, showConfirmTransferCommunity: false,
imageExpanded: false, imageExpanded: false,
viewSource: false, viewSource: false,
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -60,242 +80,505 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
render() { render() {
return ( return (
<div class="row"> <div class="row">
{!this.state.showEdit {!this.state.showEdit ? (
? this.listing() this.listing()
: ) : (
<div class="col-12"> <div class="col-12">
<PostForm post={this.props.post} onEdit={this.handleEditPost} onCancel={this.handleEditCancel}/> <PostForm
post={this.props.post}
onEdit={this.handleEditPost}
onCancel={this.handleEditCancel}
/>
</div> </div>
} )}
</div> </div>
) );
} }
listing() { listing() {
let post = this.props.post; let post = this.props.post;
return ( return (
<div class="listing col-12"> <div class="listing col-12">
<div className={`vote-bar mr-2 float-left small text-center ${this.props.viewOnly && 'no-click'}`}> <div
<button className={`btn p-0 ${post.my_vote == 1 ? 'text-info' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostLike)}> className={`vote-bar mr-2 float-left small text-center ${this.props
<svg class="icon upvote"><use xlinkHref="#icon-arrow-up"></use></svg> .viewOnly && 'no-click'}`}
>
<button
className={`btn p-0 ${
post.my_vote == 1 ? 'text-info' : 'text-muted'
}`}
onClick={linkEvent(this, this.handlePostLike)}
>
<svg class="icon upvote">
<use xlinkHref="#icon-arrow-up"></use>
</svg>
</button> </button>
<div class={`font-weight-bold text-muted`}>{post.score}</div> <div class={`font-weight-bold text-muted`}>{post.score}</div>
<button className={`btn p-0 ${post.my_vote == -1 ? 'text-danger' : 'text-muted'}`} onClick={linkEvent(this, this.handlePostDisLike)}> <button
<svg class="icon downvote"><use xlinkHref="#icon-arrow-down"></use></svg> className={`btn p-0 ${
post.my_vote == -1 ? 'text-danger' : 'text-muted'
}`}
onClick={linkEvent(this, this.handlePostDisLike)}
>
<svg class="icon downvote">
<use xlinkHref="#icon-arrow-down"></use>
</svg>
</button> </button>
</div> </div>
{post.url && isImage(post.url) && {post.url && isImage(post.url) && (
<span title={i18n.t('expand_here')} class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)}><img class="mx-2 mt-1 float-left img-fluid thumbnail rounded" src={post.url} /></span> <span
} title={i18n.t('expand_here')}
{post.url && isVideo(post.url) && class="pointer"
<video playsinline muted loop controls class="mx-2 mt-1 float-left" height="100" width="150"> onClick={linkEvent(this, this.handleImageExpandClick)}
>
<img
class="mx-2 mt-1 float-left img-fluid thumbnail rounded"
src={post.url}
/>
</span>
)}
{post.url && isVideo(post.url) && (
<video
playsinline
muted
loop
controls
class="mx-2 mt-1 float-left"
height="100"
width="150"
>
<source src={post.url} type="video/mp4" /> <source src={post.url} type="video/mp4" />
</video> </video>
} )}
<div className="ml-4"> <div className="ml-4">
<div className="post-title"> <div className="post-title">
<h5 className="mb-0 d-inline"> <h5 className="mb-0 d-inline">
{post.url ? {post.url ? (
<a className="text-body" href={post.url} target="_blank" title={post.url}>{post.name}</a> : <a
<Link className="text-body" to={`/post/${post.id}`} title={i18n.t('comments')}>{post.name}</Link> className="text-body"
} href={post.url}
target="_blank"
title={post.url}
>
{post.name}
</a>
) : (
<Link
className="text-body"
to={`/post/${post.id}`}
title={i18n.t('comments')}
>
{post.name}
</Link>
)}
</h5> </h5>
{post.url && {post.url && (
<small> <small>
<a className="ml-2 text-muted font-italic" href={post.url} target="_blank" title={post.url}>{(new URL(post.url)).hostname}</a> <a
className="ml-2 text-muted font-italic"
href={post.url}
target="_blank"
title={post.url}
>
{new URL(post.url).hostname}
</a>
</small> </small>
} )}
{ post.url && isImage(post.url) && {post.url && isImage(post.url) && (
<> <>
{ !this.state.imageExpanded {!this.state.imageExpanded ? (
? <span class="text-monospace pointer ml-2 text-muted small" title={i18n.t('expand_here')} onClick={linkEvent(this, this.handleImageExpandClick)}>[+]</span> <span
: class="text-monospace pointer ml-2 text-muted small"
title={i18n.t('expand_here')}
onClick={linkEvent(this, this.handleImageExpandClick)}
>
[+]
</span>
) : (
<span> <span>
<span class="text-monospace pointer ml-2 text-muted small" onClick={linkEvent(this, this.handleImageExpandClick)}>[-]</span> <span
class="text-monospace pointer ml-2 text-muted small"
onClick={linkEvent(this, this.handleImageExpandClick)}
>
[-]
</span>
<div> <div>
<span class="pointer" onClick={linkEvent(this, this.handleImageExpandClick)}><img class="img-fluid" src={post.url} /></span> <span
class="pointer"
onClick={linkEvent(this, this.handleImageExpandClick)}
>
<img class="img-fluid" src={post.url} />
</span>
</div> </div>
</span> </span>
} )}
</> </>
} )}
{post.removed && {post.removed && (
<small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small> <small className="ml-2 text-muted font-italic">
} <T i18nKey="removed">#</T>
{post.deleted && </small>
<small className="ml-2 text-muted font-italic"><T i18nKey="deleted">#</T></small> )}
} {post.deleted && (
{post.locked && <small className="ml-2 text-muted font-italic">
<small className="ml-2 text-muted font-italic"><T i18nKey="locked">#</T></small> <T i18nKey="deleted">#</T>
} </small>
{post.stickied && )}
<small className="ml-2 text-muted font-italic"><T i18nKey="stickied">#</T></small> {post.locked && (
} <small className="ml-2 text-muted font-italic">
{post.nsfw && <T i18nKey="locked">#</T>
<small className="ml-2 text-muted font-italic"><T i18nKey="nsfw">#</T></small> </small>
} )}
{post.stickied && (
<small className="ml-2 text-muted font-italic">
<T i18nKey="stickied">#</T>
</small>
)}
{post.nsfw && (
<small className="ml-2 text-muted font-italic">
<T i18nKey="nsfw">#</T>
</small>
)}
</div> </div>
</div> </div>
<div className="details ml-4"> <div className="details ml-4">
<ul class="list-inline mb-0 text-muted small"> <ul class="list-inline mb-0 text-muted small">
<li className="list-inline-item"> <li className="list-inline-item">
<span>{i18n.t('by')} </span> <span>{i18n.t('by')} </span>
<Link className="text-info" to={`/u/${post.creator_name}`}>{post.creator_name}</Link> <Link className="text-info" to={`/u/${post.creator_name}`}>
{this.isMod && {post.creator_name}
<span className="mx-1 badge badge-light"><T i18nKey="mod">#</T></span> </Link>
} {this.isMod && (
{this.isAdmin && <span className="mx-1 badge badge-light">
<span className="mx-1 badge badge-light"><T i18nKey="admin">#</T></span> <T i18nKey="mod">#</T>
} </span>
{(post.banned_from_community || post.banned) && )}
<span className="mx-1 badge badge-danger"><T i18nKey="banned">#</T></span> {this.isAdmin && (
} <span className="mx-1 badge badge-light">
{this.props.showCommunity && <T i18nKey="admin">#</T>
</span>
)}
{(post.banned_from_community || post.banned) && (
<span className="mx-1 badge badge-danger">
<T i18nKey="banned">#</T>
</span>
)}
{this.props.showCommunity && (
<span> <span>
<span> {i18n.t('to')} </span> <span> {i18n.t('to')} </span>
<Link to={`/c/${post.community_name}`}>{post.community_name}</Link> <Link to={`/c/${post.community_name}`}>
{post.community_name}
</Link>
</span> </span>
} )}
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<span><MomentTime data={post} /></span> <span>
<MomentTime data={post} />
</span>
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<span>( <span>
<span className="text-info">+{post.upvotes}</span> (<span className="text-info">+{post.upvotes}</span>
<span> | </span> <span> | </span>
<span className="text-danger">-{post.downvotes}</span> <span className="text-danger">-{post.downvotes}</span>
<span>) </span> <span>) </span>
</span> </span>
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<Link className="text-muted" to={`/post/${post.id}`}><T i18nKey="number_of_comments" interpolation={{count: post.number_of_comments}}>#</T></Link> <Link className="text-muted" to={`/post/${post.id}`}>
<T
i18nKey="number_of_comments"
interpolation={{ count: post.number_of_comments }}
>
#
</T>
</Link>
</li> </li>
</ul> </ul>
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
{UserService.Instance.user && {UserService.Instance.user && (
<> <>
{this.props.showBody && {this.props.showBody && (
<> <>
<li className="list-inline-item mr-2"> <li className="list-inline-item mr-2">
<span class="pointer" onClick={linkEvent(this, this.handleSavePostClick)}>{post.saved ? i18n.t('unsave') : i18n.t('save')}</span> <span
class="pointer"
onClick={linkEvent(this, this.handleSavePostClick)}
>
{post.saved ? i18n.t('unsave') : i18n.t('save')}
</span>
</li> </li>
<li className="list-inline-item mr-2"> <li className="list-inline-item mr-2">
<Link className="text-muted" to={`/create_post${this.crossPostParams}`}><T i18nKey="cross_post">#</T></Link> <Link
className="text-muted"
to={`/create_post${this.crossPostParams}`}
>
<T i18nKey="cross_post">#</T>
</Link>
</li> </li>
</> </>
} )}
{this.myPost && {this.myPost && (
<> <>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span> <span
class="pointer"
onClick={linkEvent(this, this.handleEditClick)}
>
<T i18nKey="edit">#</T>
</span>
</li> </li>
<li className="list-inline-item mr-2"> <li className="list-inline-item mr-2">
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}> <span
class="pointer"
onClick={linkEvent(this, this.handleDeleteClick)}
>
{!post.deleted ? i18n.t('delete') : i18n.t('restore')} {!post.deleted ? i18n.t('delete') : i18n.t('restore')}
</span> </span>
</li> </li>
</> </>
} )}
{this.canModOnSelf && {this.canModOnSelf && (
<> <>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleModLock)}>{post.locked ? i18n.t('unlock') : i18n.t('lock')}</span> <span
class="pointer"
onClick={linkEvent(this, this.handleModLock)}
>
{post.locked ? i18n.t('unlock') : i18n.t('lock')}
</span>
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleModSticky)}>{post.stickied ? i18n.t('unsticky') : i18n.t('sticky')}</span> <span
class="pointer"
onClick={linkEvent(this, this.handleModSticky)}
>
{post.stickied ? i18n.t('unsticky') : i18n.t('sticky')}
</span>
</li> </li>
</> </>
} )}
{/* Mods can ban from community, and appoint as mods to community */} {/* Mods can ban from community, and appoint as mods to community */}
{(this.canMod || this.canAdmin) && {(this.canMod || this.canAdmin) && (
<li className="list-inline-item"> <li className="list-inline-item">
{!post.removed ? {!post.removed ? (
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> : <span
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span> class="pointer"
} onClick={linkEvent(this, this.handleModRemoveShow)}
>
<T i18nKey="remove">#</T>
</span>
) : (
<span
class="pointer"
onClick={linkEvent(this, this.handleModRemoveSubmit)}
>
<T i18nKey="restore">#</T>
</span>
)}
</li> </li>
} )}
{this.canMod && {this.canMod && (
<> <>
{!this.isMod && {!this.isMod && (
<li className="list-inline-item"> <li className="list-inline-item">
{!post.banned_from_community ? {!post.banned_from_community ? (
<span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunityShow)}><T i18nKey="ban">#</T></span> : <span
<span class="pointer" onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}><T i18nKey="unban">#</T></span> class="pointer"
} onClick={linkEvent(
this,
this.handleModBanFromCommunityShow
)}
>
<T i18nKey="ban">#</T>
</span>
) : (
<span
class="pointer"
onClick={linkEvent(
this,
this.handleModBanFromCommunitySubmit
)}
>
<T i18nKey="unban">#</T>
</span>
)}
</li> </li>
} )}
{!post.banned_from_community && {!post.banned_from_community && (
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{this.isMod ? i18n.t('remove_as_mod') : i18n.t('appoint_as_mod')}</span> <span
class="pointer"
onClick={linkEvent(
this,
this.handleAddModToCommunity
)}
>
{this.isMod
? i18n.t('remove_as_mod')
: i18n.t('appoint_as_mod')}
</span>
</li> </li>
} )}
</> </>
} )}
{/* Community creators and admins can transfer community to another mod */} {/* Community creators and admins can transfer community to another mod */}
{(this.amCommunityCreator || this.canAdmin) && this.isMod && {(this.amCommunityCreator || this.canAdmin) && this.isMod && (
<li className="list-inline-item"> <li className="list-inline-item">
{!this.state.showConfirmTransferCommunity ? {!this.state.showConfirmTransferCommunity ? (
<span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferCommunity)}><T i18nKey="transfer_community">#</T> <span
</span> : <> class="pointer"
<span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span> onClick={linkEvent(
<span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferCommunity)}><T i18nKey="yes">#</T></span> this,
<span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferCommunity)}><T i18nKey="no">#</T></span> this.handleShowConfirmTransferCommunity
</> )}
} >
</li> <T i18nKey="transfer_community">#</T>
} </span>
{/* Admins can ban from all, and appoint other admins */} ) : (
{this.canAdmin &&
<> <>
{!this.isAdmin && <span class="d-inline-block mr-1">
<li className="list-inline-item"> <T i18nKey="are_you_sure">#</T>
{!post.banned ? </span>
<span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}><T i18nKey="ban_from_site">#</T></span> : <span
<span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}><T i18nKey="unban_from_site">#</T></span> class="pointer d-inline-block mr-1"
} onClick={linkEvent(
</li> this,
} this.handleTransferCommunity
{!post.banned && )}
<li className="list-inline-item"> >
<span class="pointer" onClick={linkEvent(this, this.handleAddAdmin)}>{this.isAdmin ? i18n.t('remove_as_admin') : i18n.t('appoint_as_admin')}</span> <T i18nKey="yes">#</T>
</li> </span>
} <span
class="pointer d-inline-block"
onClick={linkEvent(
this,
this.handleCancelShowConfirmTransferCommunity
)}
>
<T i18nKey="no">#</T>
</span>
</> </>
} )}
</li>
)}
{/* Admins can ban from all, and appoint other admins */}
{this.canAdmin && (
<>
{!this.isAdmin && (
<li className="list-inline-item">
{!post.banned ? (
<span
class="pointer"
onClick={linkEvent(this, this.handleModBanShow)}
>
<T i18nKey="ban_from_site">#</T>
</span>
) : (
<span
class="pointer"
onClick={linkEvent(this, this.handleModBanSubmit)}
>
<T i18nKey="unban_from_site">#</T>
</span>
)}
</li>
)}
{!post.banned && (
<li className="list-inline-item">
<span
class="pointer"
onClick={linkEvent(this, this.handleAddAdmin)}
>
{this.isAdmin
? i18n.t('remove_as_admin')
: i18n.t('appoint_as_admin')}
</span>
</li>
)}
</>
)}
{/* Site Creator can transfer to another admin */} {/* Site Creator can transfer to another admin */}
{this.amSiteCreator && this.isAdmin && {this.amSiteCreator && this.isAdmin && (
<li className="list-inline-item"> <li className="list-inline-item">
{!this.state.showConfirmTransferSite ? {!this.state.showConfirmTransferSite ? (
<span class="pointer" onClick={linkEvent(this, this.handleShowConfirmTransferSite)}><T i18nKey="transfer_site">#</T> <span
</span> : <> class="pointer"
<span class="d-inline-block mr-1"><T i18nKey="are_you_sure">#</T></span> onClick={linkEvent(
<span class="pointer d-inline-block mr-1" onClick={linkEvent(this, this.handleTransferSite)}><T i18nKey="yes">#</T></span> this,
<span class="pointer d-inline-block" onClick={linkEvent(this, this.handleCancelShowConfirmTransferSite)}><T i18nKey="no">#</T></span> this.handleShowConfirmTransferSite
)}
>
<T i18nKey="transfer_site">#</T>
</span>
) : (
<>
<span class="d-inline-block mr-1">
<T i18nKey="are_you_sure">#</T>
</span>
<span
class="pointer d-inline-block mr-1"
onClick={linkEvent(this, this.handleTransferSite)}
>
<T i18nKey="yes">#</T>
</span>
<span
class="pointer d-inline-block"
onClick={linkEvent(
this,
this.handleCancelShowConfirmTransferSite
)}
>
<T i18nKey="no">#</T>
</span>
</> </>
} )}
</li> </li>
} )}
</> </>
} )}
{this.props.showBody && post.body && {this.props.showBody && post.body && (
<li className="list-inline-item"> <li className="list-inline-item">
<span className="pointer" onClick={linkEvent(this, this.handleViewSource)}><T i18nKey="view_source">#</T></span> <span
className="pointer"
onClick={linkEvent(this, this.handleViewSource)}
>
<T i18nKey="view_source">#</T>
</span>
</li> </li>
} )}
</ul> </ul>
{this.state.showRemoveDialog && {this.state.showRemoveDialog && (
<form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}> <form
<input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} /> class="form-inline"
<button type="submit" class="btn btn-secondary"><T i18nKey="remove_post">#</T></button> onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
>
<input
type="text"
class="form-control mr-2"
placeholder={i18n.t('reason')}
value={this.state.removeReason}
onInput={linkEvent(this, this.handleModRemoveReasonChange)}
/>
<button type="submit" class="btn btn-secondary">
<T i18nKey="remove_post">#</T>
</button>
</form> </form>
} )}
{this.state.showBanDialog && {this.state.showBanDialog && (
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}> <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
<div class="form-group row"> <div class="form-group row">
<label class="col-form-label"><T i18nKey="reason">#</T></label> <label class="col-form-label">
<input type="text" class="form-control mr-2" placeholder={i18n.t('reason')} value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} /> <T i18nKey="reason">#</T>
</label>
<input
type="text"
class="form-control mr-2"
placeholder={i18n.t('reason')}
value={this.state.banReason}
onInput={linkEvent(this, this.handleModBanReasonChange)}
/>
</div> </div>
{/* TODO hold off on expires until later */} {/* TODO hold off on expires until later */}
{/* <div class="form-group row"> */} {/* <div class="form-group row"> */}
@ -303,40 +586,64 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */} {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
{/* </div> */} {/* </div> */}
<div class="form-group row"> <div class="form-group row">
<button type="submit" class="btn btn-secondary">{i18n.t('ban')} {post.creator_name}</button> <button type="submit" class="btn btn-secondary">
{i18n.t('ban')} {post.creator_name}
</button>
</div> </div>
</form> </form>
} )}
{this.props.showBody && post.body && {this.props.showBody && post.body && (
<> <>
{this.state.viewSource ? <pre>{post.body}</pre> : {this.state.viewSource ? (
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(post.body)} /> <pre>{post.body}</pre>
} ) : (
<div
className="md-div"
dangerouslySetInnerHTML={mdToHtml(post.body)}
/>
)}
</> </>
} )}
</div> </div>
</div> </div>
) );
} }
private get myPost(): boolean { private get myPost(): boolean {
return UserService.Instance.user && this.props.post.creator_id == UserService.Instance.user.id; return (
UserService.Instance.user &&
this.props.post.creator_id == UserService.Instance.user.id
);
} }
get isMod(): boolean { get isMod(): boolean {
return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.post.creator_id); return (
this.props.moderators &&
isMod(
this.props.moderators.map(m => m.user_id),
this.props.post.creator_id
)
);
} }
get isAdmin(): boolean { get isAdmin(): boolean {
return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.post.creator_id); return (
this.props.admins &&
isMod(this.props.admins.map(a => a.id), this.props.post.creator_id)
);
} }
get canMod(): boolean { get canMod(): boolean {
if (this.props.admins && this.props.moderators) { if (this.props.admins && this.props.moderators) {
let adminsThenMods = this.props.admins.map(a => a.id) let adminsThenMods = this.props.admins
.map(a => a.id)
.concat(this.props.moderators.map(m => m.user_id)); .concat(this.props.moderators.map(m => m.user_id));
return canMod(UserService.Instance.user, adminsThenMods, this.props.post.creator_id); return canMod(
UserService.Instance.user,
adminsThenMods,
this.props.post.creator_id
);
} else { } else {
return false; return false;
} }
@ -344,38 +651,54 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
get canModOnSelf(): boolean { get canModOnSelf(): boolean {
if (this.props.admins && this.props.moderators) { if (this.props.admins && this.props.moderators) {
let adminsThenMods = this.props.admins.map(a => a.id) let adminsThenMods = this.props.admins
.map(a => a.id)
.concat(this.props.moderators.map(m => m.user_id)); .concat(this.props.moderators.map(m => m.user_id));
return canMod(UserService.Instance.user, adminsThenMods, this.props.post.creator_id, true); return canMod(
UserService.Instance.user,
adminsThenMods,
this.props.post.creator_id,
true
);
} else { } else {
return false; return false;
} }
} }
get canAdmin(): boolean { get canAdmin(): boolean {
return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.post.creator_id); return (
this.props.admins &&
canMod(
UserService.Instance.user,
this.props.admins.map(a => a.id),
this.props.post.creator_id
)
);
} }
get amCommunityCreator(): boolean { get amCommunityCreator(): boolean {
return this.props.moderators && return (
this.props.moderators &&
UserService.Instance.user && UserService.Instance.user &&
(this.props.post.creator_id != UserService.Instance.user.id) && this.props.post.creator_id != UserService.Instance.user.id &&
(UserService.Instance.user.id == this.props.moderators[0].user_id); UserService.Instance.user.id == this.props.moderators[0].user_id
);
} }
get amSiteCreator(): boolean { get amSiteCreator(): boolean {
return this.props.admins && return (
this.props.admins &&
UserService.Instance.user && UserService.Instance.user &&
(this.props.post.creator_id != UserService.Instance.user.id) && this.props.post.creator_id != UserService.Instance.user.id &&
(UserService.Instance.user.id == this.props.admins[0].id); UserService.Instance.user.id == this.props.admins[0].id
);
} }
handlePostLike(i: PostListing) { handlePostLike(i: PostListing) {
let form: CreatePostLikeForm = { let form: CreatePostLikeForm = {
post_id: i.props.post.id, post_id: i.props.post.id,
score: (i.props.post.my_vote == 1) ? 0 : 1 score: i.props.post.my_vote == 1 ? 0 : 1,
}; };
WebSocketService.Instance.likePost(form); WebSocketService.Instance.likePost(form);
} }
@ -383,7 +706,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
handlePostDisLike(i: PostListing) { handlePostDisLike(i: PostListing) {
let form: CreatePostLikeForm = { let form: CreatePostLikeForm = {
post_id: i.props.post.id, post_id: i.props.post.id,
score: (i.props.post.my_vote == -1) ? 0 : -1 score: i.props.post.my_vote == -1 ? 0 : -1,
}; };
WebSocketService.Instance.likePost(form); WebSocketService.Instance.likePost(form);
} }
@ -414,16 +737,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
creator_id: i.props.post.creator_id, creator_id: i.props.post.creator_id,
deleted: !i.props.post.deleted, deleted: !i.props.post.deleted,
nsfw: i.props.post.nsfw, nsfw: i.props.post.nsfw,
auth: null auth: null,
}; };
WebSocketService.Instance.editPost(deleteForm); WebSocketService.Instance.editPost(deleteForm);
} }
handleSavePostClick(i: PostListing) { handleSavePostClick(i: PostListing) {
let saved = (i.props.post.saved == undefined) ? true : !i.props.post.saved; let saved = i.props.post.saved == undefined ? true : !i.props.post.saved;
let form: SavePostForm = { let form: SavePostForm = {
post_id: i.props.post.id, post_id: i.props.post.id,
save: saved save: saved,
}; };
WebSocketService.Instance.savePost(form); WebSocketService.Instance.savePost(form);
@ -622,4 +945,3 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
i.setState(i.state); i.setState(i.state);
} }
} }

View file

@ -10,7 +10,6 @@ interface PostListingsProps {
} }
export class PostListings extends Component<PostListingsProps, any> { export class PostListings extends Component<PostListingsProps, any> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
} }
@ -18,19 +17,32 @@ export class PostListings extends Component<PostListingsProps, any> {
render() { render() {
return ( return (
<div> <div>
{this.props.posts.length > 0 ? this.props.posts.map(post => {this.props.posts.length > 0 ? (
this.props.posts.map(post => (
<> <>
<PostListing post={post} showCommunity={this.props.showCommunity} /> <PostListing
post={post}
showCommunity={this.props.showCommunity}
/>
<hr class="d-md-none my-2" /> <hr class="d-md-none my-2" />
<div class="d-none d-md-block my-2"></div> <div class="d-none d-md-block my-2"></div>
</> </>
) : ))
) : (
<> <>
<div><T i18nKey="no_posts">#</T></div> <div>
{this.props.showCommunity !== undefined && <div><T i18nKey="subscribe_to_communities">#<Link to="/communities">#</Link></T></div>} <T i18nKey="no_posts">#</T>
</>
}
</div> </div>
) {this.props.showCommunity !== undefined && (
<div>
<T i18nKey="subscribe_to_communities">
#<Link to="/communities">#</Link>
</T>
</div>
)}
</>
)}
</div>
);
} }
} }

View file

@ -1,7 +1,32 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs"; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, BanUserResponse, AddModToCommunityResponse, AddAdminResponse, UserView, SearchType, SortType, SearchForm, SearchResponse, GetSiteResponse, GetCommunityResponse } from '../interfaces'; import {
UserOperation,
Community,
Post as PostI,
GetPostResponse,
PostResponse,
Comment,
CommentForm as CommentFormI,
CommentResponse,
CommentSortType,
CreatePostLikeResponse,
CommunityUser,
CommunityResponse,
CommentNode as CommentNodeI,
BanFromCommunityResponse,
BanUserResponse,
AddModToCommunityResponse,
AddAdminResponse,
UserView,
SearchType,
SortType,
SearchForm,
SearchResponse,
GetSiteResponse,
GetCommunityResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { msgOp, hotRank } from '../utils'; import { msgOp, hotRank } from '../utils';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
@ -27,7 +52,6 @@ interface PostState {
} }
export class Post extends Component<any, PostState> { export class Post extends Component<any, PostState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: PostState = { private emptyState: PostState = {
post: null, post: null,
@ -39,7 +63,7 @@ export class Post extends Component<any, PostState> {
scrolled: false, scrolled: false,
loading: true, loading: true,
crossPosts: [], crossPosts: [],
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -52,10 +76,17 @@ export class Post extends Component<any, PostState> {
} }
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), msg => this.parseMessage(msg),
(err) => console.error(err), err => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
@ -71,10 +102,16 @@ export class Post extends Component<any, PostState> {
} }
componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) { componentDidUpdate(_lastProps: any, lastState: PostState, _snapshot: any) {
if (this.state.scrolled_comment_id && !this.state.scrolled && lastState.comments.length > 0) { if (
var elmnt = document.getElementById(`comment-${this.state.scrolled_comment_id}`); this.state.scrolled_comment_id &&
!this.state.scrolled &&
lastState.comments.length > 0
) {
var elmnt = document.getElementById(
`comment-${this.state.scrolled_comment_id}`
);
elmnt.scrollIntoView(); elmnt.scrollIntoView();
elmnt.classList.add("mark"); elmnt.classList.add('mark');
this.state.scrolled = true; this.state.scrolled = true;
this.markScrolledAsRead(this.state.scrolled_comment_id); this.markScrolledAsRead(this.state.scrolled_comment_id);
} }
@ -89,17 +126,20 @@ export class Post extends Component<any, PostState> {
// this.context.router.history.push('/sponsors'); // this.context.router.history.push('/sponsors');
// this.context.refresh(); // this.context.refresh();
// this.context.router.history.push(_lastProps.location.pathname); // this.context.router.history.push(_lastProps.location.pathname);
} }
} }
markScrolledAsRead(commentId: number) { markScrolledAsRead(commentId: number) {
let found = this.state.comments.find(c => c.id == commentId); let found = this.state.comments.find(c => c.id == commentId);
let parent = this.state.comments.find(c => found.parent_id == c.id); let parent = this.state.comments.find(c => found.parent_id == c.id);
let parent_user_id = parent ? parent.creator_id : this.state.post.creator_id; let parent_user_id = parent
? parent.creator_id
if (UserService.Instance.user && UserService.Instance.user.id == parent_user_id) { : this.state.post.creator_id;
if (
UserService.Instance.user &&
UserService.Instance.user.id == parent_user_id
) {
let form: CommentFormI = { let form: CommentFormI = {
content: found.content, content: found.content,
edit_id: found.id, edit_id: found.id,
@ -107,7 +147,7 @@ export class Post extends Component<any, PostState> {
post_id: found.post_id, post_id: found.post_id,
parent_id: found.parent_id, parent_id: found.parent_id,
read: true, read: true,
auth: null auth: null,
}; };
WebSocketService.Instance.editComment(form); WebSocketService.Instance.editComment(form);
} }
@ -116,8 +156,13 @@ export class Post extends Component<any, PostState> {
render() { render() {
return ( return (
<div class="container"> <div class="container">
{this.state.loading ? {this.state.loading ? (
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : <h5>
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
</h5>
) : (
<div class="row"> <div class="row">
<div class="col-12 col-md-8 mb-3"> <div class="col-12 col-md-8 mb-3">
<PostListing <PostListing
@ -128,14 +173,19 @@ export class Post extends Component<any, PostState> {
moderators={this.state.moderators} moderators={this.state.moderators}
admins={this.state.admins} admins={this.state.admins}
/> />
{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>
<PostListings showCommunity posts={this.state.crossPosts} /> <PostListings showCommunity posts={this.state.crossPosts} />
</> </>
} )}
<div className="mb-2" /> <div className="mb-2" />
<CommentForm postId={this.state.post.id} disabled={this.state.post.locked} /> <CommentForm
postId={this.state.post.id}
disabled={this.state.post.locked}
/>
{this.sortRadios()} {this.sortRadios()}
{this.commentsTree()} {this.commentsTree()}
</div> </div>
@ -144,39 +194,62 @@ export class Post extends Component<any, PostState> {
{this.sidebar()} {this.sidebar()}
</div> </div>
</div> </div>
} )}
</div> </div>
) );
} }
sortRadios() { sortRadios() {
return ( return (
<div class="btn-group btn-group-toggle mb-3"> <div class="btn-group btn-group-toggle mb-3">
<label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Hot && 'active'}`}>{i18n.t('hot')} <label
<input type="radio" value={CommentSortType.Hot} className={`btn btn-sm btn-secondary pointer ${this.state
.commentSort === CommentSortType.Hot && 'active'}`}
>
{i18n.t('hot')}
<input
type="radio"
value={CommentSortType.Hot}
checked={this.state.commentSort === CommentSortType.Hot} checked={this.state.commentSort === CommentSortType.Hot}
onChange={linkEvent(this, this.handleCommentSortChange)} /> onChange={linkEvent(this, this.handleCommentSortChange)}
/>
</label> </label>
<label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.Top && 'active'}`}>{i18n.t('top')} <label
<input type="radio" value={CommentSortType.Top} className={`btn btn-sm btn-secondary pointer ${this.state
.commentSort === CommentSortType.Top && 'active'}`}
>
{i18n.t('top')}
<input
type="radio"
value={CommentSortType.Top}
checked={this.state.commentSort === CommentSortType.Top} checked={this.state.commentSort === CommentSortType.Top}
onChange={linkEvent(this, this.handleCommentSortChange)} /> onChange={linkEvent(this, this.handleCommentSortChange)}
/>
</label> </label>
<label className={`btn btn-sm btn-secondary pointer ${this.state.commentSort === CommentSortType.New && 'active'}`}>{i18n.t('new')} <label
<input type="radio" value={CommentSortType.New} className={`btn btn-sm btn-secondary pointer ${this.state
.commentSort === CommentSortType.New && 'active'}`}
>
{i18n.t('new')}
<input
type="radio"
value={CommentSortType.New}
checked={this.state.commentSort === CommentSortType.New} checked={this.state.commentSort === CommentSortType.New}
onChange={linkEvent(this, this.handleCommentSortChange)} /> onChange={linkEvent(this, this.handleCommentSortChange)}
/>
</label> </label>
</div> </div>
) );
} }
newComments() { newComments() {
return ( return (
<div class="d-none d-md-block new-comments mb-3 card border-secondary"> <div class="d-none d-md-block new-comments mb-3 card border-secondary">
<div class="card-body small"> <div class="card-body small">
<h6><T i18nKey="recent_comments">#</T></h6> <h6>
{this.state.comments.map(comment => <T i18nKey="recent_comments">#</T>
</h6>
{this.state.comments.map(comment => (
<CommentNodes <CommentNodes
nodes={[{ comment: comment }]} nodes={[{ comment: comment }]}
noIndent noIndent
@ -185,10 +258,10 @@ export class Post extends Component<any, PostState> {
admins={this.state.admins} admins={this.state.admins}
postCreatorId={this.state.post.creator_id} postCreatorId={this.state.post.creator_id}
/> />
)} ))}
</div> </div>
</div> </div>
) );
} }
sidebar() { sidebar() {
@ -213,7 +286,7 @@ export class Post extends Component<any, PostState> {
for (let comment of this.state.comments) { for (let comment of this.state.comments) {
let node: CommentNodeI = { let node: CommentNodeI = {
comment: comment, comment: comment,
children: [] children: [],
}; };
map.set(comment.id, { ...node }); map.set(comment.id, { ...node });
} }
@ -221,8 +294,7 @@ export class Post extends Component<any, PostState> {
for (let comment of this.state.comments) { for (let comment of this.state.comments) {
if (comment.parent_id) { if (comment.parent_id) {
map.get(comment.parent_id).children.push(map.get(comment.id)); map.get(comment.parent_id).children.push(map.get(comment.id));
} } else {
else {
tree.push(map.get(comment.id)); tree.push(map.get(comment.id));
} }
} }
@ -233,26 +305,33 @@ export class Post extends Component<any, PostState> {
} }
sortTree(tree: Array<CommentNodeI>) { sortTree(tree: Array<CommentNodeI>) {
// First, put removed and deleted comments at the bottom, then do your other sorts // First, put removed and deleted comments at the bottom, then do your other sorts
if (this.state.commentSort == CommentSortType.Top) { if (this.state.commentSort == CommentSortType.Top) {
tree.sort((a, b) => (+a.comment.removed - +b.comment.removed) || tree.sort(
(+a.comment.deleted - +b.comment.deleted ) || (a, b) =>
(b.comment.score - a.comment.score)); +a.comment.removed - +b.comment.removed ||
+a.comment.deleted - +b.comment.deleted ||
b.comment.score - a.comment.score
);
} else if (this.state.commentSort == CommentSortType.New) { } else if (this.state.commentSort == CommentSortType.New) {
tree.sort((a, b) => (+a.comment.removed - +b.comment.removed) || tree.sort(
(+a.comment.deleted - +b.comment.deleted ) || (a, b) =>
(b.comment.published.localeCompare(a.comment.published))); +a.comment.removed - +b.comment.removed ||
+a.comment.deleted - +b.comment.deleted ||
b.comment.published.localeCompare(a.comment.published)
);
} else if (this.state.commentSort == CommentSortType.Hot) { } else if (this.state.commentSort == CommentSortType.Hot) {
tree.sort((a, b) => (+a.comment.removed - +b.comment.removed) || tree.sort(
(+a.comment.deleted - +b.comment.deleted ) || (a, b) =>
(hotRank(b.comment) - hotRank(a.comment))); +a.comment.removed - +b.comment.removed ||
+a.comment.deleted - +b.comment.deleted ||
hotRank(b.comment) - hotRank(a.comment)
);
} }
for (let node of tree) { for (let node of tree) {
this.sortTree(node.children); this.sortTree(node.children);
} }
} }
commentsTree() { commentsTree() {
@ -323,12 +402,13 @@ export class Post extends Component<any, PostState> {
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let res: CommentResponse = msg; let res: CommentResponse = msg;
let found: Comment = this.state.comments.find(c => c.id === res.comment.id); let found: Comment = this.state.comments.find(
c => c.id === res.comment.id
);
found.score = res.comment.score; found.score = res.comment.score;
found.upvotes = res.comment.upvotes; found.upvotes = res.comment.upvotes;
found.downvotes = res.comment.downvotes; found.downvotes = res.comment.downvotes;
if (res.comment.my_vote !== null) if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
found.my_vote = res.comment.my_vote;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) { } else if (op == UserOperation.CreatePostLike) {
let res: CreatePostLikeResponse = msg; let res: CreatePostLikeResponse = msg;
@ -354,12 +434,14 @@ export class Post extends Component<any, PostState> {
} else if (op == UserOperation.FollowCommunity) { } else if (op == UserOperation.FollowCommunity) {
let res: CommunityResponse = msg; let res: CommunityResponse = msg;
this.state.community.subscribed = res.community.subscribed; this.state.community.subscribed = res.community.subscribed;
this.state.community.number_of_subscribers = res.community.number_of_subscribers; this.state.community.number_of_subscribers =
res.community.number_of_subscribers;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BanFromCommunity) { } else if (op == UserOperation.BanFromCommunity) {
let res: BanFromCommunityResponse = msg; let res: BanFromCommunityResponse = msg;
this.state.comments.filter(c => c.creator_id == res.user.id) this.state.comments
.forEach(c => c.banned_from_community = res.banned); .filter(c => c.creator_id == res.user.id)
.forEach(c => (c.banned_from_community = res.banned));
if (this.state.post.creator_id == res.user.id) { if (this.state.post.creator_id == res.user.id) {
this.state.post.banned_from_community = res.banned; this.state.post.banned_from_community = res.banned;
} }
@ -370,8 +452,9 @@ export class Post extends Component<any, PostState> {
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BanUser) { } else if (op == UserOperation.BanUser) {
let res: BanUserResponse = msg; let res: BanUserResponse = msg;
this.state.comments.filter(c => c.creator_id == res.user.id) this.state.comments
.forEach(c => c.banned = res.banned); .filter(c => c.creator_id == res.user.id)
.forEach(c => (c.banned = res.banned));
if (this.state.post.creator_id == res.user.id) { if (this.state.post.creator_id == res.user.id) {
this.state.post.banned = res.banned; this.state.post.banned = res.banned;
} }
@ -396,9 +479,5 @@ export class Post extends Component<any, PostState> {
this.state.admins = res.admins; this.state.admins = res.admins;
this.setState(this.state); this.setState(this.state);
} }
} }
} }

View file

@ -1,26 +1,40 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Subscription } from "rxjs"; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Post, Comment, Community, UserView, SortType, SearchForm, SearchResponse, SearchType } from '../interfaces'; import {
UserOperation,
Post,
Comment,
Community,
UserView,
SortType,
SearchForm,
SearchResponse,
SearchType,
} from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { msgOp, fetchLimit, routeSearchTypeToEnum, routeSortTypeToEnum } from '../utils'; import {
msgOp,
fetchLimit,
routeSearchTypeToEnum,
routeSortTypeToEnum,
} from '../utils';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
interface SearchState { interface SearchState {
q: string, q: string;
type_: SearchType, type_: SearchType;
sort: SortType, sort: SortType;
page: number, page: number;
searchResponse: SearchResponse; searchResponse: SearchResponse;
loading: boolean; loading: boolean;
} }
export class Search extends Component<any, SearchState> { export class Search extends Component<any, SearchState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: SearchState = { private emptyState: SearchState = {
q: this.getSearchQueryFromProps(this.props), q: this.getSearchQueryFromProps(this.props),
@ -36,26 +50,26 @@ export class Search extends Component<any, SearchState> {
users: [], users: [],
}, },
loading: false, loading: false,
} };
getSearchQueryFromProps(props: any): string { getSearchQueryFromProps(props: any): string {
return (props.match.params.q) ? props.match.params.q : ''; return props.match.params.q ? props.match.params.q : '';
} }
getSearchTypeFromProps(props: any): SearchType { getSearchTypeFromProps(props: any): SearchType {
return (props.match.params.type) ? return props.match.params.type
routeSearchTypeToEnum(props.match.params.type) : ? routeSearchTypeToEnum(props.match.params.type)
SearchType.All; : SearchType.All;
} }
getSortTypeFromProps(props: any): SortType { getSortTypeFromProps(props: any): SortType {
return (props.match.params.sort) ? return props.match.params.sort
routeSortTypeToEnum(props.match.params.sort) : ? routeSortTypeToEnum(props.match.params.sort)
SortType.TopAll; : SortType.TopAll;
} }
getPageFromProps(props: any): number { getPageFromProps(props: any): number {
return (props.match.params.page) ? Number(props.match.params.page) : 1; return props.match.params.page ? Number(props.match.params.page) : 1;
} }
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -64,17 +78,23 @@ export class Search extends Component<any, SearchState> {
this.state = this.emptyState; this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), msg => this.parseMessage(msg),
(err) => console.error(err), err => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
if (this.state.q) { if (this.state.q) {
this.search(); this.search();
} }
} }
componentWillUnmount() { componentWillUnmount() {
@ -83,7 +103,10 @@ export class Search extends Component<any, SearchState> {
// Necessary for back button for some reason // Necessary for back button for some reason
componentWillReceiveProps(nextProps: any) { componentWillReceiveProps(nextProps: any) {
if (nextProps.history.action == 'POP' || nextProps.history.action == 'PUSH') { if (
nextProps.history.action == 'POP' ||
nextProps.history.action == 'PUSH'
) {
this.state = this.emptyState; this.state = this.emptyState;
this.state.q = this.getSearchQueryFromProps(nextProps); this.state.q = this.getSearchQueryFromProps(nextProps);
this.state.type_ = this.getSearchTypeFromProps(nextProps); this.state.type_ = this.getSearchTypeFromProps(nextProps);
@ -95,7 +118,9 @@ export class Search extends Component<any, SearchState> {
} }
componentDidMount() { componentDidMount() {
document.title = `${i18n.t('search')} - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('search')} - ${
WebSocketService.Instance.site.name
}`;
} }
render() { render() {
@ -103,77 +128,129 @@ export class Search extends Component<any, SearchState> {
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<h5><T i18nKey="search">#</T></h5> <h5>
<T i18nKey="search">#</T>
</h5>
{this.selects()} {this.selects()}
{this.searchForm()} {this.searchForm()}
{this.state.type_ == SearchType.All && {this.state.type_ == SearchType.All && this.all()}
this.all() {this.state.type_ == SearchType.Comments && this.comments()}
} {this.state.type_ == SearchType.Posts && this.posts()}
{this.state.type_ == SearchType.Comments && {this.state.type_ == SearchType.Communities && this.communities()}
this.comments() {this.state.type_ == SearchType.Users && this.users()}
}
{this.state.type_ == SearchType.Posts &&
this.posts()
}
{this.state.type_ == SearchType.Communities &&
this.communities()
}
{this.state.type_ == SearchType.Users &&
this.users()
}
{this.noResults()} {this.noResults()}
{this.paginator()} {this.paginator()}
</div> </div>
</div> </div>
</div> </div>
) );
} }
searchForm() { searchForm() {
return ( return (
<form class="form-inline" onSubmit={linkEvent(this, this.handleSearchSubmit)}> <form
<input type="text" class="form-control mr-2" value={this.state.q} placeholder={`${i18n.t('search')}...`} onInput={linkEvent(this, this.handleQChange)} required minLength={3} /> class="form-inline"
onSubmit={linkEvent(this, this.handleSearchSubmit)}
>
<input
type="text"
class="form-control mr-2"
value={this.state.q}
placeholder={`${i18n.t('search')}...`}
onInput={linkEvent(this, this.handleQChange)}
required
minLength={3}
/>
<button type="submit" class="btn btn-secondary mr-2"> <button type="submit" class="btn btn-secondary mr-2">
{this.state.loading ? {this.state.loading ? (
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : <svg class="icon icon-spinner spin">
<span><T i18nKey="search">#</T></span> <use xlinkHref="#icon-spinner"></use>
} </svg>
) : (
<span>
<T i18nKey="search">#</T>
</span>
)}
</button> </button>
</form> </form>
) );
} }
selects() { selects() {
return ( return (
<div className="mb-2"> <div className="mb-2">
<select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="custom-select custom-select-sm w-auto"> <select
<option disabled><T i18nKey="type">#</T></option> value={this.state.type_}
<option value={SearchType.All}><T i18nKey="all">#</T></option> onChange={linkEvent(this, this.handleTypeChange)}
<option value={SearchType.Comments}><T i18nKey="comments">#</T></option> class="custom-select custom-select-sm w-auto"
<option value={SearchType.Posts}><T i18nKey="posts">#</T></option> >
<option value={SearchType.Communities}><T i18nKey="communities">#</T></option> <option disabled>
<option value={SearchType.Users}><T i18nKey="users">#</T></option> <T i18nKey="type">#</T>
</option>
<option value={SearchType.All}>
<T i18nKey="all">#</T>
</option>
<option value={SearchType.Comments}>
<T i18nKey="comments">#</T>
</option>
<option value={SearchType.Posts}>
<T i18nKey="posts">#</T>
</option>
<option value={SearchType.Communities}>
<T i18nKey="communities">#</T>
</option>
<option value={SearchType.Users}>
<T i18nKey="users">#</T>
</option>
</select> </select>
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2"> <select
<option disabled><T i18nKey="sort_type">#</T></option> value={this.state.sort}
<option value={SortType.New}><T i18nKey="new">#</T></option> onChange={linkEvent(this, this.handleSortChange)}
<option value={SortType.TopDay}><T i18nKey="top_day">#</T></option> class="custom-select custom-select-sm w-auto ml-2"
<option value={SortType.TopWeek}><T i18nKey="week">#</T></option> >
<option value={SortType.TopMonth}><T i18nKey="month">#</T></option> <option disabled>
<option value={SortType.TopYear}><T i18nKey="year">#</T></option> <T i18nKey="sort_type">#</T>
<option value={SortType.TopAll}><T i18nKey="all">#</T></option> </option>
<option value={SortType.New}>
<T i18nKey="new">#</T>
</option>
<option value={SortType.TopDay}>
<T i18nKey="top_day">#</T>
</option>
<option value={SortType.TopWeek}>
<T i18nKey="week">#</T>
</option>
<option value={SortType.TopMonth}>
<T i18nKey="month">#</T>
</option>
<option value={SortType.TopYear}>
<T i18nKey="year">#</T>
</option>
<option value={SortType.TopAll}>
<T i18nKey="all">#</T>
</option>
</select> </select>
</div> </div>
) );
} }
all() { all() {
let combined: Array<{type_: string, data: Comment | Post | Community | UserView}> = []; let combined: Array<{
let comments = this.state.searchResponse.comments.map(e => {return {type_: "comments", data: e}}); type_: string;
let posts = this.state.searchResponse.posts.map(e => {return {type_: "posts", data: e}}); data: Comment | Post | Community | UserView;
let communities = this.state.searchResponse.communities.map(e => {return {type_: "communities", data: e}}); }> = [];
let users = this.state.searchResponse.users.map(e => {return {type_: "users", data: e}}); let comments = this.state.searchResponse.comments.map(e => {
return { type_: 'comments', data: e };
});
let posts = this.state.searchResponse.posts.map(e => {
return { type_: 'posts', data: e };
});
let communities = this.state.searchResponse.communities.map(e => {
return { type_: 'communities', data: e };
});
let users = this.state.searchResponse.users.map(e => {
return { type_: 'users', data: e };
});
combined.push(...comments); combined.push(...comments);
combined.push(...posts); combined.push(...posts);
@ -184,49 +261,68 @@ export class Search extends Component<any, SearchState> {
if (this.state.sort == SortType.New) { if (this.state.sort == SortType.New) {
combined.sort((a, b) => b.data.published.localeCompare(a.data.published)); combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
} else { } else {
combined.sort((a, b) => ((b.data as Comment | Post).score combined.sort(
| (b.data as Community).number_of_subscribers (a, b) =>
| (b.data as UserView).comment_score) ((b.data as Comment | Post).score |
- ((a.data as Comment | Post).score (b.data as Community).number_of_subscribers |
| (a.data as Community).number_of_subscribers (b.data as UserView).comment_score) -
| (a.data as UserView).comment_score)); ((a.data as Comment | Post).score |
(a.data as Community).number_of_subscribers |
(a.data as UserView).comment_score)
);
} }
return ( return (
<div> <div>
{combined.map(i => {combined.map(i => (
<div> <div>
{i.type_ == "posts" && {i.type_ == 'posts' && (
<PostListing post={i.data as Post} showCommunity viewOnly /> <PostListing post={i.data as Post} showCommunity viewOnly />
} )}
{i.type_ == "comments" && {i.type_ == 'comments' && (
<CommentNodes nodes={[{comment: i.data as Comment}]} viewOnly noIndent /> <CommentNodes
} nodes={[{ comment: i.data as Comment }]}
{i.type_ == "communities" && viewOnly
noIndent
/>
)}
{i.type_ == 'communities' && (
<div> <div>
<span><Link to={`/c/${(i.data as Community).name}`}>{`/c/${(i.data as Community).name}`}</Link></span> <span>
<span>{` - ${(i.data as Community).title} - ${(i.data as Community).number_of_subscribers} subscribers`}</span> <Link to={`/c/${(i.data as Community).name}`}>{`/c/${
(i.data as Community).name
}`}</Link>
</span>
<span>{` - ${(i.data as Community).title} - ${
(i.data as Community).number_of_subscribers
} subscribers`}</span>
</div> </div>
} )}
{i.type_ == "users" && {i.type_ == 'users' && (
<div> <div>
<span><Link className="text-info" to={`/u/${(i.data as UserView).name}`}>{`/u/${(i.data as UserView).name}`}</Link></span> <span>
<span>{` - ${(i.data as UserView).comment_score} comment karma`}</span> <Link
className="text-info"
to={`/u/${(i.data as UserView).name}`}
>{`/u/${(i.data as UserView).name}`}</Link>
</span>
<span>{` - ${
(i.data as UserView).comment_score
} comment karma`}</span>
</div> </div>
} )}
</div> </div>
) ))}
}
</div> </div>
) );
} }
comments() { comments() {
return ( return (
<div> <div>
{this.state.searchResponse.comments.map(comment => {this.state.searchResponse.comments.map(comment => (
<CommentNodes nodes={[{ comment: comment }]} noIndent viewOnly /> <CommentNodes nodes={[{ comment: comment }]} noIndent viewOnly />
)} ))}
</div> </div>
); );
} }
@ -234,9 +330,9 @@ export class Search extends Component<any, SearchState> {
posts() { posts() {
return ( return (
<div> <div>
{this.state.searchResponse.posts.map(post => {this.state.searchResponse.posts.map(post => (
<PostListing post={post} showCommunity viewOnly /> <PostListing post={post} showCommunity viewOnly />
)} ))}
</div> </div>
); );
} }
@ -245,12 +341,14 @@ export class Search extends Component<any, SearchState> {
communities() { communities() {
return ( return (
<div> <div>
{this.state.searchResponse.communities.map(community => {this.state.searchResponse.communities.map(community => (
<div> <div>
<span><Link to={`/c/${community.name}`}>{`/c/${community.name}`}</Link></span> <span>
<Link to={`/c/${community.name}`}>{`/c/${community.name}`}</Link>
</span>
<span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span> <span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span>
</div> </div>
)} ))}
</div> </div>
); );
} }
@ -258,12 +356,17 @@ export class Search extends Component<any, SearchState> {
users() { users() {
return ( return (
<div> <div>
{this.state.searchResponse.users.map(user => {this.state.searchResponse.users.map(user => (
<div> <div>
<span><Link className="text-info" to={`/u/${user.name}`}>{`/u/${user.name}`}</Link></span> <span>
<Link
className="text-info"
to={`/u/${user.name}`}
>{`/u/${user.name}`}</Link>
</span>
<span>{` - ${user.comment_score} comment karma`}</span> <span>{` - ${user.comment_score} comment karma`}</span>
</div> </div>
)} ))}
</div> </div>
); );
} }
@ -271,10 +374,20 @@ export class Search extends Component<any, SearchState> {
paginator() { paginator() {
return ( return (
<div class="mt-2"> <div class="mt-2">
{this.state.page > 1 && {this.state.page > 1 && (
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button> <button
} class="btn btn-sm btn-secondary mr-1"
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button> onClick={linkEvent(this, this.prevPage)}
>
<T i18nKey="prev">#</T>
</button>
)}
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
<T i18nKey="next">#</T>
</button>
</div> </div>
); );
} }
@ -283,11 +396,13 @@ export class Search extends Component<any, SearchState> {
let res = this.state.searchResponse; let res = this.state.searchResponse;
return ( return (
<div> <div>
{res && res.op && res.posts.length == 0 && res.comments.length == 0 && {res && res.op && res.posts.length == 0 && res.comments.length == 0 && (
<span><T i18nKey="no_results">#</T></span> <span>
} <T i18nKey="no_results">#</T>
</span>
)}
</div> </div>
) );
} }
nextPage(i: Search) { nextPage(i: Search) {
@ -349,7 +464,9 @@ export class Search extends Component<any, SearchState> {
updateUrl() { updateUrl() {
let typeStr = SearchType[this.state.type_].toLowerCase(); let typeStr = SearchType[this.state.type_].toLowerCase();
let sortStr = SortType[this.state.sort].toLowerCase(); let sortStr = SortType[this.state.sort].toLowerCase();
this.props.history.push(`/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`); this.props.history.push(
`/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
);
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -362,10 +479,11 @@ export class Search extends Component<any, SearchState> {
let res: SearchResponse = msg; let res: SearchResponse = msg;
this.state.searchResponse = res; this.state.searchResponse = res;
this.state.loading = false; this.state.loading = false;
document.title = `${i18n.t('search')} - ${this.state.q} - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('search')} - ${this.state.q} - ${
WebSocketService.Instance.site.name
}`;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state); this.setState(this.state);
} }
} }
} }

View file

@ -1,5 +1,5 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs"; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { RegisterForm, LoginResponse, UserOperation } from '../interfaces'; import { RegisterForm, LoginResponse, UserOperation } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
@ -27,8 +27,7 @@ export class Setup extends Component<any, State> {
}, },
doneRegisteringUser: false, doneRegisteringUser: false,
userLoading: false, userLoading: false,
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -36,11 +35,18 @@ export class Setup extends Component<any, State> {
this.state = this.emptyState; this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), msg => this.parseMessage(msg),
(err) => console.error(err), err => console.error(err),
() => console.log("complete") () => console.log('complete')
); );
} }
@ -57,54 +63,103 @@ export class Setup extends Component<any, State> {
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 offset-lg-3 col-lg-6"> <div class="col-12 offset-lg-3 col-lg-6">
<h3><T i18nKey="lemmy_instance_setup">#</T></h3> <h3>
{!this.state.doneRegisteringUser ? this.registerUser() : <SiteForm />} <T i18nKey="lemmy_instance_setup">#</T>
</h3>
{!this.state.doneRegisteringUser ? (
this.registerUser()
) : (
<SiteForm />
)}
</div> </div>
</div> </div>
</div> </div>
) );
} }
registerUser() { registerUser() {
return ( return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}> <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h5><T i18nKey="setup_admin">#</T></h5> <h5>
<T i18nKey="setup_admin">#</T>
</h5>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="username">#</T></label> <label class="col-sm-2 col-form-label">
<T i18nKey="username">#</T>
</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" class="form-control" value={this.state.userForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} maxLength={20} pattern="[a-zA-Z0-9_]+" /> <input
type="text"
class="form-control"
value={this.state.userForm.username}
onInput={linkEvent(this, this.handleRegisterUsernameChange)}
required
minLength={3}
maxLength={20}
pattern="[a-zA-Z0-9_]+"
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="email">#</T></label> <label class="col-sm-2 col-form-label">
<T i18nKey="email">#</T>
</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="email" class="form-control" placeholder={i18n.t('optional')} value={this.state.userForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} /> <input
type="email"
class="form-control"
placeholder={i18n.t('optional')}
value={this.state.userForm.email}
onInput={linkEvent(this, this.handleRegisterEmailChange)}
minLength={3}
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="password">#</T></label> <label class="col-sm-2 col-form-label">
<T i18nKey="password">#</T>
</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="password" value={this.state.userForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required /> <input
type="password"
value={this.state.userForm.password}
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
class="form-control"
required
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label"><T i18nKey="verify_password">#</T></label> <label class="col-sm-2 col-form-label">
<T i18nKey="verify_password">#</T>
</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="password" value={this.state.userForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required /> <input
type="password"
value={this.state.userForm.password_verify}
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
class="form-control"
required
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<button type="submit" class="btn btn-secondary">{this.state.userLoading ? <button type="submit" class="btn btn-secondary">
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : i18n.t('sign_up')}</button> {this.state.userLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
i18n.t('sign_up')
)}
</button>
</div> </div>
</div> </div>
</form> </form>
); );
} }
handleRegisterSubmit(i: Setup, event: any) { handleRegisterSubmit(i: Setup, event: any) {
event.preventDefault(); event.preventDefault();
i.state.userLoading = true; i.state.userLoading = true;

View file

@ -1,6 +1,12 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Community, CommunityUser, FollowCommunityForm, CommunityForm as CommunityFormI, UserView } from '../interfaces'; import {
Community,
CommunityUser,
FollowCommunityForm,
CommunityForm as CommunityFormI,
UserView,
} from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { mdToHtml, getUnixTime } from '../utils'; import { mdToHtml, getUnixTime } from '../utils';
import { CommunityForm } from './community-form'; import { CommunityForm } from './community-form';
@ -21,13 +27,12 @@ interface SidebarState {
} }
export class Sidebar extends Component<SidebarProps, SidebarState> { export class Sidebar extends Component<SidebarProps, SidebarState> {
private emptyState: SidebarState = { private emptyState: SidebarState = {
showEdit: false, showEdit: false,
showRemoveDialog: false, showRemoveDialog: false,
removeReason: null, removeReason: null,
removeExpires: null removeExpires: null,
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -39,15 +44,17 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
render() { render() {
return ( return (
<div> <div>
{!this.state.showEdit {!this.state.showEdit ? (
? this.sidebar() this.sidebar()
: <CommunityForm ) : (
<CommunityForm
community={this.props.community} community={this.props.community}
onEdit={this.handleEditCommunity} onEdit={this.handleEditCommunity}
onCancel={this.handleEditCancel} /> onCancel={this.handleEditCancel}
} />
)}
</div> </div>
) );
} }
sidebar() { sidebar() {
@ -58,44 +65,78 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<div class="card-body"> <div class="card-body">
<h5 className="mb-0"> <h5 className="mb-0">
<span>{community.title}</span> <span>{community.title}</span>
{community.removed && {community.removed && (
<small className="ml-2 text-muted font-italic"><T i18nKey="removed">#</T></small> <small className="ml-2 text-muted font-italic">
} <T i18nKey="removed">#</T>
{community.deleted && </small>
<small className="ml-2 text-muted font-italic"><T i18nKey="deleted">#</T></small> )}
} {community.deleted && (
<small className="ml-2 text-muted font-italic">
<T i18nKey="deleted">#</T>
</small>
)}
</h5> </h5>
<Link className="text-muted" to={`/c/${community.name}`}>/c/{community.name}</Link> <Link className="text-muted" to={`/c/${community.name}`}>
/c/{community.name}
</Link>
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
{this.canMod && {this.canMod && (
<> <>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}><T i18nKey="edit">#</T></span> <span
</li> class="pointer"
{this.amCreator && onClick={linkEvent(this, this.handleEditClick)}
<li className="list-inline-item"> >
<span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}> <T i18nKey="edit">#</T>
{!community.deleted ? i18n.t('delete') : i18n.t('restore')}
</span> </span>
</li> </li>
} {this.amCreator && (
</>
}
{this.canAdmin &&
<li className="list-inline-item"> <li className="list-inline-item">
{!this.props.community.removed ? <span
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}><T i18nKey="remove">#</T></span> : class="pointer"
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}><T i18nKey="restore">#</T></span> onClick={linkEvent(this, this.handleDeleteClick)}
} >
{!community.deleted
? i18n.t('delete')
: i18n.t('restore')}
</span>
</li> </li>
)}
} </>
)}
{this.canAdmin && (
<li className="list-inline-item">
{!this.props.community.removed ? (
<span
class="pointer"
onClick={linkEvent(this, this.handleModRemoveShow)}
>
<T i18nKey="remove">#</T>
</span>
) : (
<span
class="pointer"
onClick={linkEvent(this, this.handleModRemoveSubmit)}
>
<T i18nKey="restore">#</T>
</span>
)}
</li>
)}
</ul> </ul>
{this.state.showRemoveDialog && {this.state.showRemoveDialog && (
<form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}> <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
<div class="form-group row"> <div class="form-group row">
<label class="col-form-label"><T i18nKey="reason">#</T></label> <label class="col-form-label">
<input type="text" class="form-control mr-2" placeholder={i18n.t('optional')} value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} /> <T i18nKey="reason">#</T>
</label>
<input
type="text"
class="form-control mr-2"
placeholder={i18n.t('optional')}
value={this.state.removeReason}
onInput={linkEvent(this, this.handleModRemoveReasonChange)}
/>
</div> </div>
{/* TODO hold off on expires for now */} {/* TODO hold off on expires for now */}
{/* <div class="form-group row"> */} {/* <div class="form-group row"> */}
@ -103,40 +144,98 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */} {/* <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
{/* </div> */} {/* </div> */}
<div class="form-group row"> <div class="form-group row">
<button type="submit" class="btn btn-secondary"><T i18nKey="remove_community">#</T></button> <button type="submit" class="btn btn-secondary">
<T i18nKey="remove_community">#</T>
</button>
</div> </div>
</form> </form>
} )}
<ul class="my-1 list-inline"> <ul class="my-1 list-inline">
<li className="list-inline-item"><Link className="badge badge-secondary" to="/communities">{community.category_name}</Link></li> <li className="list-inline-item">
<li className="list-inline-item badge badge-secondary"><T i18nKey="number_of_subscribers" interpolation={{count: community.number_of_subscribers}}>#</T></li> <Link className="badge badge-secondary" to="/communities">
<li className="list-inline-item badge badge-secondary"><T i18nKey="number_of_posts" interpolation={{count: community.number_of_posts}}>#</T></li> {community.category_name}
<li className="list-inline-item badge badge-secondary"><T i18nKey="number_of_comments" interpolation={{count: community.number_of_comments}}>#</T></li> </Link>
<li className="list-inline-item"><Link className="badge badge-secondary" to={`/modlog/community/${this.props.community.id}`}><T i18nKey="modlog">#</T></Link></li> </li>
<li className="list-inline-item badge badge-secondary">
<T
i18nKey="number_of_subscribers"
interpolation={{ count: community.number_of_subscribers }}
>
#
</T>
</li>
<li className="list-inline-item badge badge-secondary">
<T
i18nKey="number_of_posts"
interpolation={{ count: community.number_of_posts }}
>
#
</T>
</li>
<li className="list-inline-item badge badge-secondary">
<T
i18nKey="number_of_comments"
interpolation={{ count: community.number_of_comments }}
>
#
</T>
</li>
<li className="list-inline-item">
<Link
className="badge badge-secondary"
to={`/modlog/community/${this.props.community.id}`}
>
<T i18nKey="modlog">#</T>
</Link>
</li>
</ul> </ul>
<ul class="list-inline small"> <ul class="list-inline small">
<li class="list-inline-item">{i18n.t('mods')}: </li> <li class="list-inline-item">{i18n.t('mods')}: </li>
{this.props.moderators.map(mod => {this.props.moderators.map(mod => (
<li class="list-inline-item"><Link class="text-info" to={`/u/${mod.user_name}`}>{mod.user_name}</Link></li> <li class="list-inline-item">
)} <Link class="text-info" to={`/u/${mod.user_name}`}>
{mod.user_name}
</Link>
</li>
))}
</ul> </ul>
<Link class={`btn btn-sm btn-secondary btn-block mb-3 ${(community.deleted || community.removed) && 'no-click'}`} <Link
to={`/create_post?community=${community.name}`}><T i18nKey="create_a_post">#</T></Link> class={`btn btn-sm btn-secondary btn-block mb-3 ${(community.deleted ||
community.removed) &&
'no-click'}`}
to={`/create_post?community=${community.name}`}
>
<T i18nKey="create_a_post">#</T>
</Link>
<div> <div>
{community.subscribed {community.subscribed ? (
? <button class="btn btn-sm btn-secondary btn-block" onClick={linkEvent(community.id, this.handleUnsubscribe)}><T i18nKey="unsubscribe">#</T></button> <button
: <button class="btn btn-sm btn-secondary btn-block" onClick={linkEvent(community.id, this.handleSubscribe)}><T i18nKey="subscribe">#</T></button> class="btn btn-sm btn-secondary btn-block"
} onClick={linkEvent(community.id, this.handleUnsubscribe)}
>
<T i18nKey="unsubscribe">#</T>
</button>
) : (
<button
class="btn btn-sm btn-secondary btn-block"
onClick={linkEvent(community.id, this.handleSubscribe)}
>
<T i18nKey="subscribe">#</T>
</button>
)}
</div> </div>
</div> </div>
</div> </div>
{community.description && {community.description && (
<div class="card border-secondary"> <div class="card border-secondary">
<div class="card-body"> <div class="card-body">
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(community.description)} /> <div
className="md-div"
dangerouslySetInnerHTML={mdToHtml(community.description)}
/>
</div> </div>
</div> </div>
} )}
</div> </div>
); );
} }
@ -173,7 +272,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
handleUnsubscribe(communityId: number) { handleUnsubscribe(communityId: number) {
let form: FollowCommunityForm = { let form: FollowCommunityForm = {
community_id: communityId, community_id: communityId,
follow: false follow: false,
}; };
WebSocketService.Instance.followCommunity(form); WebSocketService.Instance.followCommunity(form);
} }
@ -181,7 +280,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
handleSubscribe(communityId: number) { handleSubscribe(communityId: number) {
let form: FollowCommunityForm = { let form: FollowCommunityForm = {
community_id: communityId, community_id: communityId,
follow: true follow: true,
}; };
WebSocketService.Instance.followCommunity(form); WebSocketService.Instance.followCommunity(form);
} }
@ -191,11 +290,19 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
} }
get canMod(): boolean { get canMod(): boolean {
return UserService.Instance.user && this.props.moderators.map(m => m.user_id).includes(UserService.Instance.user.id); return (
UserService.Instance.user &&
this.props.moderators
.map(m => m.user_id)
.includes(UserService.Instance.user.id)
);
} }
get canAdmin(): boolean { get canAdmin(): boolean {
return UserService.Instance.user && this.props.admins.map(a => a.id).includes(UserService.Instance.user.id); return (
UserService.Instance.user &&
this.props.admins.map(a => a.id).includes(UserService.Instance.user.id)
);
} }
handleModRemoveShow(i: Sidebar) { handleModRemoveShow(i: Sidebar) {

View file

@ -19,10 +19,10 @@ interface SiteFormState {
export class SiteForm extends Component<SiteFormProps, SiteFormState> { export class SiteForm extends Component<SiteFormProps, SiteFormState> {
private emptyState: SiteFormState = { private emptyState: SiteFormState = {
siteForm: { siteForm: {
name: null name: null,
}, },
loading: false loading: false,
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -31,7 +31,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
this.state.siteForm = { this.state.siteForm = {
name: this.props.site.name, name: this.props.site.name,
description: this.props.site.description, description: this.props.site.description,
} };
} }
} }
@ -42,26 +42,63 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
render() { render() {
return ( return (
<form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}> <form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
<h5>{`${this.props.site ? capitalizeFirstLetter(i18n.t('edit')) : capitalizeFirstLetter(i18n.t('name'))} ${i18n.t('your_site')}`}</h5> <h5>{`${
this.props.site
? capitalizeFirstLetter(i18n.t('edit'))
: capitalizeFirstLetter(i18n.t('name'))
} ${i18n.t('your_site')}`}</h5>
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label"><T i18nKey="name">#</T></label> <label class="col-12 col-form-label">
<T i18nKey="name">#</T>
</label>
<div class="col-12"> <div class="col-12">
<input type="text" class="form-control" value={this.state.siteForm.name} onInput={linkEvent(this, this.handleSiteNameChange)} required minLength={3} maxLength={20} /> <input
type="text"
class="form-control"
value={this.state.siteForm.name}
onInput={linkEvent(this, this.handleSiteNameChange)}
required
minLength={3}
maxLength={20}
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label"><T i18nKey="sidebar">#</T></label> <label class="col-12 col-form-label">
<T i18nKey="sidebar">#</T>
</label>
<div class="col-12"> <div class="col-12">
<textarea value={this.state.siteForm.description} onInput={linkEvent(this, this.handleSiteDescriptionChange)} class="form-control" rows={3} maxLength={10000} /> <textarea
value={this.state.siteForm.description}
onInput={linkEvent(this, this.handleSiteDescriptionChange)}
class="form-control"
rows={3}
maxLength={10000}
/>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-12"> <div class="col-12">
<button type="submit" class="btn btn-secondary mr-2"> <button type="submit" class="btn btn-secondary mr-2">
{this.state.loading ? {this.state.loading ? (
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : <svg class="icon icon-spinner spin">
this.props.site ? capitalizeFirstLetter(i18n.t('save')) : capitalizeFirstLetter(i18n.t('create'))}</button> <use xlinkHref="#icon-spinner"></use>
{this.props.site && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleCancel)}><T i18nKey="cancel">#</T></button>} </svg>
) : this.props.site ? (
capitalizeFirstLetter(i18n.t('save'))
) : (
capitalizeFirstLetter(i18n.t('create'))
)}
</button>
{this.props.site && (
<button
type="button"
class="btn btn-secondary"
onClick={linkEvent(this, this.handleCancel)}
>
<T i18nKey="cancel">#</T>
</button>
)}
</div> </div>
</div> </div>
</form> </form>

View file

@ -3,23 +3,21 @@ import { WebSocketService } from '../services';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
let general = [ let general = ['riccardo', 'NotTooHighToHack'];
"riccardo","NotTooHighToHack",
];
// let highlighted = []; // let highlighted = [];
// let silver = []; // let silver = [];
// let gold = []; // let gold = [];
// let latinum = []; // let latinum = [];
export class Sponsors extends Component<any, any> { export class Sponsors extends Component<any, any> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
} }
componentDidMount() { componentDidMount() {
document.title = `${i18n.t('sponsors')} - ${WebSocketService.Instance.site.name}`; document.title = `${i18n.t('sponsors')} - ${
WebSocketService.Instance.site.name
}`;
} }
render() { render() {
@ -31,62 +29,85 @@ export class Sponsors extends Component<any, any> {
<hr /> <hr />
{this.bitcoin()} {this.bitcoin()}
</div> </div>
) );
} }
topMessage() { topMessage() {
return ( return (
<div> <div>
<h5><T i18nKey="sponsors_of_lemmy">#</T></h5> <h5>
<T i18nKey="sponsors_of_lemmy">#</T>
</h5>
<p> <p>
<T i18nKey="sponsor_message">#<a href="https://github.com/dessalines/lemmy">#</a></T> <T i18nKey="sponsor_message">
#<a href="https://github.com/dessalines/lemmy">#</a>
</T>
</p> </p>
<a class="btn btn-secondary" href="https://www.patreon.com/dessalines"><T i18nKey="support_on_patreon">#</T></a> <a class="btn btn-secondary" href="https://www.patreon.com/dessalines">
<T i18nKey="support_on_patreon">#</T>
</a>
</div> </div>
) );
} }
sponsors() { sponsors() {
return ( return (
<div class="container"> <div class="container">
<h5><T i18nKey="sponsors">#</T></h5> <h5>
<p><T i18nKey="general_sponsors">#</T></p> <T i18nKey="sponsors">#</T>
</h5>
<p>
<T i18nKey="general_sponsors">#</T>
</p>
<div class="row card-columns"> <div class="row card-columns">
{general.map(s => {general.map(s => (
<div class="card col-12 col-md-2"> <div class="card col-12 col-md-2">
<div>{s}</div> <div>{s}</div>
</div> </div>
)} ))}
</div> </div>
</div> </div>
) );
} }
bitcoin() { bitcoin() {
return ( return (
<div> <div>
<h5><T i18nKey="crypto">#</T></h5> <h5>
<T i18nKey="crypto">#</T>
</h5>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover text-center"> <table class="table table-hover text-center">
<tbody> <tbody>
<tr> <tr>
<td><T i18nKey="bitcoin">#</T></td>
<td><code>1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK</code></td>
</tr>
<tr>
<td><T i18nKey="ethereum">#</T></td>
<td><code>0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01</code></td>
</tr>
<tr>
<td><T i18nKey="monero">#</T></td>
<td> <td>
<code>41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV</code> <T i18nKey="bitcoin">#</T>
</td>
<td>
<code>1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK</code>
</td>
</tr>
<tr>
<td>
<T i18nKey="ethereum">#</T>
</td>
<td>
<code>0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01</code>
</td>
</tr>
<tr>
<td>
<T i18nKey="monero">#</T>
</td>
<td>
<code>
41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV
</code>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
) );
} }
} }

View file

@ -1,14 +1,19 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
export class Symbols extends Component<any, any> { export class Symbols extends Component<any, any> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
} }
render() { render() {
return ( return (
<svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"> <svg
aria-hidden="true"
style="position: absolute; width: 0; height: 0; overflow: hidden;"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs> <defs>
<symbol id="icon-arrow-down" viewBox="0 0 26 28"> <symbol id="icon-arrow-down" viewBox="0 0 26 28">
<title>arrow-down</title> <title>arrow-down</title>
@ -22,12 +27,18 @@ export class Symbols extends Component<any, any> {
<title>mail</title> <title>mail</title>
<path d="M28 5h-24c-2.209 0-4 1.792-4 4v13c0 2.209 1.791 4 4 4h24c2.209 0 4-1.791 4-4v-13c0-2.208-1.791-4-4-4zM2 10.25l6.999 5.25-6.999 5.25v-10.5zM30 22c0 1.104-0.898 2-2 2h-24c-1.103 0-2-0.896-2-2l7.832-5.875 4.368 3.277c0.533 0.398 1.166 0.6 1.8 0.6 0.633 0 1.266-0.201 1.799-0.6l4.369-3.277 7.832 5.875zM30 20.75l-7-5.25 7-5.25v10.5zM17.199 18.602c-0.349 0.262-0.763 0.4-1.199 0.4s-0.851-0.139-1.2-0.4l-12.8-9.602c0-1.103 0.897-2 2-2h24c1.102 0 2 0.897 2 2l-12.801 9.602z"></path> <path d="M28 5h-24c-2.209 0-4 1.792-4 4v13c0 2.209 1.791 4 4 4h24c2.209 0 4-1.791 4-4v-13c0-2.208-1.791-4-4-4zM2 10.25l6.999 5.25-6.999 5.25v-10.5zM30 22c0 1.104-0.898 2-2 2h-24c-1.103 0-2-0.896-2-2l7.832-5.875 4.368 3.277c0.533 0.398 1.166 0.6 1.8 0.6 0.633 0 1.266-0.201 1.799-0.6l4.369-3.277 7.832 5.875zM30 20.75l-7-5.25 7-5.25v10.5zM17.199 18.602c-0.349 0.262-0.763 0.4-1.199 0.4s-0.851-0.139-1.2-0.4l-12.8-9.602c0-1.103 0.897-2 2-2h24c1.102 0 2 0.897 2 2l-12.801 9.602z"></path>
</symbol> </symbol>
<symbol id="icon-mouse" version="1.1" x="0px" y="0px" <symbol
viewBox="0 0 1024 1024"> id="icon-mouse"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 1024 1024"
>
<g <g
id="layer1" id="layer1"
transform="translate(0,-26.066658)" transform="translate(0,-26.066658)"
style="display:inline"> style="display:inline"
>
<path <path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 167.03908,270.78735 c -0.94784,-0.002 -1.8939,0.004 -2.83789,0.0215 -4.31538,0.0778 -8.58934,0.3593 -12.8125,0.8457 -33.78522,3.89116 -64.215716,21.86394 -82.871086,53.27344 -18.27982,30.77718 -22.77749,64.66635 -13.46094,96.06837 9.31655,31.40203 31.88488,59.93174 65.296886,82.5332 0.20163,0.13618 0.40678,0.26709 0.61523,0.39258 28.65434,17.27768 57.18167,28.93179 87.74218,34.95508 -0.74566,12.61339 -0.72532,25.5717 0.082,38.84375 2.43989,40.10943 16.60718,77.03742 38.0957,109.67187 l -77.00781,31.4375 c -8.30605,3.25932 -12.34178,12.68234 -8.96967,20.94324 3.37211,8.2609 12.84919,12.16798 21.06342,8.68371 l 84.69727,-34.57617 c 15.70675,18.72702 33.75346,35.68305 53.12109,50.57032 0.74013,0.56891 1.4904,1.12236 2.23437,1.68554 l -49.61132,65.69141 c -5.45446,7.0474 -4.10058,17.19288 3.01098,22.5634 7.11156,5.37052 17.24028,3.89649 22.52612,-3.27824 l 50.38672,-66.71876 c 27.68572,17.53469 57.07524,31.20388 86.07227,40.25196 14.88153,27.28008 43.96965,44.64648 77.58789,44.64648 33.93762,0 63.04252,-18.68693 77.80082,-45.4375 28.7072,-9.21295 57.7527,-22.93196 85.1484,-40.40234 l 51.0977,67.66016 c 5.2858,7.17473 15.4145,8.64876 22.5261,3.27824 7.1115,-5.37052 8.4654,-15.516 3.011,-22.5634 l -50.3614,-66.68555 c 0.334,-0.25394 0.6727,-0.50077 1.0059,-0.75586 19.1376,-14.64919 37.0259,-31.28581 52.7031,-49.63476 l 82.5625,33.70507 c 8.2143,3.48427 17.6913,-0.42281 21.0634,-8.68371 3.3722,-8.2609 -0.6636,-17.68392 -8.9696,-20.94324 l -74.5391,-30.42773 c 22.1722,-32.82971 37.0383,-70.03397 40.1426,-110.46094 1.0253,-13.35251 1.2292,-26.42535 0.6387,-39.17578 30.3557,-6.05408 58.7164,-17.66833 87.2011,-34.84375 0.2085,-0.12549 0.4136,-0.2564 0.6153,-0.39258 33.412,-22.60147 55.9803,-51.13117 65.2968,-82.5332 9.3166,-31.40202 4.8189,-65.29118 -13.4609,-96.06837 -18.6553,-31.40951 -49.0859,-49.38228 -82.8711,-53.27344 -4.2231,-0.4864 -8.4971,-0.76791 -12.8125,-0.8457 -30.2077,-0.54448 -62.4407,8.82427 -93.4316,26.71484 -22.7976,13.16063 -43.3521,33.31423 -59.4375,55.30469 -44.9968,-25.75094 -103.5444,-40.25065 -175.4785,-41.43945 -6.4522,-0.10663 -13.0125,-0.10696 -19.67974,0.002 -80.18875,1.30929 -144.38284,16.5086 -192.87109,43.9922 -0.11914,-0.19111 -0.24287,-0.37932 -0.37109,-0.56446 -16.29,-22.764 -37.41085,-43.73706 -60.89649,-57.29493 -30.02247,-17.33149 -61.21051,-26.66489 -90.59375,-26.73633 z" d="m 167.03908,270.78735 c -0.94784,-0.002 -1.8939,0.004 -2.83789,0.0215 -4.31538,0.0778 -8.58934,0.3593 -12.8125,0.8457 -33.78522,3.89116 -64.215716,21.86394 -82.871086,53.27344 -18.27982,30.77718 -22.77749,64.66635 -13.46094,96.06837 9.31655,31.40203 31.88488,59.93174 65.296886,82.5332 0.20163,0.13618 0.40678,0.26709 0.61523,0.39258 28.65434,17.27768 57.18167,28.93179 87.74218,34.95508 -0.74566,12.61339 -0.72532,25.5717 0.082,38.84375 2.43989,40.10943 16.60718,77.03742 38.0957,109.67187 l -77.00781,31.4375 c -8.30605,3.25932 -12.34178,12.68234 -8.96967,20.94324 3.37211,8.2609 12.84919,12.16798 21.06342,8.68371 l 84.69727,-34.57617 c 15.70675,18.72702 33.75346,35.68305 53.12109,50.57032 0.74013,0.56891 1.4904,1.12236 2.23437,1.68554 l -49.61132,65.69141 c -5.45446,7.0474 -4.10058,17.19288 3.01098,22.5634 7.11156,5.37052 17.24028,3.89649 22.52612,-3.27824 l 50.38672,-66.71876 c 27.68572,17.53469 57.07524,31.20388 86.07227,40.25196 14.88153,27.28008 43.96965,44.64648 77.58789,44.64648 33.93762,0 63.04252,-18.68693 77.80082,-45.4375 28.7072,-9.21295 57.7527,-22.93196 85.1484,-40.40234 l 51.0977,67.66016 c 5.2858,7.17473 15.4145,8.64876 22.5261,3.27824 7.1115,-5.37052 8.4654,-15.516 3.011,-22.5634 l -50.3614,-66.68555 c 0.334,-0.25394 0.6727,-0.50077 1.0059,-0.75586 19.1376,-14.64919 37.0259,-31.28581 52.7031,-49.63476 l 82.5625,33.70507 c 8.2143,3.48427 17.6913,-0.42281 21.0634,-8.68371 3.3722,-8.2609 -0.6636,-17.68392 -8.9696,-20.94324 l -74.5391,-30.42773 c 22.1722,-32.82971 37.0383,-70.03397 40.1426,-110.46094 1.0253,-13.35251 1.2292,-26.42535 0.6387,-39.17578 30.3557,-6.05408 58.7164,-17.66833 87.2011,-34.84375 0.2085,-0.12549 0.4136,-0.2564 0.6153,-0.39258 33.412,-22.60147 55.9803,-51.13117 65.2968,-82.5332 9.3166,-31.40202 4.8189,-65.29118 -13.4609,-96.06837 -18.6553,-31.40951 -49.0859,-49.38228 -82.8711,-53.27344 -4.2231,-0.4864 -8.4971,-0.76791 -12.8125,-0.8457 -30.2077,-0.54448 -62.4407,8.82427 -93.4316,26.71484 -22.7976,13.16063 -43.3521,33.31423 -59.4375,55.30469 -44.9968,-25.75094 -103.5444,-40.25065 -175.4785,-41.43945 -6.4522,-0.10663 -13.0125,-0.10696 -19.67974,0.002 -80.18875,1.30929 -144.38284,16.5086 -192.87109,43.9922 -0.11914,-0.19111 -0.24287,-0.37932 -0.37109,-0.56446 -16.29,-22.764 -37.41085,-43.73706 -60.89649,-57.29493 -30.02247,-17.33149 -61.21051,-26.66489 -90.59375,-26.73633 z"

View file

@ -1,10 +1,32 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Subscription } from "rxjs"; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Post, Comment, CommunityUser, GetUserDetailsForm, SortType, UserDetailsResponse, UserView, CommentResponse, UserSettingsForm, LoginResponse, BanUserResponse, AddAdminResponse, DeleteAccountForm } from '../interfaces'; import {
UserOperation,
Post,
Comment,
CommunityUser,
GetUserDetailsForm,
SortType,
UserDetailsResponse,
UserView,
CommentResponse,
UserSettingsForm,
LoginResponse,
BanUserResponse,
AddAdminResponse,
DeleteAccountForm,
} from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { msgOp, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter, themes, setTheme } from '../utils'; import {
msgOp,
fetchLimit,
routeSortTypeToEnum,
capitalizeFirstLetter,
themes,
setTheme,
} from '../utils';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
import { CommentNodes } from './comment-nodes'; import { CommentNodes } from './comment-nodes';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
@ -12,7 +34,10 @@ import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
enum View { enum View {
Overview, Comments, Posts, Saved Overview,
Comments,
Posts,
Saved,
} }
interface UserState { interface UserState {
@ -37,7 +62,6 @@ interface UserState {
} }
export class User extends Component<any, UserState> { export class User extends Component<any, UserState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: UserState = { private emptyState: UserState = {
user: { user: {
@ -72,8 +96,8 @@ export class User extends Component<any, UserState> {
deleteAccountShowConfirm: false, deleteAccountShowConfirm: false,
deleteAccountForm: { deleteAccountForm: {
password: null, password: null,
} },
} };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -84,10 +108,17 @@ export class User extends Component<any, UserState> {
this.state.username = this.props.match.params.username; this.state.username = this.props.match.params.username;
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe( .subscribe(
(msg) => this.parseMessage(msg), msg => this.parseMessage(msg),
(err) => console.error(err), err => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
@ -95,23 +126,26 @@ export class User extends Component<any, UserState> {
} }
get isCurrentUser() { get isCurrentUser() {
return UserService.Instance.user && UserService.Instance.user.id == this.state.user.id; return (
UserService.Instance.user &&
UserService.Instance.user.id == this.state.user.id
);
} }
getViewFromProps(props: any): View { getViewFromProps(props: any): View {
return (props.match.params.view) ? return props.match.params.view
View[capitalizeFirstLetter(props.match.params.view)] : ? View[capitalizeFirstLetter(props.match.params.view)]
View.Overview; : View.Overview;
} }
getSortTypeFromProps(props: any): SortType { getSortTypeFromProps(props: any): SortType {
return (props.match.params.sort) ? return props.match.params.sort
routeSortTypeToEnum(props.match.params.sort) : ? routeSortTypeToEnum(props.match.params.sort)
SortType.New; : SortType.New;
} }
getPageFromProps(props: any): number { getPageFromProps(props: any): number {
return (props.match.params.page) ? Number(props.match.params.page) : 1; return props.match.params.page ? Number(props.match.params.page) : 1;
} }
componentWillUnmount() { componentWillUnmount() {
@ -132,68 +166,98 @@ export class User extends Component<any, UserState> {
render() { render() {
return ( return (
<div class="container"> <div class="container">
{this.state.loading ? {this.state.loading ? (
<h5><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h5> : <h5>
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
</h5>
) : (
<div class="row"> <div class="row">
<div class="col-12 col-md-8"> <div class="col-12 col-md-8">
<h5>/u/{this.state.user.name}</h5> <h5>/u/{this.state.user.name}</h5>
{this.selects()} {this.selects()}
{this.state.view == View.Overview && {this.state.view == View.Overview && this.overview()}
this.overview() {this.state.view == View.Comments && this.comments()}
} {this.state.view == View.Posts && this.posts()}
{this.state.view == View.Comments && {this.state.view == View.Saved && this.overview()}
this.comments()
}
{this.state.view == View.Posts &&
this.posts()
}
{this.state.view == View.Saved &&
this.overview()
}
{this.paginator()} {this.paginator()}
</div> </div>
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
{this.userInfo()} {this.userInfo()}
{this.isCurrentUser && {this.isCurrentUser && this.userSettings()}
this.userSettings()
}
{this.moderates()} {this.moderates()}
{this.follows()} {this.follows()}
</div> </div>
</div> </div>
} )}
</div> </div>
) );
} }
selects() { selects() {
return ( return (
<div className="mb-2"> <div className="mb-2">
<select value={this.state.view} onChange={linkEvent(this, this.handleViewChange)} class="custom-select custom-select-sm w-auto"> <select
<option disabled><T i18nKey="view">#</T></option> value={this.state.view}
<option value={View.Overview}><T i18nKey="overview">#</T></option> onChange={linkEvent(this, this.handleViewChange)}
<option value={View.Comments}><T i18nKey="comments">#</T></option> class="custom-select custom-select-sm w-auto"
<option value={View.Posts}><T i18nKey="posts">#</T></option> >
<option value={View.Saved}><T i18nKey="saved">#</T></option> <option disabled>
<T i18nKey="view">#</T>
</option>
<option value={View.Overview}>
<T i18nKey="overview">#</T>
</option>
<option value={View.Comments}>
<T i18nKey="comments">#</T>
</option>
<option value={View.Posts}>
<T i18nKey="posts">#</T>
</option>
<option value={View.Saved}>
<T i18nKey="saved">#</T>
</option>
</select> </select>
<select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2"> <select
<option disabled><T i18nKey="sort_type">#</T></option> value={this.state.sort}
<option value={SortType.New}><T i18nKey="new">#</T></option> onChange={linkEvent(this, this.handleSortChange)}
<option value={SortType.TopDay}><T i18nKey="top_day">#</T></option> class="custom-select custom-select-sm w-auto ml-2"
<option value={SortType.TopWeek}><T i18nKey="week">#</T></option> >
<option value={SortType.TopMonth}><T i18nKey="month">#</T></option> <option disabled>
<option value={SortType.TopYear}><T i18nKey="year">#</T></option> <T i18nKey="sort_type">#</T>
<option value={SortType.TopAll}><T i18nKey="all">#</T></option> </option>
<option value={SortType.New}>
<T i18nKey="new">#</T>
</option>
<option value={SortType.TopDay}>
<T i18nKey="top_day">#</T>
</option>
<option value={SortType.TopWeek}>
<T i18nKey="week">#</T>
</option>
<option value={SortType.TopMonth}>
<T i18nKey="month">#</T>
</option>
<option value={SortType.TopYear}>
<T i18nKey="year">#</T>
</option>
<option value={SortType.TopAll}>
<T i18nKey="all">#</T>
</option>
</select> </select>
</div> </div>
) );
} }
overview() { overview() {
let combined: Array<{type_: string, data: Comment | Post}> = []; let combined: Array<{ type_: string; data: Comment | Post }> = [];
let comments = this.state.comments.map(e => {return {type_: "comments", data: e}}); let comments = this.state.comments.map(e => {
let posts = this.state.posts.map(e => {return {type_: "posts", data: e}}); return { type_: 'comments', data: e };
});
let posts = this.state.posts.map(e => {
return { type_: 'posts', data: e };
});
combined.push(...comments); combined.push(...comments);
combined.push(...posts); combined.push(...posts);
@ -207,35 +271,38 @@ export class User extends Component<any, UserState> {
return ( return (
<div> <div>
{combined.map(i => {combined.map(i => (
<div> <div>
{i.type_ == "posts" {i.type_ == 'posts' ? (
? <PostListing <PostListing
post={i.data as Post} post={i.data as Post}
admins={this.state.admins} admins={this.state.admins}
showCommunity showCommunity
viewOnly /> viewOnly
: />
) : (
<CommentNodes <CommentNodes
nodes={[{ comment: i.data as Comment }]} nodes={[{ comment: i.data as Comment }]}
admins={this.state.admins} admins={this.state.admins}
noIndent /> noIndent
} />
)}
</div> </div>
) ))}
}
</div> </div>
) );
} }
comments() { comments() {
return ( return (
<div> <div>
{this.state.comments.map(comment => {this.state.comments.map(comment => (
<CommentNodes nodes={[{comment: comment}]} <CommentNodes
nodes={[{ comment: comment }]}
admins={this.state.admins} admins={this.state.admins}
noIndent /> noIndent
)} />
))}
</div> </div>
); );
} }
@ -243,13 +310,14 @@ export class User extends Component<any, UserState> {
posts() { posts() {
return ( return (
<div> <div>
{this.state.posts.map(post => {this.state.posts.map(post => (
<PostListing <PostListing
post={post} post={post}
admins={this.state.admins} admins={this.state.admins}
showCommunity showCommunity
viewOnly /> viewOnly
)} />
))}
</div> </div>
); );
} }
@ -263,28 +331,60 @@ export class User extends Component<any, UserState> {
<h5> <h5>
<ul class="list-inline mb-0"> <ul class="list-inline mb-0">
<li className="list-inline-item">{user.name}</li> <li className="list-inline-item">{user.name}</li>
{user.banned && {user.banned && (
<li className="list-inline-item badge badge-danger"><T i18nKey="banned">#</T></li> <li className="list-inline-item badge badge-danger">
} <T i18nKey="banned">#</T>
</li>
)}
</ul> </ul>
</h5> </h5>
<div>{i18n.t('joined')} <MomentTime data={user} /></div> <div>
{i18n.t('joined')} <MomentTime data={user} />
</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-bordered table-sm mt-2 mb-0"> <table class="table table-bordered table-sm mt-2 mb-0">
<tr> <tr>
<td><T i18nKey="number_of_points" interpolation={{count: user.post_score}}>#</T></td> <td>
<td><T i18nKey="number_of_posts" interpolation={{count: user.number_of_posts}}>#</T></td> <T
i18nKey="number_of_points"
interpolation={{ count: user.post_score }}
>
#
</T>
</td>
<td>
<T
i18nKey="number_of_posts"
interpolation={{ count: user.number_of_posts }}
>
#
</T>
</td>
</tr> </tr>
<tr> <tr>
<td><T i18nKey="number_of_points" interpolation={{count: user.comment_score}}>#</T></td> <td>
<td><T i18nKey="number_of_comments" interpolation={{count: user.number_of_comments}}>#</T></td> <T
i18nKey="number_of_points"
interpolation={{ count: user.comment_score }}
>
#
</T>
</td>
<td>
<T
i18nKey="number_of_comments"
interpolation={{ count: user.number_of_comments }}
>
#
</T>
</td>
</tr> </tr>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
) );
} }
userSettings() { userSettings() {
@ -292,95 +392,184 @@ export class User extends Component<any, UserState> {
<div> <div>
<div class="card border-secondary mb-3"> <div class="card border-secondary mb-3">
<div class="card-body"> <div class="card-body">
<h5><T i18nKey="settings">#</T></h5> <h5>
<T i18nKey="settings">#</T>
</h5>
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}> <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
<div class="form-group"> <div class="form-group">
<div class="col-12"> <div class="col-12">
<label><T i18nKey="theme">#</T></label> <label>
<select value={this.state.userSettingsForm.theme} onChange={linkEvent(this, this.handleUserSettingsThemeChange)} class="ml-2 custom-select custom-select-sm w-auto"> <T i18nKey="theme">#</T>
<option disabled><T i18nKey="theme">#</T></option> </label>
{themes.map(theme => <select
<option value={theme}>{theme}</option> value={this.state.userSettingsForm.theme}
onChange={linkEvent(
this,
this.handleUserSettingsThemeChange
)} )}
class="ml-2 custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="theme">#</T>
</option>
{themes.map(theme => (
<option value={theme}>{theme}</option>
))}
</select> </select>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-12"> <div class="col-12">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" checked={this.state.userSettingsForm.show_nsfw} onChange={linkEvent(this, this.handleUserSettingsShowNsfwChange)}/> <input
<label class="form-check-label"><T i18nKey="show_nsfw">#</T></label> class="form-check-input"
type="checkbox"
checked={this.state.userSettingsForm.show_nsfw}
onChange={linkEvent(
this,
this.handleUserSettingsShowNsfwChange
)}
/>
<label class="form-check-label">
<T i18nKey="show_nsfw">#</T>
</label>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row mb-0"> <div class="form-group row mb-0">
<div class="col-12"> <div class="col-12">
<button type="submit" class="btn btn-secondary mr-4">{this.state.userSettingsLoading ? <button type="submit" class="btn btn-secondary mr-4">
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : capitalizeFirstLetter(i18n.t('save'))}</button> {this.state.userSettingsLoading ? (
<button class="btn btn-danger" onClick={linkEvent(this, this.handleDeleteAccountShowConfirmToggle)}><T i18nKey="delete_account">#</T></button> <svg class="icon icon-spinner spin">
{this.state.deleteAccountShowConfirm && <use xlinkHref="#icon-spinner"></use>
</svg>
) : (
capitalizeFirstLetter(i18n.t('save'))
)}
</button>
<button
class="btn btn-danger"
onClick={linkEvent(
this,
this.handleDeleteAccountShowConfirmToggle
)}
>
<T i18nKey="delete_account">#</T>
</button>
{this.state.deleteAccountShowConfirm && (
<> <>
<div class="my-2 alert alert-danger" role="alert"><T i18nKey="delete_account_confirm">#</T></div> <div class="my-2 alert alert-danger" role="alert">
<input type="password" value={this.state.deleteAccountForm.password} onInput={linkEvent(this, this.handleDeleteAccountPasswordChange)} class="form-control my-2" /> <T i18nKey="delete_account_confirm">#</T>
<button class="btn btn-danger mr-4" disabled={!this.state.deleteAccountForm.password} onClick={linkEvent(this, this.handleDeleteAccount)}>{this.state.deleteAccountLoading ? </div>
<svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> : capitalizeFirstLetter(i18n.t('delete'))}</button> <input
<button class="btn btn-secondary" onClick={linkEvent(this, this.handleDeleteAccountShowConfirmToggle)}><T i18nKey="cancel">#</T></button> type="password"
value={this.state.deleteAccountForm.password}
onInput={linkEvent(
this,
this.handleDeleteAccountPasswordChange
)}
class="form-control my-2"
/>
<button
class="btn btn-danger mr-4"
disabled={!this.state.deleteAccountForm.password}
onClick={linkEvent(this, this.handleDeleteAccount)}
>
{this.state.deleteAccountLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
capitalizeFirstLetter(i18n.t('delete'))
)}
</button>
<button
class="btn btn-secondary"
onClick={linkEvent(
this,
this.handleDeleteAccountShowConfirmToggle
)}
>
<T i18nKey="cancel">#</T>
</button>
</> </>
} )}
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
) );
} }
moderates() { moderates() {
return ( return (
<div> <div>
{this.state.moderates.length > 0 && {this.state.moderates.length > 0 && (
<div class="card border-secondary mb-3"> <div class="card border-secondary mb-3">
<div class="card-body"> <div class="card-body">
<h5><T i18nKey="moderates">#</T></h5> <h5>
<T i18nKey="moderates">#</T>
</h5>
<ul class="list-unstyled mb-0"> <ul class="list-unstyled mb-0">
{this.state.moderates.map(community => {this.state.moderates.map(community => (
<li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li> <li>
)} <Link to={`/c/${community.community_name}`}>
{community.community_name}
</Link>
</li>
))}
</ul> </ul>
</div> </div>
</div> </div>
} )}
</div> </div>
) );
} }
follows() { follows() {
return ( return (
<div> <div>
{this.state.follows.length > 0 && {this.state.follows.length > 0 && (
<div class="card border-secondary mb-3"> <div class="card border-secondary mb-3">
<div class="card-body"> <div class="card-body">
<h5><T i18nKey="subscribed">#</T></h5> <h5>
<T i18nKey="subscribed">#</T>
</h5>
<ul class="list-unstyled mb-0"> <ul class="list-unstyled mb-0">
{this.state.follows.map(community => {this.state.follows.map(community => (
<li><Link to={`/c/${community.community_name}`}>{community.community_name}</Link></li> <li>
)} <Link to={`/c/${community.community_name}`}>
{community.community_name}
</Link>
</li>
))}
</ul> </ul>
</div> </div>
</div> </div>
} )}
</div> </div>
) );
} }
paginator() { paginator() {
return ( return (
<div class="my-2"> <div class="my-2">
{this.state.page > 1 && {this.state.page > 1 && (
<button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button> <button
} class="btn btn-sm btn-secondary mr-1"
<button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button> onClick={linkEvent(this, this.prevPage)}
>
<T i18nKey="prev">#</T>
</button>
)}
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
<T i18nKey="next">#</T>
</button>
</div> </div>
); );
} }
@ -388,7 +577,9 @@ export class User extends Component<any, UserState> {
updateUrl() { updateUrl() {
let viewStr = View[this.state.view].toLowerCase(); let viewStr = View[this.state.view].toLowerCase();
let sortStr = SortType[this.state.sort].toLowerCase(); let sortStr = SortType[this.state.sort].toLowerCase();
this.props.history.push(`/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`); this.props.history.push(
`/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}`
);
} }
nextPage(i: User) { nextPage(i: User) {
@ -489,8 +680,11 @@ export class User extends Component<any, UserState> {
this.state.admins = res.admins; this.state.admins = res.admins;
this.state.loading = false; this.state.loading = false;
if (this.isCurrentUser) { if (this.isCurrentUser) {
this.state.userSettingsForm.show_nsfw = UserService.Instance.user.show_nsfw; this.state.userSettingsForm.show_nsfw =
this.state.userSettingsForm.theme = UserService.Instance.user.theme ? UserService.Instance.user.theme : 'darkly'; UserService.Instance.user.show_nsfw;
this.state.userSettingsForm.theme = UserService.Instance.user.theme
? UserService.Instance.user.theme
: 'darkly';
} }
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`; document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
window.scrollTo(0, 0); window.scrollTo(0, 0);
@ -520,19 +714,22 @@ export class User extends Component<any, UserState> {
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let res: CommentResponse = msg; let res: CommentResponse = msg;
let found: Comment = this.state.comments.find(c => c.id === res.comment.id); let found: Comment = this.state.comments.find(
c => c.id === res.comment.id
);
found.score = res.comment.score; found.score = res.comment.score;
found.upvotes = res.comment.upvotes; found.upvotes = res.comment.upvotes;
found.downvotes = res.comment.downvotes; found.downvotes = res.comment.downvotes;
if (res.comment.my_vote !== null) if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
found.my_vote = res.comment.my_vote;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BanUser) { } else if (op == UserOperation.BanUser) {
let res: BanUserResponse = msg; let res: BanUserResponse = msg;
this.state.comments.filter(c => c.creator_id == res.user.id) this.state.comments
.forEach(c => c.banned = res.banned); .filter(c => c.creator_id == res.user.id)
this.state.posts.filter(c => c.creator_id == res.user.id) .forEach(c => (c.banned = res.banned));
.forEach(c => c.banned = res.banned); this.state.posts
.filter(c => c.creator_id == res.user.id)
.forEach(c => (c.banned = res.banned));
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.AddAdmin) { } else if (op == UserOperation.AddAdmin) {
let res: AddAdminResponse = msg; let res: AddAdminResponse = msg;
@ -552,4 +749,3 @@ export class User extends Component<any, UserState> {
} }
} }
} }

6
ui/src/env.ts vendored
View file

@ -1,4 +1,6 @@
let host = `${window.location.hostname}`; let host = `${window.location.hostname}`;
let port = `${window.location.port == "4444" ? '8536' : window.location.port}`; let port = `${window.location.port == '4444' ? '8536' : window.location.port}`;
let endpoint = `${host}:${port}`; let endpoint = `${host}:${port}`;
export let wsUri = `${(window.location.protocol=='https:') ? 'wss://' : 'ws://'}${endpoint}/api/v1/ws`; export let wsUri = `${
window.location.protocol == 'https:' ? 'wss://' : 'ws://'
}${endpoint}/api/v1/ws`;

10
ui/src/i18next.ts vendored
View file

@ -22,15 +22,14 @@ const resources = {
sv, sv,
ru, ru,
nl, nl,
} };
function format(value: any, format: any, lng: any) { function format(value: any, format: any, lng: any) {
if (format === 'uppercase') return value.toUpperCase(); if (format === 'uppercase') return value.toUpperCase();
return value; return value;
} }
i18n i18n.init({
.init({
debug: true, debug: true,
// load: 'languageOnly', // load: 'languageOnly',
@ -39,9 +38,8 @@ i18n
fallbackLng: 'en', fallbackLng: 'en',
resources, resources,
interpolation: { interpolation: {
format: format format: format,
},
}
}); });
export { i18n, resources }; export { i18n, resources };

27
ui/src/index.tsx vendored
View file

@ -24,7 +24,6 @@ import { WebSocketService, UserService } from './services';
const container = document.getElementById('app'); const container = document.getElementById('app');
class Index extends Component<any, any> { class Index extends Component<any, any> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
WebSocketService.Instance; WebSocketService.Instance;
@ -38,7 +37,10 @@ class Index extends Component<any, any> {
<Navbar /> <Navbar />
<div class="mt-4 p-0"> <div class="mt-4 p-0">
<Switch> <Switch>
<Route path={`/home/type/:type/sort/:sort/page/:page`} component={Main} /> <Route
path={`/home/type/:type/sort/:sort/page/:page`}
component={Main}
/>
<Route exact path={`/`} component={Main} /> <Route exact path={`/`} component={Main} />
<Route path={`/login`} component={Login} /> <Route path={`/login`} component={Login} />
<Route path={`/create_post`} component={CreatePost} /> <Route path={`/create_post`} component={CreatePost} />
@ -47,17 +49,29 @@ class Index extends Component<any, any> {
<Route path={`/communities`} component={Communities} /> <Route path={`/communities`} component={Communities} />
<Route path={`/post/:id/comment/:comment_id`} component={Post} /> <Route path={`/post/:id/comment/:comment_id`} component={Post} />
<Route path={`/post/:id`} component={Post} /> <Route path={`/post/:id`} component={Post} />
<Route path={`/c/:name/sort/:sort/page/:page`} component={Community} /> <Route
path={`/c/:name/sort/:sort/page/:page`}
component={Community}
/>
<Route path={`/community/:id`} component={Community} /> <Route path={`/community/:id`} component={Community} />
<Route path={`/c/:name`} component={Community} /> <Route path={`/c/:name`} component={Community} />
<Route path={`/u/:username/view/:view/sort/:sort/page/:page`} component={User} /> <Route
path={`/u/:username/view/:view/sort/:sort/page/:page`}
component={User}
/>
<Route path={`/user/:id`} component={User} /> <Route path={`/user/:id`} component={User} />
<Route path={`/u/:username`} component={User} /> <Route path={`/u/:username`} component={User} />
<Route path={`/inbox`} component={Inbox} /> <Route path={`/inbox`} component={Inbox} />
<Route path={`/modlog/community/:community_id`} component={Modlog} /> <Route
path={`/modlog/community/:community_id`}
component={Modlog}
/>
<Route path={`/modlog`} component={Modlog} /> <Route path={`/modlog`} component={Modlog} />
<Route path={`/setup`} component={Setup} /> <Route path={`/setup`} component={Setup} />
<Route path={`/search/q/:q/type/:type/sort/:sort/page/:page`} component={Search} /> <Route
path={`/search/q/:q/type/:type/sort/:sort/page/:page`}
component={Search}
/>
<Route path={`/search`} component={Search} /> <Route path={`/search`} component={Search} />
<Route path={`/sponsors`} component={Sponsors} /> <Route path={`/sponsors`} component={Sponsors} />
</Switch> </Switch>
@ -68,7 +82,6 @@ class Index extends Component<any, any> {
</Provider> </Provider>
); );
} }
} }
render(<Index />, container); render(<Index />, container);

266
ui/src/interfaces.ts vendored
View file

@ -1,21 +1,70 @@
export enum UserOperation { export enum UserOperation {
Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead, SaveUserSettings, TransferCommunity, TransferSite, DeleteAccount Login,
Register,
CreateCommunity,
CreatePost,
ListCommunities,
ListCategories,
GetPost,
GetCommunity,
CreateComment,
EditComment,
SaveComment,
CreateCommentLike,
GetPosts,
CreatePostLike,
EditPost,
SavePost,
EditCommunity,
FollowCommunity,
GetFollowedCommunities,
GetUserDetails,
GetReplies,
GetModlog,
BanFromCommunity,
AddModToCommunity,
CreateSite,
EditSite,
GetSite,
AddAdmin,
BanUser,
Search,
MarkAllAsRead,
SaveUserSettings,
TransferCommunity,
TransferSite,
DeleteAccount,
} }
export enum CommentSortType { export enum CommentSortType {
Hot, Top, New Hot,
Top,
New,
} }
export enum ListingType { export enum ListingType {
All, Subscribed, Community All,
Subscribed,
Community,
} }
export enum SortType { export enum SortType {
Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll Hot,
New,
TopDay,
TopWeek,
TopMonth,
TopYear,
TopAll,
} }
export enum SearchType { export enum SearchType {
All, Comments, Posts, Communities, Users, Url All,
Comments,
Posts,
Communities,
Users,
Url,
} }
export interface User { export interface User {
@ -104,7 +153,7 @@ export interface Post {
export interface Comment { export interface Comment {
id: number; id: number;
creator_id: number; creator_id: number;
post_id: number, post_id: number;
parent_id?: number; parent_id?: number;
content: string; content: string;
removed: boolean; removed: boolean;
@ -112,7 +161,7 @@ export interface Comment {
read: boolean; read: boolean;
published: string; published: string;
updated?: string; updated?: string;
community_id: number, community_id: number;
banned: boolean; banned: boolean;
banned_from_community: boolean; banned_from_community: boolean;
creator_name: string; creator_name: string;
@ -143,7 +192,10 @@ export interface Site {
number_of_communities: number; number_of_communities: number;
} }
export enum BanType {Community, Site}; export enum BanType {
Community,
Site,
}
export interface FollowCommunityForm { export interface FollowCommunityForm {
community_id: number; community_id: number;
@ -193,15 +245,15 @@ export interface BanFromCommunityForm {
community_id: number; community_id: number;
user_id: number; user_id: number;
ban: boolean; ban: boolean;
reason?: string, reason?: string;
expires?: number, expires?: number;
auth?: string; auth?: string;
} }
export interface BanFromCommunityResponse { export interface BanFromCommunityResponse {
op: string; op: string;
user: UserView, user: UserView;
banned: boolean, banned: boolean;
} }
export interface AddModToCommunityForm { export interface AddModToCommunityForm {
@ -236,15 +288,15 @@ export interface GetModlogForm {
export interface GetModlogResponse { export interface GetModlogResponse {
op: string; op: string;
removed_posts: Array<ModRemovePost>, removed_posts: Array<ModRemovePost>;
locked_posts: Array<ModLockPost>, locked_posts: Array<ModLockPost>;
stickied_posts: Array<ModStickyPost>, stickied_posts: Array<ModStickyPost>;
removed_comments: Array<ModRemoveComment>, removed_comments: Array<ModRemoveComment>;
removed_communities: Array<ModRemoveCommunity>, removed_communities: Array<ModRemoveCommunity>;
banned_from_community: Array<ModBanFromCommunity>, banned_from_community: Array<ModBanFromCommunity>;
banned: Array<ModBan>, banned: Array<ModBan>;
added_to_community: Array<ModAddCommunity>, added_to_community: Array<ModAddCommunity>;
added: Array<ModAdd>, added: Array<ModAdd>;
} }
export interface ModRemovePost { export interface ModRemovePost {
@ -253,7 +305,7 @@ export interface ModRemovePost {
post_id: number; post_id: number;
reason?: string; reason?: string;
removed?: boolean; removed?: boolean;
when_: string when_: string;
mod_user_name: string; mod_user_name: string;
post_name: string; post_name: string;
community_id: number; community_id: number;
@ -261,104 +313,104 @@ export interface ModRemovePost {
} }
export interface ModLockPost { export interface ModLockPost {
id: number, id: number;
mod_user_id: number, mod_user_id: number;
post_id: number, post_id: number;
locked?: boolean, locked?: boolean;
when_: string, when_: string;
mod_user_name: string, mod_user_name: string;
post_name: string, post_name: string;
community_id: number, community_id: number;
community_name: string, community_name: string;
} }
export interface ModStickyPost { export interface ModStickyPost {
id: number, id: number;
mod_user_id: number, mod_user_id: number;
post_id: number, post_id: number;
stickied?: boolean, stickied?: boolean;
when_: string, when_: string;
mod_user_name: string, mod_user_name: string;
post_name: string, post_name: string;
community_id: number, community_id: number;
community_name: string, community_name: string;
} }
export interface ModRemoveComment { export interface ModRemoveComment {
id: number, id: number;
mod_user_id: number, mod_user_id: number;
comment_id: number, comment_id: number;
reason?: string, reason?: string;
removed?: boolean, removed?: boolean;
when_: string, when_: string;
mod_user_name: string, mod_user_name: string;
comment_user_id: number, comment_user_id: number;
comment_user_name: string, comment_user_name: string;
comment_content: string, comment_content: string;
post_id: number, post_id: number;
post_name: string, post_name: string;
community_id: number, community_id: number;
community_name: string, community_name: string;
} }
export interface ModRemoveCommunity { export interface ModRemoveCommunity {
id: number, id: number;
mod_user_id: number, mod_user_id: number;
community_id: number, community_id: number;
reason?: string, reason?: string;
removed?: boolean, removed?: boolean;
expires?: number, expires?: number;
when_: string, when_: string;
mod_user_name: string, mod_user_name: string;
community_name: string, community_name: string;
} }
export interface ModBanFromCommunity { export interface ModBanFromCommunity {
id: number, id: number;
mod_user_id: number, mod_user_id: number;
other_user_id: number, other_user_id: number;
community_id: number, community_id: number;
reason?: string, reason?: string;
banned?: boolean, banned?: boolean;
expires?: number, expires?: number;
when_: string, when_: string;
mod_user_name: string, mod_user_name: string;
other_user_name: string, other_user_name: string;
community_name: string, community_name: string;
} }
export interface ModBan { export interface ModBan {
id: number, id: number;
mod_user_id: number, mod_user_id: number;
other_user_id: number, other_user_id: number;
reason?: string, reason?: string;
banned?: boolean, banned?: boolean;
expires?: number, expires?: number;
when_: string, when_: string;
mod_user_name: string, mod_user_name: string;
other_user_name: string, other_user_name: string;
} }
export interface ModAddCommunity { export interface ModAddCommunity {
id: number, id: number;
mod_user_id: number, mod_user_id: number;
other_user_id: number, other_user_id: number;
community_id: number, community_id: number;
removed?: boolean, removed?: boolean;
when_: string, when_: string;
mod_user_name: string, mod_user_name: string;
other_user_name: string, other_user_name: string;
community_name: string, community_name: string;
} }
export interface ModAdd { export interface ModAdd {
id: number, id: number;
mod_user_id: number, mod_user_id: number;
other_user_id: number, other_user_id: number;
removed?: boolean, removed?: boolean;
when_: string, when_: string;
mod_user_name: string, mod_user_name: string;
other_user_name: string, other_user_name: string;
} }
export interface LoginForm { export interface LoginForm {
@ -389,8 +441,8 @@ export interface UserSettingsForm {
export interface CommunityForm { export interface CommunityForm {
name: string; name: string;
title: string; title: string;
description?: string, description?: string;
category_id: number, category_id: number;
edit_id?: number; edit_id?: number;
removed?: boolean; removed?: boolean;
deleted?: boolean; deleted?: boolean;
@ -407,7 +459,6 @@ export interface GetCommunityResponse {
admins: Array<UserView>; admins: Array<UserView>;
} }
export interface CommunityResponse { export interface CommunityResponse {
op: string; op: string;
community: Community; community: Community;
@ -537,7 +588,7 @@ export interface CreatePostLikeResponse {
export interface SiteForm { export interface SiteForm {
name: string; name: string;
description?: string, description?: string;
removed?: boolean; removed?: boolean;
reason?: string; reason?: string;
expires?: number; expires?: number;
@ -552,7 +603,6 @@ export interface GetSiteResponse {
online: number; online: number;
} }
export interface SiteResponse { export interface SiteResponse {
op: string; op: string;
site: Site; site: Site;
@ -561,15 +611,15 @@ export interface SiteResponse {
export interface BanUserForm { export interface BanUserForm {
user_id: number; user_id: number;
ban: boolean; ban: boolean;
reason?: string, reason?: string;
expires?: number, expires?: number;
auth?: string; auth?: string;
} }
export interface BanUserResponse { export interface BanUserResponse {
op: string; op: string;
user: UserView, user: UserView;
banned: boolean, banned: boolean;
} }
export interface AddAdminForm { export interface AddAdminForm {

View file

@ -5,13 +5,15 @@ import * as jwt_decode from 'jwt-decode';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
export class UserService { export class UserService {
private static _instance: UserService; private static _instance: UserService;
public user: User; public user: User;
public sub: Subject<{user: User, unreadCount: number}> = new Subject<{user: User, unreadCount: number}>(); public sub: Subject<{ user: User; unreadCount: number }> = new Subject<{
user: User;
unreadCount: number;
}>();
private constructor() { private constructor() {
let jwt = Cookies.get("jwt"); let jwt = Cookies.get('jwt');
if (jwt) { if (jwt) {
this.setUser(jwt); this.setUser(jwt);
} else { } else {
@ -22,20 +24,20 @@ export class UserService {
public login(res: LoginResponse) { public login(res: LoginResponse) {
this.setUser(res.jwt); this.setUser(res.jwt);
Cookies.set("jwt", res.jwt, { expires: 365 }); Cookies.set('jwt', res.jwt, { expires: 365 });
console.log("jwt cookie set"); console.log('jwt cookie set');
} }
public logout() { public logout() {
this.user = undefined; this.user = undefined;
Cookies.remove("jwt"); Cookies.remove('jwt');
setTheme(); setTheme();
this.sub.next({ user: undefined, unreadCount: 0 }); this.sub.next({ user: undefined, unreadCount: 0 });
console.log("Logged out."); console.log('Logged out.');
} }
public get auth(): string { public get auth(): string {
return Cookies.get("jwt"); return Cookies.get('jwt');
} }
private setUser(jwt: string) { private setUser(jwt: string) {

View file

@ -1,5 +1,34 @@
import { wsUri } from '../env'; import { wsUri } from '../env';
import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, SavePostForm, CommentForm, SaveCommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm, TransferCommunityForm, AddAdminForm, TransferSiteForm, BanUserForm, SiteForm, Site, UserView, GetRepliesForm, SearchForm, UserSettingsForm, DeleteAccountForm } from '../interfaces'; import {
LoginForm,
RegisterForm,
UserOperation,
CommunityForm,
PostForm,
SavePostForm,
CommentForm,
SaveCommentForm,
CommentLikeForm,
GetPostsForm,
CreatePostLikeForm,
FollowCommunityForm,
GetUserDetailsForm,
ListCommunitiesForm,
GetModlogForm,
BanFromCommunityForm,
AddModToCommunityForm,
TransferCommunityForm,
AddAdminForm,
TransferSiteForm,
BanUserForm,
SiteForm,
Site,
UserView,
GetRepliesForm,
SearchForm,
UserSettingsForm,
DeleteAccountForm,
} from '../interfaces';
import { webSocket } from 'rxjs/webSocket'; import { webSocket } from 'rxjs/webSocket';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
@ -19,7 +48,14 @@ export class WebSocketService {
// Necessary to not keep reconnecting // Necessary to not keep reconnecting
this.subject this.subject
.pipe(retryWhen(errors => errors.pipe(delay(60000), take(999)))) .pipe(
retryWhen(errors =>
errors.pipe(
delay(60000),
take(999)
)
)
)
.subscribe(); .subscribe();
console.log(`Connected to ${wsUri}`); console.log(`Connected to ${wsUri}`);
@ -39,17 +75,23 @@ export class WebSocketService {
public createCommunity(communityForm: CommunityForm) { public createCommunity(communityForm: CommunityForm) {
this.setAuth(communityForm); this.setAuth(communityForm);
this.subject.next(this.wsSendWrapper(UserOperation.CreateCommunity, communityForm)); this.subject.next(
this.wsSendWrapper(UserOperation.CreateCommunity, communityForm)
);
} }
public editCommunity(communityForm: CommunityForm) { public editCommunity(communityForm: CommunityForm) {
this.setAuth(communityForm); this.setAuth(communityForm);
this.subject.next(this.wsSendWrapper(UserOperation.EditCommunity, communityForm)); this.subject.next(
this.wsSendWrapper(UserOperation.EditCommunity, communityForm)
);
} }
public followCommunity(followCommunityForm: FollowCommunityForm) { public followCommunity(followCommunityForm: FollowCommunityForm) {
this.setAuth(followCommunityForm); this.setAuth(followCommunityForm);
this.subject.next(this.wsSendWrapper(UserOperation.FollowCommunity, followCommunityForm)); this.subject.next(
this.wsSendWrapper(UserOperation.FollowCommunity, followCommunityForm)
);
} }
public listCommunities(form: ListCommunitiesForm) { public listCommunities(form: ListCommunitiesForm) {
@ -59,11 +101,15 @@ export class WebSocketService {
public getFollowedCommunities() { public getFollowedCommunities() {
let data = { auth: UserService.Instance.auth }; let data = { auth: UserService.Instance.auth };
this.subject.next(this.wsSendWrapper(UserOperation.GetFollowedCommunities, data)); this.subject.next(
this.wsSendWrapper(UserOperation.GetFollowedCommunities, data)
);
} }
public listCategories() { public listCategories() {
this.subject.next(this.wsSendWrapper(UserOperation.ListCategories, undefined)); this.subject.next(
this.wsSendWrapper(UserOperation.ListCategories, undefined)
);
} }
public createPost(postForm: PostForm) { public createPost(postForm: PostForm) {
@ -88,17 +134,23 @@ export class WebSocketService {
public createComment(commentForm: CommentForm) { public createComment(commentForm: CommentForm) {
this.setAuth(commentForm); this.setAuth(commentForm);
this.subject.next(this.wsSendWrapper(UserOperation.CreateComment, commentForm)); this.subject.next(
this.wsSendWrapper(UserOperation.CreateComment, commentForm)
);
} }
public editComment(commentForm: CommentForm) { public editComment(commentForm: CommentForm) {
this.setAuth(commentForm); this.setAuth(commentForm);
this.subject.next(this.wsSendWrapper(UserOperation.EditComment, commentForm)); this.subject.next(
this.wsSendWrapper(UserOperation.EditComment, commentForm)
);
} }
public likeComment(form: CommentLikeForm) { public likeComment(form: CommentLikeForm) {
this.setAuth(form); this.setAuth(form);
this.subject.next(this.wsSendWrapper(UserOperation.CreateCommentLike, form)); this.subject.next(
this.wsSendWrapper(UserOperation.CreateCommentLike, form)
);
} }
public saveComment(form: SaveCommentForm) { public saveComment(form: SaveCommentForm) {
@ -133,12 +185,16 @@ export class WebSocketService {
public addModToCommunity(form: AddModToCommunityForm) { public addModToCommunity(form: AddModToCommunityForm) {
this.setAuth(form); this.setAuth(form);
this.subject.next(this.wsSendWrapper(UserOperation.AddModToCommunity, form)); this.subject.next(
this.wsSendWrapper(UserOperation.AddModToCommunity, form)
);
} }
public transferCommunity(form: TransferCommunityForm) { public transferCommunity(form: TransferCommunityForm) {
this.setAuth(form); this.setAuth(form);
this.subject.next(this.wsSendWrapper(UserOperation.TransferCommunity, form)); this.subject.next(
this.wsSendWrapper(UserOperation.TransferCommunity, form)
);
} }
public transferSite(form: TransferSiteForm) { public transferSite(form: TransferSiteForm) {
@ -196,7 +252,9 @@ export class WebSocketService {
public saveUserSettings(userSettingsForm: UserSettingsForm) { public saveUserSettings(userSettingsForm: UserSettingsForm) {
this.setAuth(userSettingsForm); this.setAuth(userSettingsForm);
this.subject.next(this.wsSendWrapper(UserOperation.SaveUserSettings, userSettingsForm)); this.subject.next(
this.wsSendWrapper(UserOperation.SaveUserSettings, userSettingsForm)
);
} }
public deleteAccount(form: DeleteAccountForm) { public deleteAccount(form: DeleteAccountForm) {
@ -214,13 +272,12 @@ export class WebSocketService {
obj.auth = UserService.Instance.auth; obj.auth = UserService.Instance.auth;
if (obj.auth == null && throwErr) { if (obj.auth == null && throwErr) {
alert(i18n.t('not_logged_in')); alert(i18n.t('not_logged_in'));
throw "Not logged in"; throw 'Not logged in';
} }
} }
} }
window.onbeforeunload = (() => { window.onbeforeunload = () => {
WebSocketService.Instance.subject.unsubscribe(); WebSocketService.Instance.subject.unsubscribe();
WebSocketService.Instance.subject = null; WebSocketService.Instance.subject = null;
}); };

View file

@ -88,7 +88,8 @@ export const de = {
view: 'Ansicht', view: 'Ansicht',
logout: 'Ausloggen', logout: 'Ausloggen',
login_sign_up: 'Einloggen / Registrieren', login_sign_up: 'Einloggen / Registrieren',
notifications_error: 'Desktop-Benachrichtigungen sind in deinem browser nicht verfügbar. Versuche Firefox oder Chrome.', notifications_error:
'Desktop-Benachrichtigungen sind in deinem browser nicht verfügbar. Versuche Firefox oder Chrome.',
unread_messages: 'Ungelesene Nachrichten', unread_messages: 'Ungelesene Nachrichten',
password: 'Passwort', password: 'Passwort',
verify_password: 'Passwort überprüfen', verify_password: 'Passwort überprüfen',
@ -111,14 +112,17 @@ export const de = {
modified: 'verändert', modified: 'verändert',
sponsors: 'Sponsoren', sponsors: 'Sponsoren',
sponsors_of_lemmy: 'Sponsoren von Lemmy', sponsors_of_lemmy: 'Sponsoren von Lemmy',
sponsor_message: 'Lemmy ist freie <1>Open-Source</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:', sponsor_message:
'Lemmy ist freie <1>Open-Source</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:',
support_on_patreon: 'Auf Patreon unterstützen', support_on_patreon: 'Auf Patreon unterstützen',
general_sponsors:'Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.', general_sponsors:
'Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.',
bitcoin: 'Bitcoin', bitcoin: 'Bitcoin',
ethereum: 'Ethereum', ethereum: 'Ethereum',
code: 'Code', code: 'Code',
powered_by: 'Bereitgestellt durch', powered_by: 'Bereitgestellt durch',
landing_0: 'Lemmy ist ein <1>Link Aggregator</1> / Reddit Alternative im <2>Fediverse</2>.<3></3>Es ist selbst-hostbar, hat live-updates von Kommentar-threads und ist winzig (<4>~80kB</4>). Federation in das ActivityPub Netzwerk ist geplant. <5></5>Dies ist eine <6>sehr frühe Beta Version</6>, und viele Features funktionieren zurzeit nicht richtig oder fehlen. <7></7>Schlage neue Features vor oder melde Bugs <8>hier.</8><9></9>Gebaut mit <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.', landing_0:
'Lemmy ist ein <1>Link Aggregator</1> / Reddit Alternative im <2>Fediverse</2>.<3></3>Es ist selbst-hostbar, hat live-updates von Kommentar-threads und ist winzig (<4>~80kB</4>). Federation in das ActivityPub Netzwerk ist geplant. <5></5>Dies ist eine <6>sehr frühe Beta Version</6>, und viele Features funktionieren zurzeit nicht richtig oder fehlen. <7></7>Schlage neue Features vor oder melde Bugs <8>hier.</8><9></9>Gebaut mit <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
not_logged_in: 'Nicht eingeloggt.', not_logged_in: 'Nicht eingeloggt.',
community_ban: 'Du wurdest von dieser Community gebannt.', community_ban: 'Du wurdest von dieser Community gebannt.',
site_ban: 'Du wurdest von dieser Seite gebannt', site_ban: 'Du wurdest von dieser Seite gebannt',
@ -132,7 +136,8 @@ export const de = {
couldnt_find_community: 'Konnte Community nicht finden.', couldnt_find_community: 'Konnte Community nicht finden.',
couldnt_update_community: 'Konnte Community nicht aktualisieren.', couldnt_update_community: 'Konnte Community nicht aktualisieren.',
community_already_exists: 'Community existiert bereits.', community_already_exists: 'Community existiert bereits.',
community_moderator_already_exists: 'Community Moderator existiert bereits.', community_moderator_already_exists:
'Community Moderator existiert bereits.',
community_follower_already_exists: 'Community Follower existiert bereits.', community_follower_already_exists: 'Community Follower existiert bereits.',
community_user_already_banned: 'Community Nutzer schon gebannt.', community_user_already_banned: 'Community Nutzer schon gebannt.',
couldnt_create_post: 'Konnte Beitrag nicht anlegen.', couldnt_create_post: 'Konnte Beitrag nicht anlegen.',
@ -145,13 +150,14 @@ export const de = {
not_an_admin: 'Kein Administrator.', not_an_admin: 'Kein Administrator.',
site_already_exists: 'Seite existiert bereits.', site_already_exists: 'Seite existiert bereits.',
couldnt_update_site: 'Konnte Seite nicht aktualisieren.', couldnt_update_site: 'Konnte Seite nicht aktualisieren.',
couldnt_find_that_username_or_email: 'Konnte Username oder E-Mail nicht finden.', couldnt_find_that_username_or_email:
'Konnte Username oder E-Mail nicht finden.',
password_incorrect: 'Passwort falsch.', password_incorrect: 'Passwort falsch.',
passwords_dont_match: 'Passwörter stimmen nicht überein.', passwords_dont_match: 'Passwörter stimmen nicht überein.',
admin_already_created: 'Entschuldigung, es gibt schon einen Administrator.', admin_already_created: 'Entschuldigung, es gibt schon einen Administrator.',
user_already_exists: 'Nutzer existiert bereits.', user_already_exists: 'Nutzer existiert bereits.',
couldnt_update_user: 'Konnte Nutzer nicht aktualisieren', couldnt_update_user: 'Konnte Nutzer nicht aktualisieren',
system_err_login: 'Systemfehler. Versuche dich aus- und wieder einzuloggen.', system_err_login:
'Systemfehler. Versuche dich aus- und wieder einzuloggen.',
}, },
} };

View file

@ -56,7 +56,8 @@ export const en = {
delete: 'delete', delete: 'delete',
deleted: 'deleted', deleted: 'deleted',
delete_account: 'Delete Account', delete_account: 'Delete Account',
delete_account_confirm: 'Warning: this will permanently delete all your data. Enter your password to confirm.', delete_account_confirm:
'Warning: this will permanently delete all your data. Enter your password to confirm.',
restore: 'restore', restore: 'restore',
ban: 'ban', ban: 'ban',
ban_from_site: 'ban from site', ban_from_site: 'ban from site',
@ -108,7 +109,8 @@ export const en = {
login_sign_up: 'Login / Sign up', login_sign_up: 'Login / Sign up',
login: 'Login', login: 'Login',
sign_up: 'Sign Up', sign_up: 'Sign Up',
notifications_error: 'Desktop notifications not available in your browser. Try Firefox or Chrome.', notifications_error:
'Desktop notifications not available in your browser. Try Firefox or Chrome.',
unread_messages: 'Unread Messages', unread_messages: 'Unread Messages',
password: 'Password', password: 'Password',
verify_password: 'Verify Password', verify_password: 'Verify Password',
@ -134,9 +136,11 @@ export const en = {
theme: 'Theme', theme: 'Theme',
sponsors: 'Sponsors', sponsors: 'Sponsors',
sponsors_of_lemmy: 'Sponsors of Lemmy', sponsors_of_lemmy: 'Sponsors of Lemmy',
sponsor_message: 'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:', sponsor_message:
'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
support_on_patreon: 'Support on Patreon', support_on_patreon: 'Support on Patreon',
general_sponsors: 'General Sponsors are those that pledged $10 to $39 to Lemmy.', general_sponsors:
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
crypto: 'Crypto', crypto: 'Crypto',
bitcoin: 'Bitcoin', bitcoin: 'Bitcoin',
ethereum: 'Ethereum', ethereum: 'Ethereum',
@ -151,40 +155,41 @@ export const en = {
yes: 'yes', yes: 'yes',
no: 'no', no: 'no',
powered_by: 'Powered by', powered_by: 'Powered by',
landing_0: 'Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It\'s self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.', landing_0:
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
not_logged_in: 'Not logged in.', not_logged_in: 'Not logged in.',
community_ban: 'You have been banned from this community.', community_ban: 'You have been banned from this community.',
site_ban: 'You have been banned from the site', site_ban: 'You have been banned from the site',
couldnt_create_comment: 'Couldn\'t create comment.', couldnt_create_comment: "Couldn't create comment.",
couldnt_like_comment: 'Couldn\'t like comment.', couldnt_like_comment: "Couldn't like comment.",
couldnt_update_comment: 'Couldn\'t update comment.', couldnt_update_comment: "Couldn't update comment.",
couldnt_save_comment: 'Couldn\'t save comment.', couldnt_save_comment: "Couldn't save comment.",
no_comment_edit_allowed: 'Not allowed to edit comment.', no_comment_edit_allowed: 'Not allowed to edit comment.',
no_post_edit_allowed: 'Not allowed to edit post.', no_post_edit_allowed: 'Not allowed to edit post.',
no_community_edit_allowed: 'Not allowed to edit community.', no_community_edit_allowed: 'Not allowed to edit community.',
couldnt_find_community: 'Couldn\'t find community.', couldnt_find_community: "Couldn't find community.",
couldnt_update_community: 'Couldn\'t update Community.', couldnt_update_community: "Couldn't update Community.",
community_already_exists: 'Community already exists.', community_already_exists: 'Community already exists.',
community_moderator_already_exists: 'Community moderator already exists.', community_moderator_already_exists: 'Community moderator already exists.',
community_follower_already_exists: 'Community follower already exists.', community_follower_already_exists: 'Community follower already exists.',
community_user_already_banned: 'Community user already banned.', community_user_already_banned: 'Community user already banned.',
couldnt_create_post: 'Couldn\'t create post.', couldnt_create_post: "Couldn't create post.",
couldnt_like_post: 'Couldn\'t like post.', couldnt_like_post: "Couldn't like post.",
couldnt_find_post: 'Couldn\'t find post.', couldnt_find_post: "Couldn't find post.",
couldnt_get_posts: 'Couldn\'t get posts', couldnt_get_posts: "Couldn't get posts",
couldnt_update_post: 'Couldn\'t update post', couldnt_update_post: "Couldn't update post",
couldnt_save_post: 'Couldn\'t save post.', couldnt_save_post: "Couldn't save post.",
no_slurs: 'No slurs.', no_slurs: 'No slurs.',
not_an_admin: 'Not an admin.', not_an_admin: 'Not an admin.',
site_already_exists: 'Site already exists.', site_already_exists: 'Site already exists.',
couldnt_update_site: 'Couldn\'t update site.', couldnt_update_site: "Couldn't update site.",
couldnt_find_that_username_or_email: 'Couldn\'t find that username or email.', couldnt_find_that_username_or_email:
"Couldn't find that username or email.",
password_incorrect: 'Password incorrect.', password_incorrect: 'Password incorrect.',
passwords_dont_match: 'Passwords do not match.', passwords_dont_match: 'Passwords do not match.',
admin_already_created: 'Sorry, there\'s already an admin.', admin_already_created: "Sorry, there's already an admin.",
user_already_exists: 'User already exists.', user_already_exists: 'User already exists.',
couldnt_update_user: 'Couldn\'t update user.', couldnt_update_user: "Couldn't update user.",
system_err_login: 'System error. Try logging out and back in.', system_err_login: 'System error. Try logging out and back in.',
}, },
} };

View file

@ -95,7 +95,8 @@ export const eo = {
login_sign_up: 'Ensaluti / Registriĝi', login_sign_up: 'Ensaluti / Registriĝi',
login: 'Ensaluti', login: 'Ensaluti',
sign_up: 'Registriĝi', sign_up: 'Registriĝi',
notifications_error: 'Labortablaj avizoj estas nehavebla en via retumilo. Provu Firefox-on aŭ Chrome-on.', notifications_error:
'Labortablaj avizoj estas nehavebla en via retumilo. Provu Firefox-on aŭ Chrome-on.',
unread_messages: 'Nelegitaj Mesaĝoj', unread_messages: 'Nelegitaj Mesaĝoj',
password: 'Pasvorto', password: 'Pasvorto',
verify_password: 'Konfirmu Vian Pasvorton', verify_password: 'Konfirmu Vian Pasvorton',
@ -120,9 +121,11 @@ export const eo = {
show_nsfw: 'Vidigi NSFW-an enhavon', show_nsfw: 'Vidigi NSFW-an enhavon',
sponsors: 'Subtenantoj', sponsors: 'Subtenantoj',
sponsors_of_lemmy: 'Subtenantoj de Lemmy', sponsors_of_lemmy: 'Subtenantoj de Lemmy',
sponsor_message: 'Lemmy estas senpaga, <1>liberkoda</1> programaro. Tio signifas ne reklami, pagigi, aŭ riska kapitalo, ĉiam. Viaj donacoj rekte subtenas plentempan evoluon de la projekto. Dankon al tiuj homoj:', sponsor_message:
'Lemmy estas senpaga, <1>liberkoda</1> programaro. Tio signifas ne reklami, pagigi, aŭ riska kapitalo, ĉiam. Viaj donacoj rekte subtenas plentempan evoluon de la projekto. Dankon al tiuj homoj:',
support_on_patreon: 'Subteni per Patreon', support_on_patreon: 'Subteni per Patreon',
general_sponsors:'Ĝeneralaj Subtenantoj estas tiuj ke donacis inter $10 kaj $39 al Lemmy.', general_sponsors:
'Ĝeneralaj Subtenantoj estas tiuj ke donacis inter $10 kaj $39 al Lemmy.',
crypto: 'Crypto', crypto: 'Crypto',
bitcoin: 'Bitcoin', bitcoin: 'Bitcoin',
ethereum: 'Ethereum', ethereum: 'Ethereum',
@ -134,7 +137,8 @@ export const eo = {
transfer_community: 'transdoni la komunumon', transfer_community: 'transdoni la komunumon',
transfer_site: 'transdoni la retejon', transfer_site: 'transdoni la retejon',
powered_by: 'Konstruis per', powered_by: 'Konstruis per',
landing_0: 'Lemmy estas <1>ligila agregatilo</1> / Reddit anstataŭo ke intenciĝas funkci en la <2>federacio-universo</2>.<3></3>ĝi estas mem-gastigebla, havas nuna-ĝisdatigajn komentarojn, kaj estas malgrandega (<4>~80kB</4>). Federacio en la ActivityPub-an reton estas planizita. <5></5>Estas <6>fruega beta versio</6>, kaj multaj trajtoj estas nune difektaj aŭ mankaj. <7></7>Sugestias novajn trajtojn aŭ raportas cimojn <8>ĉi tie.</8><9></9>Faris per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.', landing_0:
'Lemmy estas <1>ligila agregatilo</1> / Reddit anstataŭo ke intenciĝas funkci en la <2>federacio-universo</2>.<3></3>ĝi estas mem-gastigebla, havas nuna-ĝisdatigajn komentarojn, kaj estas malgrandega (<4>~80kB</4>). Federacio en la ActivityPub-an reton estas planizita. <5></5>Estas <6>fruega beta versio</6>, kaj multaj trajtoj estas nune difektaj aŭ mankaj. <7></7>Sugestias novajn trajtojn aŭ raportas cimojn <8>ĉi tie.</8><9></9>Faris per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
not_logged_in: 'Ne estas ensalutinta.', not_logged_in: 'Ne estas ensalutinta.',
community_ban: 'Vi estas forbarita de la komunumo.', community_ban: 'Vi estas forbarita de la komunumo.',
site_ban: 'Vi estas forbarita de la retejo', site_ban: 'Vi estas forbarita de la retejo',
@ -161,7 +165,8 @@ export const eo = {
not_an_admin: 'Ne estas administranto.', not_an_admin: 'Ne estas administranto.',
site_already_exists: 'Retejo jam ekzistas.', site_already_exists: 'Retejo jam ekzistas.',
couldnt_update_site: 'Ne povis ĝisdatigi la retejon.', couldnt_update_site: 'Ne povis ĝisdatigi la retejon.',
couldnt_find_that_username_or_email: 'Ne povis trovi tiun uzantnomon aŭ retadreson.', couldnt_find_that_username_or_email:
'Ne povis trovi tiun uzantnomon aŭ retadreson.',
password_incorrect: 'Pasvorto malĝustas.', password_incorrect: 'Pasvorto malĝustas.',
passwords_dont_match: 'Pasvortoj ne samas.', passwords_dont_match: 'Pasvortoj ne samas.',
admin_already_created: 'Pardonu, jam estas administranto.', admin_already_created: 'Pardonu, jam estas administranto.',
@ -169,5 +174,4 @@ export const eo = {
couldnt_update_user: 'Ne povis ĝisdatigi la uzanton.', couldnt_update_user: 'Ne povis ĝisdatigi la uzanton.',
system_err_login: 'Sistema eraro. Provu elsaluti kaj ensaluti.', system_err_login: 'Sistema eraro. Provu elsaluti kaj ensaluti.',
}, },
} };

View file

@ -56,7 +56,8 @@ export const es = {
delete: 'eliminar', delete: 'eliminar',
deleted: 'eliminado', deleted: 'eliminado',
delete_account: 'Eliminar Cuenta', delete_account: 'Eliminar Cuenta',
delete_account_confirm: 'Peligro: esta acción eliminará permanentemente tu información. ¿Estás seguro?', delete_account_confirm:
'Peligro: esta acción eliminará permanentemente tu información. ¿Estás seguro?',
restore: 'restaurar', restore: 'restaurar',
ban: 'expulsar', ban: 'expulsar',
ban_from_site: 'expulsar del sitio', ban_from_site: 'expulsar del sitio',
@ -108,7 +109,8 @@ export const es = {
login_sign_up: 'Iniciar sesión / Crear cuenta', login_sign_up: 'Iniciar sesión / Crear cuenta',
login: 'Iniciar sesión', login: 'Iniciar sesión',
sign_up: 'Crear cuenta', sign_up: 'Crear cuenta',
notifications_error: 'Notificaciones de escritorio no disponibles en tu navegador. Prueba Firefox o Chrome.', notifications_error:
'Notificaciones de escritorio no disponibles en tu navegador. Prueba Firefox o Chrome.',
unread_messages: 'Mensajes no leídos', unread_messages: 'Mensajes no leídos',
password: 'Contraseña', password: 'Contraseña',
verify_password: 'Verificar contraseña', verify_password: 'Verificar contraseña',
@ -134,9 +136,11 @@ export const es = {
theme: 'Tema', theme: 'Tema',
sponsors: 'Patrocinadores', sponsors: 'Patrocinadores',
sponsors_of_lemmy: 'Patrocinadores de Lemmy', sponsors_of_lemmy: 'Patrocinadores de Lemmy',
sponsor_message: 'Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:', sponsor_message:
'Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:',
support_on_patreon: 'Apoyo en Patreon', support_on_patreon: 'Apoyo en Patreon',
general_sponsors:'Patrocinadores Generales son aquellos que señaron entre $10 y $39 a Lemmy.', general_sponsors:
'Patrocinadores Generales son aquellos que señaron entre $10 y $39 a Lemmy.',
crypto: 'Crypto', crypto: 'Crypto',
bitcoin: 'Bitcoin', bitcoin: 'Bitcoin',
ethereum: 'Ethereum', ethereum: 'Ethereum',
@ -151,7 +155,8 @@ export const es = {
yes: 'sí', yes: 'sí',
no: 'no', no: 'no',
powered_by: 'Impulsado por', powered_by: 'Impulsado por',
landing_0: 'Lemmy es un <1>agregador de links</1> / alternativa a reddit, con la intención de funcionar en el <2>fediverso</2>.<3></3>Es alojable por uno mismo (sin necesidad de grandes compañías), tiene actualización en vivo de cadenas de comentarios, y es pequeño (<4>~80kB</4>). Federar con el sistema de redes ActivityPub forma parte de los objetivos del proyecto. <5></5>Esta es una <6>version beta muy prematura</6>, y actualmente muchas de las características están rotas o faltan. <7></7>Sugiere nuevas características o reporta errores <8>aquí</8>.<9></9>Hecho con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.', landing_0:
'Lemmy es un <1>agregador de links</1> / alternativa a reddit, con la intención de funcionar en el <2>fediverso</2>.<3></3>Es alojable por uno mismo (sin necesidad de grandes compañías), tiene actualización en vivo de cadenas de comentarios, y es pequeño (<4>~80kB</4>). Federar con el sistema de redes ActivityPub forma parte de los objetivos del proyecto. <5></5>Esta es una <6>version beta muy prematura</6>, y actualmente muchas de las características están rotas o faltan. <7></7>Sugiere nuevas características o reporta errores <8>aquí</8>.<9></9>Hecho con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
not_logged_in: 'No has iniciado sesión.', not_logged_in: 'No has iniciado sesión.',
community_ban: 'Has sido expulsado de esta comunidad.', community_ban: 'Has sido expulsado de esta comunidad.',
site_ban: 'Has sido expulsado del sitio', site_ban: 'Has sido expulsado del sitio',
@ -165,9 +170,12 @@ export const es = {
couldnt_find_community: 'No se pudo encontrar la comunidad.', couldnt_find_community: 'No se pudo encontrar la comunidad.',
couldnt_update_community: 'No se pudo actualizar la comunidad.', couldnt_update_community: 'No se pudo actualizar la comunidad.',
community_already_exists: 'Esta comunidad ya existe.', community_already_exists: 'Esta comunidad ya existe.',
community_moderator_already_exists: 'Este moderador de la comunidad ya existe.', community_moderator_already_exists:
community_follower_already_exists: 'Este seguidor de la comunidad ya existe.', 'Este moderador de la comunidad ya existe.',
community_user_already_banned: 'Este usuario de la comunidad ya fue expulsado.', community_follower_already_exists:
'Este seguidor de la comunidad ya existe.',
community_user_already_banned:
'Este usuario de la comunidad ya fue expulsado.',
couldnt_create_post: 'No se pudo crear la publicación.', couldnt_create_post: 'No se pudo crear la publicación.',
couldnt_like_post: 'No se pudo gustar la publicación.', couldnt_like_post: 'No se pudo gustar la publicación.',
couldnt_find_post: 'No se pudo encontrar la publicación.', couldnt_find_post: 'No se pudo encontrar la publicación.',
@ -178,13 +186,14 @@ export const es = {
not_an_admin: 'No es un administrador.', not_an_admin: 'No es un administrador.',
site_already_exists: 'El sitio ya existe.', site_already_exists: 'El sitio ya existe.',
couldnt_update_site: 'No se pudo actualizar el sitio.', couldnt_update_site: 'No se pudo actualizar el sitio.',
couldnt_find_that_username_or_email: 'No se pudo encontrar ese nombre de usuario o correo electrónico.', couldnt_find_that_username_or_email:
'No se pudo encontrar ese nombre de usuario o correo electrónico.',
password_incorrect: 'Contraseña incorrecta.', password_incorrect: 'Contraseña incorrecta.',
passwords_dont_match: 'Las contraseñas no coinciden.', passwords_dont_match: 'Las contraseñas no coinciden.',
admin_already_created: 'Lo sentimos, ya hay un adminisitrador.', admin_already_created: 'Lo sentimos, ya hay un adminisitrador.',
user_already_exists: 'El usuario ya existe.', user_already_exists: 'El usuario ya existe.',
couldnt_update_user: 'No se pudo actualizar el usuario.', couldnt_update_user: 'No se pudo actualizar el usuario.',
system_err_login: 'Error del sistema. Intente cerrar sesión e ingresar de nuevo.', system_err_login:
'Error del sistema. Intente cerrar sesión e ingresar de nuevo.',
}, },
} };

View file

@ -56,7 +56,8 @@ export const fr = {
delete: 'supprimer', delete: 'supprimer',
deleted: 'supprimé', deleted: 'supprimé',
delete_account: 'Supprimer le compte', delete_account: 'Supprimer le compte',
delete_account_confirm: 'Attention: cette action supprime toutes vos données de façons permanente. Entrez votre mot de passe pour confirmer.', delete_account_confirm:
'Attention: cette action supprime toutes vos données de façons permanente. Entrez votre mot de passe pour confirmer.',
restore: 'restaurer', restore: 'restaurer',
ban: 'bannir', ban: 'bannir',
ban_from_site: 'bannir du site', ban_from_site: 'bannir du site',
@ -67,8 +68,8 @@ export const fr = {
unsave: 'retirer', unsave: 'retirer',
create: 'créer', create: 'créer',
creator: 'createur', creator: 'createur',
username: 'Nom d\'utilisateur', username: "Nom d'utilisateur",
email_or_username: 'Email ou Nom d\'utilisateur', email_or_username: "Email ou Nom d'utilisateur",
number_of_users: '{{count}} Utilisateurs', number_of_users: '{{count}} Utilisateurs',
number_of_subscribers: '{{count}} Abonnés', number_of_subscribers: '{{count}} Abonnés',
number_of_points: '{{count}} Points', number_of_points: '{{count}} Points',
@ -80,7 +81,7 @@ export const fr = {
both: 'Les deux', both: 'Les deux',
saved: 'Sauvegardé', saved: 'Sauvegardé',
unsubscribe: 'Se désincrire', unsubscribe: 'Se désincrire',
subscribe: 'S\'inscrire', subscribe: "S'inscrire",
subscribed: 'Inscris', subscribed: 'Inscris',
prev: 'Précédent', prev: 'Précédent',
next: 'Suivant', next: 'Suivant',
@ -105,10 +106,11 @@ export const fr = {
overview: 'Général', overview: 'Général',
view: 'Voir', view: 'Voir',
logout: 'Se déconnecter', logout: 'Se déconnecter',
login_sign_up: 'Se connecter / S\'inscrire', login_sign_up: "Se connecter / S'inscrire",
login: 'Se connecter', login: 'Se connecter',
sign_up: 'S\'inscrire', sign_up: "S'inscrire",
notifications_error: 'Les notifications de bureau ne sont pas discponibles sur votre navigateur. Essayez Firefox ou Chrome.', notifications_error:
'Les notifications de bureau ne sont pas discponibles sur votre navigateur. Essayez Firefox ou Chrome.',
unread_messages: 'Messages non-lu', unread_messages: 'Messages non-lu',
password: 'Mot de passe', password: 'Mot de passe',
verify_password: 'Vérifiez le mot de passe', verify_password: 'Vérifiez le mot de passe',
@ -120,12 +122,12 @@ export const fr = {
copy_suggested_title: 'Ajouter le titre suggéré: {{title}}', copy_suggested_title: 'Ajouter le titre suggéré: {{title}}',
community: 'Communauté', community: 'Communauté',
expand_here: 'Développer ici', expand_here: 'Développer ici',
subscribe_to_communities: 'S\'abonner à quelques <1>communautés</1>.', subscribe_to_communities: "S'abonner à quelques <1>communautés</1>.",
chat: 'Chat', chat: 'Chat',
recent_comments: 'Commentaires récents', recent_comments: 'Commentaires récents',
no_results: 'Pas de résultats.', no_results: 'Pas de résultats.',
setup: 'Installation', setup: 'Installation',
lemmy_instance_setup: 'Installation d\'une instance Lemmy', lemmy_instance_setup: "Installation d'une instance Lemmy",
setup_admin: 'Créer un administrateur', setup_admin: 'Créer un administrateur',
your_site: 'votre site', your_site: 'votre site',
modified: 'modifié', modified: 'modifié',
@ -134,9 +136,11 @@ export const fr = {
theme: 'Thème', theme: 'Thème',
sponsors: 'Sponsors', sponsors: 'Sponsors',
sponsors_of_lemmy: 'Sponsors de Lemmy', sponsors_of_lemmy: 'Sponsors de Lemmy',
sponsor_message: 'Lemmy est gratuit et <1>open-source</1>, c\'est à dire sans publicité et sans monétisation. Pour toujours. Vos dons soutiennent directement le développement du projet. Merci à nos soutiens.', sponsor_message:
"Lemmy est gratuit et <1>open-source</1>, c'est à dire sans publicité et sans monétisation. Pour toujours. Vos dons soutiennent directement le développement du projet. Merci à nos soutiens.",
support_on_patreon: 'Soutenir sur Patreon', support_on_patreon: 'Soutenir sur Patreon',
general_sponsors:'General Sponsors are those that pledged $10 to $39 to Lemmy.', general_sponsors:
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
crypto: 'Crypto', crypto: 'Crypto',
bitcoin: 'Bitcoin', bitcoin: 'Bitcoin',
ethereum: 'Ethereum', ethereum: 'Ethereum',
@ -151,39 +155,44 @@ export const fr = {
yes: 'oui', yes: 'oui',
no: 'non', no: 'non',
powered_by: 'Propulsé par', powered_by: 'Propulsé par',
landing_0: 'Lemmy est un <1>aggrégateur de lien</1>, similaire à reddit et conçu pour fonctionner sur le <2>fédiverse</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via Activitypub est prévue sur la feuille de route. <5></5>Lemmy est une <6>version beta très précoce</6>, et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez rapporter des bugs et suggérez de nouvelles fonctionnalités <8>ici.</8><9></9>Crée avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.', landing_0:
not_logged_in: 'Vous n\'êtes pas connecté.', 'Lemmy est un <1>aggrégateur de lien</1>, similaire à reddit et conçu pour fonctionner sur le <2>fédiverse</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via Activitypub est prévue sur la feuille de route. <5></5>Lemmy est une <6>version beta très précoce</6>, et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez rapporter des bugs et suggérez de nouvelles fonctionnalités <8>ici.</8><9></9>Crée avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
not_logged_in: "Vous n'êtes pas connecté.",
community_ban: 'Vous avez été banni de cette communauté.', community_ban: 'Vous avez été banni de cette communauté.',
site_ban: 'Vous avez été banni du site', site_ban: 'Vous avez été banni du site',
couldnt_create_comment: 'Impossible de poster le commentaire.', couldnt_create_comment: 'Impossible de poster le commentaire.',
couldnt_like_comment: 'Impossible d\'aimer le commentaire.', couldnt_like_comment: "Impossible d'aimer le commentaire.",
couldnt_update_comment: 'Impossible de mettre à jour le commentaire.', couldnt_update_comment: 'Impossible de mettre à jour le commentaire.',
couldnt_save_comment: 'Impossible de sauvegarder le commentaire.', couldnt_save_comment: 'Impossible de sauvegarder le commentaire.',
no_comment_edit_allowed: 'Vous n\'êtes pas autorisé à éditer ce commentaire.', no_comment_edit_allowed:
no_post_edit_allowed: 'ous n\'êtes pas autorisé à éditer sujet.', "Vous n'êtes pas autorisé à éditer ce commentaire.",
no_community_edit_allowed: 'ous n\'êtes pas autorisé à éditer cette communauté.', no_post_edit_allowed: "ous n'êtes pas autorisé à éditer sujet.",
no_community_edit_allowed:
"ous n'êtes pas autorisé à éditer cette communauté.",
couldnt_find_community: 'Impossible de trouver cette communauté.', couldnt_find_community: 'Impossible de trouver cette communauté.',
couldnt_update_community: 'Impossible d\'éditer cette communauté.', couldnt_update_community: "Impossible d'éditer cette communauté.",
community_already_exists: 'Cette communauté existe déjà.', community_already_exists: 'Cette communauté existe déjà.',
community_moderator_already_exists: 'Ce membre est déjà modérateur.', community_moderator_already_exists: 'Ce membre est déjà modérateur.',
community_follower_already_exists: 'Ce membre est déjà abonné.', community_follower_already_exists: 'Ce membre est déjà abonné.',
community_user_already_banned: 'Ce membre est déjà banni.', community_user_already_banned: 'Ce membre est déjà banni.',
couldnt_create_post: 'Impossible dae créer le sujet.', couldnt_create_post: 'Impossible dae créer le sujet.',
couldnt_like_post: 'Impossible d\'aimer le sujet.', couldnt_like_post: "Impossible d'aimer le sujet.",
couldnt_find_post: 'Impossible de trouver le sujet.', couldnt_find_post: 'Impossible de trouver le sujet.',
couldnt_get_posts: 'Impossible d\'obtenir les sujets', couldnt_get_posts: "Impossible d'obtenir les sujets",
couldnt_update_post: 'Impossible de mettre à jour le sujet', couldnt_update_post: 'Impossible de mettre à jour le sujet',
couldnt_save_post: 'Impossible de sauvegarder le sujet.', couldnt_save_post: 'Impossible de sauvegarder le sujet.',
no_slurs: 'Pas d\'insultes.', no_slurs: "Pas d'insultes.",
not_an_admin: 'Pas administrateur.', not_an_admin: 'Pas administrateur.',
site_already_exists: 'Le site existe déjà.', site_already_exists: 'Le site existe déjà.',
couldnt_update_site: 'Impossible de mettre à jour le site.', couldnt_update_site: 'Impossible de mettre à jour le site.',
couldnt_find_that_username_or_email: 'Impossible de trouver cet utilisateur ou cet email.', couldnt_find_that_username_or_email:
'Impossible de trouver cet utilisateur ou cet email.',
password_incorrect: 'Mot de passe incorrect.', password_incorrect: 'Mot de passe incorrect.',
passwords_dont_match: 'Les mots de passes ne correspondent pas..', passwords_dont_match: 'Les mots de passes ne correspondent pas..',
admin_already_created: 'Désolé, il y a déjà un admin.', admin_already_created: 'Désolé, il y a déjà un admin.',
user_already_exists: 'L\'utilisateur existe déjà.', user_already_exists: "L'utilisateur existe déjà.",
couldnt_update_user: 'Impossible de mettre à jour l\'utilisateur.', couldnt_update_user: "Impossible de mettre à jour l'utilisateur.",
system_err_login: 'Erreur système. Essayez de vous déconneter puis de vous reconnecter.', system_err_login:
'Erreur système. Essayez de vous déconneter puis de vous reconnecter.',
}, },
} };

View file

@ -96,7 +96,8 @@ export const nl = {
login_sign_up: 'Log in / Aanmelden', login_sign_up: 'Log in / Aanmelden',
login: 'Log in', login: 'Log in',
sign_up: 'Aanmelden', sign_up: 'Aanmelden',
notifications_error: 'Bureabladberichten niet beschikbaar in je browser. Probeer Firefox of Chrome.', notifications_error:
'Bureabladberichten niet beschikbaar in je browser. Probeer Firefox of Chrome.',
unread_messages: 'Ongelezen berichten', unread_messages: 'Ongelezen berichten',
password: 'Wachtwoord', password: 'Wachtwoord',
verify_password: 'Herhaal wachtwoord', verify_password: 'Herhaal wachtwoord',
@ -121,9 +122,11 @@ export const nl = {
show_nsfw: 'Laat NSFW-inhoud zien', show_nsfw: 'Laat NSFW-inhoud zien',
sponsors: 'Sponsoren', sponsors: 'Sponsoren',
sponsors_of_lemmy: 'Sponsoren van Lemmy', sponsors_of_lemmy: 'Sponsoren van Lemmy',
sponsor_message: 'Lemmy is vrije, <1>open-source</1> software, dus zonder reclame, winstoogmerk en durfkapitaal, punt. Jouw donaties gaan direct naar de full-time-ontwikkeling van het project. Met veel dank aan de volgende mensen:', sponsor_message:
'Lemmy is vrije, <1>open-source</1> software, dus zonder reclame, winstoogmerk en durfkapitaal, punt. Jouw donaties gaan direct naar de full-time-ontwikkeling van het project. Met veel dank aan de volgende mensen:',
support_on_patreon: 'Ondersteun op Patreon', support_on_patreon: 'Ondersteun op Patreon',
general_sponsors:'Algemene sponsors zijn sponsors die tussen de $10 en $39 hebben gegeven aan Lemmy.', general_sponsors:
'Algemene sponsors zijn sponsors die tussen de $10 en $39 hebben gegeven aan Lemmy.',
crypto: 'Cryptovaluta', crypto: 'Cryptovaluta',
bitcoin: 'Bitcoin', bitcoin: 'Bitcoin',
ethereum: 'Ethereum', ethereum: 'Ethereum',
@ -138,7 +141,8 @@ export const nl = {
yes: 'ja', yes: 'ja',
no: 'nee', no: 'nee',
powered_by: 'Mogelijk gemaakt door', powered_by: 'Mogelijk gemaakt door',
landing_0: 'Lemmy is een <1>linkverzameler</1> / reddit-alternatief, bedoeld om in de <2>fediverse</2> te werken.<3></3>Lemmy kan door om het even wie gehost worden, heeft live-bijgewerkte reacties en is superklein (<4>ca. 80 kB</4>). Federatie in hte ActivityPub-netwerk is gepland. <5></5>Dit is een <6>erg vroege bèta-versie</6>, en een hoop functies zijn stuk of afwezig. <7></7>Stel nieuwe functies voor of meldt fouten <8>hier</8>.<9></9>Gemaakt met <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> en <13>Typescript</13>.', landing_0:
'Lemmy is een <1>linkverzameler</1> / reddit-alternatief, bedoeld om in de <2>fediverse</2> te werken.<3></3>Lemmy kan door om het even wie gehost worden, heeft live-bijgewerkte reacties en is superklein (<4>ca. 80 kB</4>). Federatie in hte ActivityPub-netwerk is gepland. <5></5>Dit is een <6>erg vroege bèta-versie</6>, en een hoop functies zijn stuk of afwezig. <7></7>Stel nieuwe functies voor of meldt fouten <8>hier</8>.<9></9>Gemaakt met <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> en <13>Typescript</13>.',
not_logged_in: 'Niet ingelogd.', not_logged_in: 'Niet ingelogd.',
community_ban: 'Je bent verbannen uit deze community.', community_ban: 'Je bent verbannen uit deze community.',
site_ban: 'Je bent verbannen van deze site.', site_ban: 'Je bent verbannen van deze site.',
@ -165,12 +169,14 @@ export const nl = {
not_an_admin: 'Niet een beheerder.', not_an_admin: 'Niet een beheerder.',
site_already_exists: 'Site bestaat al.', site_already_exists: 'Site bestaat al.',
couldnt_update_site: 'Kon site niet bijwerken.', couldnt_update_site: 'Kon site niet bijwerken.',
couldnt_find_that_username_or_email: 'Kon gebruikersnaam of e-mailadres niet vinden.', couldnt_find_that_username_or_email:
'Kon gebruikersnaam of e-mailadres niet vinden.',
password_incorrect: 'Wachtwoord incorrect.', password_incorrect: 'Wachtwoord incorrect.',
passwords_dont_match: 'Wachtwoorden zijn niet gelijk.', passwords_dont_match: 'Wachtwoorden zijn niet gelijk.',
admin_already_created: 'Sorry, er is al een beheerder.', admin_already_created: 'Sorry, er is al een beheerder.',
user_already_exists: 'Gebruiker bestaat al.', user_already_exists: 'Gebruiker bestaat al.',
couldnt_update_user: 'Kon gebruiker niet bijwerken.', couldnt_update_user: 'Kon gebruiker niet bijwerken.',
system_err_login: 'Systeemfout. Probeer uit te loggen en weer in te loggen.', system_err_login:
'Systeemfout. Probeer uit te loggen en weer in te loggen.',
}, },
} };

View file

@ -93,7 +93,8 @@ export const ru = {
login_sign_up: 'Войти / Регистрация', login_sign_up: 'Войти / Регистрация',
login: 'Авторизация', login: 'Авторизация',
sign_up: 'Регистрация', sign_up: 'Регистрация',
notifications_error: 'Уведомления на рабочем столе недоступны в вашем браузере. Попробуйте Firefox или Chrome.', notifications_error:
'Уведомления на рабочем столе недоступны в вашем браузере. Попробуйте Firefox или Chrome.',
unread_messages: 'Непрочитанные сообщения', unread_messages: 'Непрочитанные сообщения',
password: 'Пароль', password: 'Пароль',
verify_password: 'Повторите пароль', verify_password: 'Повторите пароль',
@ -117,16 +118,19 @@ export const ru = {
show_nsfw: 'Показывать NSFW-контент', show_nsfw: 'Показывать NSFW-контент',
sponsors: 'Спонсоры', sponsors: 'Спонсоры',
sponsors_of_lemmy: 'Спонсоры Lemmy', sponsors_of_lemmy: 'Спонсоры Lemmy',
sponsor_message: 'Lemmy это бесплатное, <1>открытое</1> программное обеспечение, что означает отсутствие рекламы, монетизации или венчурного капитала, никогда. Ваши пожертвования напрямую поддерживают развитие проекта. Спасибо нижеуказанным людям:', sponsor_message:
'Lemmy это бесплатное, <1>открытое</1> программное обеспечение, что означает отсутствие рекламы, монетизации или венчурного капитала, никогда. Ваши пожертвования напрямую поддерживают развитие проекта. Спасибо нижеуказанным людям:',
support_on_patreon: 'Поддержать на Patreon', support_on_patreon: 'Поддержать на Patreon',
general_sponsors:'Генеральные спонсоры - это те, кто пообещал Lemmy от $10 до $39.', general_sponsors:
'Генеральные спонсоры - это те, кто пообещал Lemmy от $10 до $39.',
crypto: 'Крипто', crypto: 'Крипто',
bitcoin: 'Bitcoin', bitcoin: 'Bitcoin',
ethereum: 'Ethereum', ethereum: 'Ethereum',
code: 'Код', code: 'Код',
joined: 'Присоединился', joined: 'Присоединился',
powered_by: 'Работает на', powered_by: 'Работает на',
landing_0: 'Lemmy - это <1>агрегатор ссылок</1> / альтернатива reddit, предназначенный для работы в <2>федиверсе</2>.<3></3>Это самодостаточная система, с обновляемыми комментариями, и эта система крошечная (<4>~80 Кб</4>). Федерация в сети ActivityPub находится в разработке. <5></5>Это <6>очень ранняя бета-версия</6>, и многие функции в настоящее время сломаны или отсутствуют. <7></7>Предлагать новые функции или сообщать об ошибках можно <8>здесь.</8><9></9>Сделано на <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.', landing_0:
'Lemmy - это <1>агрегатор ссылок</1> / альтернатива reddit, предназначенный для работы в <2>федиверсе</2>.<3></3>Это самодостаточная система, с обновляемыми комментариями, и эта система крошечная (<4>~80 Кб</4>). Федерация в сети ActivityPub находится в разработке. <5></5>Это <6>очень ранняя бета-версия</6>, и многие функции в настоящее время сломаны или отсутствуют. <7></7>Предлагать новые функции или сообщать об ошибках можно <8>здесь.</8><9></9>Сделано на <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.',
not_logged_in: 'Не авторизованы.', not_logged_in: 'Не авторизованы.',
community_ban: 'Вы были заблокированы на данном сообществе.', community_ban: 'Вы были заблокированы на данном сообществе.',
site_ban: 'Вы были заблокированы на данном сайте', site_ban: 'Вы были заблокированы на данном сайте',
@ -153,13 +157,14 @@ export const ru = {
not_an_admin: 'Не администратор.', not_an_admin: 'Не администратор.',
site_already_exists: 'Сайт уже существует.', site_already_exists: 'Сайт уже существует.',
couldnt_update_site: 'Не получилось обновить сайт.', couldnt_update_site: 'Не получилось обновить сайт.',
couldnt_find_that_username_or_email: 'Не получилось найти данное имя пользователя или электронную почту.', couldnt_find_that_username_or_email:
'Не получилось найти данное имя пользователя или электронную почту.',
password_incorrect: 'Неверный пароль.', password_incorrect: 'Неверный пароль.',
passwords_dont_match: 'Пароли не совпадают.', passwords_dont_match: 'Пароли не совпадают.',
admin_already_created: 'Извините, уже есть администратор.', admin_already_created: 'Извините, уже есть администратор.',
user_already_exists: 'Пользователь уже существует.', user_already_exists: 'Пользователь уже существует.',
couldnt_update_user: 'Не получилось обновить пользователя.', couldnt_update_user: 'Не получилось обновить пользователя.',
system_err_login: 'Системная ошибка. Попробуйте выйти из системы и вернуться обратно.', system_err_login:
'Системная ошибка. Попробуйте выйти из системы и вернуться обратно.',
}, },
} };

View file

@ -56,7 +56,8 @@ export const sv = {
delete: 'radera', delete: 'radera',
deleted: 'raderad', deleted: 'raderad',
delete_account: 'Ta bort konto', delete_account: 'Ta bort konto',
delete_account_confirm: 'Varning: den här åtgärden kommer radera alla dina data permanent. Är du säker?', delete_account_confirm:
'Varning: den här åtgärden kommer radera alla dina data permanent. Är du säker?',
restore: 'återställ', restore: 'återställ',
ban: 'blockera', ban: 'blockera',
ban_from_site: 'blockera från webbplats', ban_from_site: 'blockera från webbplats',
@ -108,7 +109,8 @@ export const sv = {
login_sign_up: 'Logga in eller skapa konto', login_sign_up: 'Logga in eller skapa konto',
login: 'Logga in', login: 'Logga in',
sign_up: 'Skapa konto', sign_up: 'Skapa konto',
notifications_error: 'Din webbläsare har inte stöd för skrivbordsaviseringar. Testa Firefox eller Chrome.', notifications_error:
'Din webbläsare har inte stöd för skrivbordsaviseringar. Testa Firefox eller Chrome.',
unread_messages: 'Olästa meddelanden', unread_messages: 'Olästa meddelanden',
password: 'Lösenord', password: 'Lösenord',
verify_password: 'Bekräfta lösenord', verify_password: 'Bekräfta lösenord',
@ -134,9 +136,11 @@ export const sv = {
theme: 'Utseende', theme: 'Utseende',
sponsors: 'Sponsorer', sponsors: 'Sponsorer',
sponsors_of_lemmy: 'Lemmys sponsorer', sponsors_of_lemmy: 'Lemmys sponsorer',
sponsor_message: 'Lemmy är fri mjukvara med <1>öppen källkod</1>, vilket innebär att ingen reklam, vinstindrivning eller venturekapital förekommer, någonsin. Dina donationer går direkt till att stöda utvecklingen av projektet. Stort tack till följande personer:', sponsor_message:
'Lemmy är fri mjukvara med <1>öppen källkod</1>, vilket innebär att ingen reklam, vinstindrivning eller venturekapital förekommer, någonsin. Dina donationer går direkt till att stöda utvecklingen av projektet. Stort tack till följande personer:',
support_on_patreon: 'Stöd på Patreon', support_on_patreon: 'Stöd på Patreon',
general_sponsors: 'Allmänna sponsorer är dem som givit mellan 10 och 39\u00a0dollar till Lemmy.', general_sponsors:
'Allmänna sponsorer är dem som givit mellan 10 och 39\u00a0dollar till Lemmy.',
crypto: 'Kryptovaluta', crypto: 'Kryptovaluta',
bitcoin: 'Bitcoin', bitcoin: 'Bitcoin',
ethereum: 'Ethereum', ethereum: 'Ethereum',
@ -151,7 +155,8 @@ export const sv = {
yes: 'ja', yes: 'ja',
no: 'nej', no: 'nej',
powered_by: 'Drivs av', powered_by: 'Drivs av',
landing_0: 'Lemmy är en <1>länksamlare</1> och alternativ till reddit, ämnad att fungera i <2>Fediversumet</2>.<3></3>Lemmy kan drivas av vem som helst, har kommentarstrådar som updateras i realid och är mycket liten (<4>ca 80\u00a0kB</4>). Federering med ActivityPub-nätverket är planerat. <5></5>Detta är en <6>väldigt tidig betaversion</6> och många funktioner saknas därför eller är trasiga.<7></7>Föreslå nya funktioner eller anmäl buggar <8>här</8>.<9></9>Skapad i <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> och <13>Typescript</13>.', landing_0:
'Lemmy är en <1>länksamlare</1> och alternativ till reddit, ämnad att fungera i <2>Fediversumet</2>.<3></3>Lemmy kan drivas av vem som helst, har kommentarstrådar som updateras i realid och är mycket liten (<4>ca 80\u00a0kB</4>). Federering med ActivityPub-nätverket är planerat. <5></5>Detta är en <6>väldigt tidig betaversion</6> och många funktioner saknas därför eller är trasiga.<7></7>Föreslå nya funktioner eller anmäl buggar <8>här</8>.<9></9>Skapad i <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> och <13>Typescript</13>.',
not_logged_in: 'Inte inloggad.', not_logged_in: 'Inte inloggad.',
community_ban: 'Du har blockerats från den här gemenskapen.', community_ban: 'Du har blockerats från den här gemenskapen.',
site_ban: 'Du har blockerats från webbplatsen.', site_ban: 'Du har blockerats från webbplatsen.',
@ -178,7 +183,8 @@ export const sv = {
not_an_admin: 'Inte en administratör.', not_an_admin: 'Inte en administratör.',
site_already_exists: 'Webbplatsen finns redan.', site_already_exists: 'Webbplatsen finns redan.',
couldnt_update_site: 'Kunde inte uppdatera webbplats.', couldnt_update_site: 'Kunde inte uppdatera webbplats.',
couldnt_find_that_username_or_email: 'Kunde inte hitta det användarnamnet eller e-postadressen.', couldnt_find_that_username_or_email:
'Kunde inte hitta det användarnamnet eller e-postadressen.',
password_incorrect: 'Ogiltigt lösenord.', password_incorrect: 'Ogiltigt lösenord.',
passwords_dont_match: 'Lösenorden stämmer inte överens.', passwords_dont_match: 'Lösenorden stämmer inte överens.',
admin_already_created: 'Beklagar, men det finns redan en administratör.', admin_already_created: 'Beklagar, men det finns redan en administratör.',
@ -186,4 +192,4 @@ export const sv = {
couldnt_update_user: 'Kunde inte uppdatera användare.', couldnt_update_user: 'Kunde inte uppdatera användare.',
system_err_login: 'Systemfel. Försök att logga ut och sedan in igen.', system_err_login: 'Systemfel. Försök att logga ut och sedan in igen.',
}, },
} };

View file

@ -113,16 +113,19 @@ export const zh = {
modified: '修改', modified: '修改',
sponsors: 'Sponsors', sponsors: 'Sponsors',
sponsors_of_lemmy: 'Sponsors of Lemmy', sponsors_of_lemmy: 'Sponsors of Lemmy',
sponsor_message: 'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:', sponsor_message:
'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
support_on_patreon: 'Support on Patreon', support_on_patreon: 'Support on Patreon',
general_sponsors:'General Sponsors are those that pledged $10 to $39 to Lemmy.', general_sponsors:
'General Sponsors are those that pledged $10 to $39 to Lemmy.',
crypto: '加密', crypto: '加密',
bitcoin: '比特币', bitcoin: '比特币',
ethereum: '以太币', ethereum: '以太币',
code: '代码', code: '代码',
joined: '已加入', joined: '已加入',
powered_by: '保留所有权利', powered_by: '保留所有权利',
landing_0: 'Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It\'s self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.', landing_0:
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
not_logged_in: '未登录.', not_logged_in: '未登录.',
community_ban: '你被此节点禁止.', community_ban: '你被此节点禁止.',
site_ban: '你被此站点禁止', site_ban: '你被此站点禁止',
@ -157,5 +160,4 @@ export const zh = {
couldnt_update_user: '不可以更新用户.', couldnt_update_user: '不可以更新用户.',
system_err_login: '系统错误. 尝试注销再登录', system_err_login: '系统错误. 尝试注销再登录',
}, },
} };

88
ui/src/utils.ts vendored
View file

@ -7,7 +7,14 @@ import 'moment/locale/sv';
import 'moment/locale/ru'; import 'moment/locale/ru';
import 'moment/locale/nl'; import 'moment/locale/nl';
import { UserOperation, Comment, User, SortType, ListingType, SearchType } from './interfaces'; import {
UserOperation,
Comment,
User,
SortType,
ListingType,
SearchType,
} from './interfaces';
import * as markdown_it from 'markdown-it'; import * as markdown_it from 'markdown-it';
import * as markdownitEmoji from 'markdown-it-emoji/light'; import * as markdownitEmoji from 'markdown-it-emoji/light';
import * as markdown_it_container from 'markdown-it-container'; import * as markdown_it_container from 'markdown-it-container';
@ -21,7 +28,12 @@ export const postRefetchSeconds: number = 60*1000;
export const fetchLimit: number = 20; export const fetchLimit: number = 20;
export const mentionDropdownFetchLimit = 6; export const mentionDropdownFetchLimit = 6;
export function randomStr() {return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10)} 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;
@ -31,8 +43,9 @@ export function msgOp(msg: any): UserOperation {
export const md = new markdown_it({ export const md = new markdown_it({
html: false, html: false,
linkify: true, linkify: true,
typographer: true typographer: true,
}).use(markdown_it_container, 'spoiler', { })
.use(markdown_it_container, 'spoiler', {
validate: function(params: any) { validate: function(params: any) {
return params.trim().match(/^spoiler\s+(.*)$/); return params.trim().match(/^spoiler\s+(.*)$/);
}, },
@ -42,15 +55,17 @@ export const md = new markdown_it({
if (tokens[idx].nesting === 1) { if (tokens[idx].nesting === 1) {
// opening tag // opening tag
return '<details><summary>' + md.utils.escapeHtml(m[1]) + '</summary>\n'; return (
'<details><summary>' + md.utils.escapeHtml(m[1]) + '</summary>\n'
);
} else { } else {
// closing tag // closing tag
return '</details>\n'; return '</details>\n';
} }
} },
}).use(markdownitEmoji, { })
defs: objectFlip(emojiShortName) .use(markdownitEmoji, {
defs: objectFlip(emojiShortName),
}); });
md.renderer.rules.emoji = function(token, idx) { md.renderer.rules.emoji = function(token, idx) {
@ -64,7 +79,9 @@ export function hotRank(comment: Comment): number {
let now: Date = new Date(); let now: Date = new Date();
let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5; let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
let rank = (10000 * Math.log10(Math.max(1, 3 + comment.score))) / Math.pow(hoursElapsed + 2, 1.8); let rank =
(10000 * Math.log10(Math.max(1, 3 + comment.score))) /
Math.pow(hoursElapsed + 2, 1.8);
// console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`); // console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`);
@ -79,11 +96,21 @@ export function getUnixTime(text: string): number {
return text ? new Date(text).getTime() / 1000 : undefined; return text ? new Date(text).getTime() / 1000 : undefined;
} }
export function addTypeInfo<T>(arr: Array<T>, name: string): Array<{type_: string, data: T}> { export function addTypeInfo<T>(
return arr.map(e => {return {type_: name, data: e}}); arr: Array<T>,
name: string
): Array<{ type_: string; data: T }> {
return arr.map(e => {
return { type_: name, data: e };
});
} }
export function canMod(user: User, modIds: Array<number>, creator_id: number, onSelf: boolean = false): boolean { export function canMod(
user: User,
modIds: Array<number>,
creator_id: number,
onSelf: boolean = false
): boolean {
// You can do moderator actions only on the mods added after you. // You can do moderator actions only on the mods added after you.
if (user) { if (user) {
let yourIndex = modIds.findIndex(id => id == user.id); let yourIndex = modIds.findIndex(id => id == user.id);
@ -103,8 +130,9 @@ export function isMod(modIds: Array<number>, creator_id: number): boolean {
return modIds.includes(creator_id); return modIds.includes(creator_id);
} }
var imageRegex = new RegExp(
var imageRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:png|jpg|jpeg|gif|png|svg))`); `(http)?s?:?(\/\/[^"']*\.(?:png|jpg|jpeg|gif|png|svg))`
);
var videoRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:mp4))`); var videoRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:mp4))`);
export function isImage(url: string) { export function isImage(url: string) {
@ -127,7 +155,6 @@ export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1); return str.charAt(0).toUpperCase() + str.slice(1);
} }
export function routeSortTypeToEnum(sort: string): SortType { export function routeSortTypeToEnum(sort: string): SortType {
if (sort == 'new') { if (sort == 'new') {
return SortType.New; return SortType.New;
@ -158,7 +185,11 @@ export async function getPageTitle(url: string) {
return data; return data;
} }
export function debounce(func: any, wait: number = 500, immediate: boolean = false) { export function debounce(
func: any,
wait: number = 500,
immediate: boolean = false
) {
// 'private' variable for instance // 'private' variable for instance
// The returned function will be able to reference this due to closure. // The returned function will be able to reference this due to closure.
// Each call to the returned function will share this common timer. // Each call to the returned function will share this common timer.
@ -182,7 +213,6 @@ export function debounce(func: any, wait: number = 500, immediate: boolean = fal
// Set the new timeout // Set the new timeout
timeout = setTimeout(function() { timeout = setTimeout(function() {
// Inside the timeout function, clear the timeout variable // Inside the timeout function, clear the timeout variable
// which will let the next execution run when in 'immediate' mode // which will let the next execution run when in 'immediate' mode
timeout = null; timeout = null;
@ -198,16 +228,16 @@ export function debounce(func: any, wait: number = 500, immediate: boolean = fal
// Immediate mode and no wait timer? Execute the function.. // Immediate mode and no wait timer? Execute the function..
if (callNow) func.apply(context, args); if (callNow) func.apply(context, args);
} };
} }
export function getLanguage(): string { export function getLanguage(): string {
return (navigator.language || navigator.userLanguage); return navigator.language || navigator.userLanguage;
} }
export function objectFlip(obj: any) { export function objectFlip(obj: any) {
const ret = {}; const ret = {};
Object.keys(obj).forEach((key) => { Object.keys(obj).forEach(key => {
ret[obj[key]] = key; ret[obj[key]] = key;
}); });
return ret; return ret;
@ -237,16 +267,24 @@ export function getMomentLanguage(): string {
return lang; return lang;
} }
export const themes = ['litera', 'minty', 'solar', 'united', 'cyborg','darkly', 'journal', 'sketchy']; export const themes = [
'litera',
'minty',
'solar',
'united',
'cyborg',
'darkly',
'journal',
'sketchy',
];
export function setTheme(theme: string = 'darkly') { export function setTheme(theme: string = 'darkly') {
for (var i = 0; i < themes.length; i++) { for (var i = 0; i < themes.length; i++) {
let styleSheet = document.getElementById(themes[i]); let styleSheet = document.getElementById(themes[i]);
if (themes[i] == theme) { if (themes[i] == theme) {
styleSheet.removeAttribute("disabled"); styleSheet.removeAttribute('disabled');
} else { } else {
styleSheet.setAttribute("disabled", "disabled"); styleSheet.setAttribute('disabled', 'disabled');
} }
} }
} }

2
ui/src/version.ts vendored
View file

@ -1 +1 @@
export let version: string = "v0.3.0.4-0-gce10a34"; export let version: string = 'v0.3.0.4-0-gce10a34';