mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-23 12:51:18 +00:00
parent
e260e19a50
commit
97e21f6fd9
43 changed files with 4523 additions and 2059 deletions
162
ui/src/components/comment-form.tsx
vendored
162
ui/src/components/comment-form.tsx
vendored
|
@ -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 {
|
||||||
|
|
562
ui/src/components/comment-node.tsx
vendored
562
ui/src/components/comment-node.tsx
vendored
|
@ -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;
|
||||||
|
|
26
ui/src/components/comment-nodes.tsx
vendored
26
ui/src/components/comment-nodes.tsx
vendored
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
151
ui/src/components/communities.tsx
vendored
151
ui/src/components/communities.tsx
vendored
|
@ -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,9 +243,11 @@ 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);
|
||||||
let table = document.querySelector('#community_table');
|
let table = document.querySelector('#community_table');
|
||||||
Sortable.initTable(table);
|
Sortable.initTable(table);
|
||||||
|
|
132
ui/src/components/community-form.tsx
vendored
132
ui/src/components/community-form.tsx
vendored
|
@ -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>
|
||||||
|
@ -174,7 +247,7 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
return;
|
return;
|
||||||
} else if (op == UserOperation.ListCategories){
|
} else if (op == UserOperation.ListCategories) {
|
||||||
let res: ListCategoriesResponse = msg;
|
let res: ListCategoriesResponse = msg;
|
||||||
this.state.categories = res.categories;
|
this.state.categories = res.categories;
|
||||||
if (!this.props.community) {
|
if (!this.props.community) {
|
||||||
|
@ -193,5 +266,4 @@ export class CommunityForm extends Component<CommunityFormProps, CommunityFormSt
|
||||||
this.props.onEdit(res.community);
|
this.props.onEdit(res.community);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
155
ui/src/components/community.tsx
vendored
155
ui/src/components/community.tsx
vendored
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -163,7 +226,7 @@ export class Community extends Component<any, State> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchPosts();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPage(i: Community) {
|
prevPage(i: Community) {
|
||||||
|
@ -171,7 +234,7 @@ export class Community extends Component<any, State> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchPosts();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortChange(i: Community, event: any) {
|
handleSortChange(i: Community, event: any) {
|
||||||
|
@ -180,12 +243,14 @@ export class Community extends Component<any, State> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchPosts();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
15
ui/src/components/create-community.tsx
vendored
15
ui/src/components/create-community.tsx
vendored
|
@ -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>
|
||||||
<CommunityForm onCreate={this.handleCommunityCreate}/>
|
<T i18nKey="create_community">#</T>
|
||||||
|
</h5>
|
||||||
|
<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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
25
ui/src/components/create-post.tsx
vendored
25
ui/src/components/create-post.tsx
vendored
|
@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
19
ui/src/components/footer.tsx
vendored
19
ui/src/components/footer.tsx
vendored
|
@ -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> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
156
ui/src/components/inbox.tsx
vendored
156
ui/src/components/inbox.tsx
vendored
|
@ -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,11 +228,14 @@ 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();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.EditComment) {
|
} else if (op == UserOperation.EditComment) {
|
||||||
let res: CommentResponse = msg;
|
let res: CommentResponse = msg;
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
158
ui/src/components/login.tsx
vendored
158
ui/src/components/login.tsx
vendored
|
@ -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> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
371
ui/src/components/main.tsx
vendored
371
ui/src/components/main.tsx
vendored
|
@ -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) {
|
||||||
|
@ -364,7 +531,7 @@ export class Main extends Component<any, MainState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchPosts();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
prevPage(i: Main) {
|
prevPage(i: Main) {
|
||||||
|
@ -373,7 +540,7 @@ export class Main extends Component<any, MainState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchPosts();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSortChange(i: Main, event: any) {
|
handleSortChange(i: Main, event: any) {
|
||||||
|
@ -383,7 +550,7 @@ export class Main extends Component<any, MainState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchPosts();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTypeChange(i: Main, event: any) {
|
handleTypeChange(i: Main, event: any) {
|
||||||
|
@ -393,7 +560,7 @@ export class Main extends Component<any, MainState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchPosts();
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
keepFetchingPosts() {
|
keepFetchingPosts() {
|
||||||
|
@ -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> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
371
ui/src/components/modlog.tsx
vendored
371
ui/src/components/modlog.tsx
vendored
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -219,7 +420,7 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
i.refetch();
|
i.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
refetch(){
|
refetch() {
|
||||||
let modlogForm: GetModlogForm = {
|
let modlogForm: GetModlogForm = {
|
||||||
community_id: this.state.communityId,
|
community_id: this.state.communityId,
|
||||||
page: this.state.page,
|
page: this.state.page,
|
||||||
|
@ -237,7 +438,7 @@ export class Modlog extends Component<any, ModlogState> {
|
||||||
} else if (op == UserOperation.GetModlog) {
|
} else if (op == UserOperation.GetModlog) {
|
||||||
let res: GetModlogResponse = msg;
|
let res: GetModlogResponse = msg;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
window.scrollTo(0,0);
|
window.scrollTo(0, 0);
|
||||||
this.setCombined(res);
|
this.setCombined(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
ui/src/components/moment-time.tsx
vendored
13
ui/src/components/moment-time.tsx
vendored
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
157
ui/src/components/navbar.tsx
vendored
157
ui/src/components/navbar.tsx
vendored
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,7 +258,7 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 9999,
|
limit: 9999,
|
||||||
};
|
};
|
||||||
if (this.currentLocation !=='/inbox') {
|
if (this.currentLocation !== '/inbox') {
|
||||||
WebSocketService.Instance.getReplies(repliesForm);
|
WebSocketService.Instance.getReplies(repliesForm);
|
||||||
this.state.fetchCount++;
|
this.state.fetchCount++;
|
||||||
}
|
}
|
||||||
|
@ -205,12 +270,15 @@ 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() {
|
||||||
if (UserService.Instance.user) {
|
if (UserService.Instance.user) {
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (!Notification) {
|
if (!Notification) {
|
||||||
alert(i18n.t('notifications_error'));
|
alert(i18n.t('notifications_error'));
|
||||||
return;
|
return;
|
||||||
|
@ -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}`
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
257
ui/src/components/post-form.tsx
vendored
257
ui/src/components/post-form.tsx
vendored
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
672
ui/src/components/post-listing.tsx
vendored
672
ui/src/components/post-listing.tsx
vendored
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
30
ui/src/components/post-listings.tsx
vendored
30
ui/src/components/post-listings.tsx
vendored
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
209
ui/src/components/post.tsx
vendored
209
ui/src/components/post.tsx
vendored
|
@ -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,51 +194,74 @@ 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
|
||||||
locked={this.state.post.locked}
|
locked={this.state.post.locked}
|
||||||
moderators={this.state.moderators}
|
moderators={this.state.moderators}
|
||||||
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,16 +286,15 @@ 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 });
|
||||||
}
|
}
|
||||||
let tree: Array<CommentNodeI> = [];
|
let tree: Array<CommentNodeI> = [];
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
348
ui/src/components/search.tsx
vendored
348
ui/src/components/search.tsx
vendored
|
@ -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} - ${
|
||||||
window.scrollTo(0,0);
|
WebSocketService.Instance.site.name
|
||||||
|
}`;
|
||||||
|
window.scrollTo(0, 0);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
101
ui/src/components/setup.tsx
vendored
101
ui/src/components/setup.tsx
vendored
|
@ -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;
|
||||||
|
|
227
ui/src/components/sidebar.tsx
vendored
227
ui/src/components/sidebar.tsx
vendored
|
@ -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) {
|
||||||
|
|
65
ui/src/components/site-form.tsx
vendored
65
ui/src/components/site-form.tsx
vendored
|
@ -17,12 +17,12 @@ 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>
|
||||||
|
|
79
ui/src/components/sponsors.tsx
vendored
79
ui/src/components/sponsors.tsx
vendored
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
21
ui/src/components/symbols.tsx
vendored
21
ui/src/components/symbols.tsx
vendored
|
@ -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"
|
||||||
|
|
460
ui/src/components/user.tsx
vendored
460
ui/src/components/user.tsx
vendored
|
@ -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,11 +680,14 @@ 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);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
} else if (op == UserOperation.EditComment) {
|
} else if (op == UserOperation.EditComment) {
|
||||||
let res: CommentResponse = msg;
|
let res: CommentResponse = msg;
|
||||||
|
@ -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
6
ui/src/env.ts
vendored
|
@ -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
10
ui/src/i18next.ts
vendored
|
@ -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
27
ui/src/index.tsx
vendored
|
@ -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
266
ui/src/interfaces.ts
vendored
|
@ -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 {
|
||||||
|
|
24
ui/src/services/UserService.ts
vendored
24
ui/src/services/UserService.ts
vendored
|
@ -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,30 +24,30 @@ 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) {
|
||||||
this.user = jwt_decode(jwt);
|
this.user = jwt_decode(jwt);
|
||||||
setTheme(this.user.theme);
|
setTheme(this.user.theme);
|
||||||
this.sub.next({user: this.user, unreadCount: 0});
|
this.sub.next({ user: this.user, unreadCount: 0 });
|
||||||
console.log(this.user);
|
console.log(this.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get Instance(){
|
public static get Instance() {
|
||||||
return this._instance || (this._instance = new this());
|
return this._instance || (this._instance = new this());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
101
ui/src/services/WebSocketService.ts
vendored
101
ui/src/services/WebSocketService.ts
vendored
|
@ -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,13 +48,20 @@ 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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get Instance(){
|
public static get Instance() {
|
||||||
return this._instance || (this._instance = new this());
|
return this._instance || (this._instance = new this());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -58,12 +100,16 @@ 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) {
|
||||||
|
@ -72,33 +118,39 @@ export class WebSocketService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPost(postId: number) {
|
public getPost(postId: number) {
|
||||||
let data = {id: postId, auth: UserService.Instance.auth };
|
let data = { id: postId, auth: UserService.Instance.auth };
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetPost, data));
|
this.subject.next(this.wsSendWrapper(UserOperation.GetPost, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCommunity(communityId: number) {
|
public getCommunity(communityId: number) {
|
||||||
let data = {id: communityId, auth: UserService.Instance.auth };
|
let data = { id: communityId, auth: UserService.Instance.auth };
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetCommunity, data));
|
this.subject.next(this.wsSendWrapper(UserOperation.GetCommunity, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCommunityByName(name: string) {
|
public getCommunityByName(name: string) {
|
||||||
let data = {name: name, auth: UserService.Instance.auth };
|
let data = { name: name, auth: UserService.Instance.auth };
|
||||||
this.subject.next(this.wsSendWrapper(UserOperation.GetCommunity, data));
|
this.subject.next(this.wsSendWrapper(UserOperation.GetCommunity, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
|
38
ui/src/translations/de.ts
vendored
38
ui/src/translations/de.ts
vendored
|
@ -5,18 +5,18 @@ export const de = {
|
||||||
no_posts: 'Keine Beiträge.',
|
no_posts: 'Keine Beiträge.',
|
||||||
create_a_post: 'Einen Beitrag anlegen',
|
create_a_post: 'Einen Beitrag anlegen',
|
||||||
create_post: 'Beitrag anlegen',
|
create_post: 'Beitrag anlegen',
|
||||||
number_of_posts:'{{count}} Beiträge',
|
number_of_posts: '{{count}} Beiträge',
|
||||||
posts: 'Beiträge',
|
posts: 'Beiträge',
|
||||||
related_posts: 'Diese Beiträge könnten verwandt sein',
|
related_posts: 'Diese Beiträge könnten verwandt sein',
|
||||||
comments: 'Kommentare',
|
comments: 'Kommentare',
|
||||||
number_of_comments:'{{count}} Kommentare',
|
number_of_comments: '{{count}} Kommentare',
|
||||||
remove_comment: 'Kommentar löschen',
|
remove_comment: 'Kommentar löschen',
|
||||||
communities: 'Communities',
|
communities: 'Communities',
|
||||||
create_a_community: 'Eine community anlegen',
|
create_a_community: 'Eine community anlegen',
|
||||||
create_community: 'Community anlegen',
|
create_community: 'Community anlegen',
|
||||||
remove_community: 'Community entfernen',
|
remove_community: 'Community entfernen',
|
||||||
subscribed_to_communities:'Abonnierte <1>communities</1>',
|
subscribed_to_communities: 'Abonnierte <1>communities</1>',
|
||||||
trending_communities:'Trending <1>communities</1>',
|
trending_communities: 'Trending <1>communities</1>',
|
||||||
list_of_communities: 'Liste von communities',
|
list_of_communities: 'Liste von communities',
|
||||||
community_reqs: 'Kleinbuchstaben, Großbuchstaben und keine Leerzeichen.',
|
community_reqs: 'Kleinbuchstaben, Großbuchstaben und keine Leerzeichen.',
|
||||||
edit: 'editieren',
|
edit: 'editieren',
|
||||||
|
@ -53,9 +53,9 @@ export const de = {
|
||||||
create: 'anlegen',
|
create: 'anlegen',
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
email_or_username: 'Email oder Username',
|
email_or_username: 'Email oder Username',
|
||||||
number_of_users:'{{count}} Benutzer',
|
number_of_users: '{{count}} Benutzer',
|
||||||
number_of_subscribers:'{{count}} Abonnenten',
|
number_of_subscribers: '{{count}} Abonnenten',
|
||||||
number_of_points:'{{count}} Punkte',
|
number_of_points: '{{count}} Punkte',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
title: 'Titel',
|
title: 'Titel',
|
||||||
category: 'Kategorie',
|
category: 'Kategorie',
|
||||||
|
@ -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.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
51
ui/src/translations/en.ts
vendored
51
ui/src/translations/en.ts
vendored
|
@ -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.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
32
ui/src/translations/eo.ts
vendored
32
ui/src/translations/eo.ts
vendored
|
@ -5,21 +5,21 @@ export const eo = {
|
||||||
no_posts: 'Ne Poŝtoj.',
|
no_posts: 'Ne Poŝtoj.',
|
||||||
create_a_post: 'Verki Poŝton',
|
create_a_post: 'Verki Poŝton',
|
||||||
create_post: 'Verki Poŝton',
|
create_post: 'Verki Poŝton',
|
||||||
number_of_posts:'{{count}} Poŝtoj',
|
number_of_posts: '{{count}} Poŝtoj',
|
||||||
posts: 'Poŝtoj',
|
posts: 'Poŝtoj',
|
||||||
related_posts: 'Tiuj poŝtoj eble rilatas',
|
related_posts: 'Tiuj poŝtoj eble rilatas',
|
||||||
cross_posts: 'Tiuj ligilo ankaŭ estas poŝtinta al:',
|
cross_posts: 'Tiuj ligilo ankaŭ estas poŝtinta al:',
|
||||||
cross_post: 'laŭapoŝto',
|
cross_post: 'laŭapoŝto',
|
||||||
comments: 'Komentoj',
|
comments: 'Komentoj',
|
||||||
number_of_comments:'{{count}} Komentoj',
|
number_of_comments: '{{count}} Komentoj',
|
||||||
remove_comment: 'Fortiri Komentojn',
|
remove_comment: 'Fortiri Komentojn',
|
||||||
communities: 'Komunumoj',
|
communities: 'Komunumoj',
|
||||||
users: 'Uzantoj',
|
users: 'Uzantoj',
|
||||||
create_a_community: 'Krei komunumon',
|
create_a_community: 'Krei komunumon',
|
||||||
create_community: 'Krei Komunumon',
|
create_community: 'Krei Komunumon',
|
||||||
remove_community: 'Forigi Komunumon',
|
remove_community: 'Forigi Komunumon',
|
||||||
subscribed_to_communities:'Abonita al <1>komunumoj</1>',
|
subscribed_to_communities: 'Abonita al <1>komunumoj</1>',
|
||||||
trending_communities:'Furora <1>komunumoj</1>',
|
trending_communities: 'Furora <1>komunumoj</1>',
|
||||||
list_of_communities: 'Listo de komunumoj',
|
list_of_communities: 'Listo de komunumoj',
|
||||||
community_reqs: 'minusklaj leteroj, substrekoj, kaj ne spacetoj.',
|
community_reqs: 'minusklaj leteroj, substrekoj, kaj ne spacetoj.',
|
||||||
edit: 'redakti',
|
edit: 'redakti',
|
||||||
|
@ -57,9 +57,9 @@ export const eo = {
|
||||||
create: 'krei',
|
create: 'krei',
|
||||||
username: 'Uzantnomo',
|
username: 'Uzantnomo',
|
||||||
email_or_username: 'Retadreso aŭ Uzantnomo',
|
email_or_username: 'Retadreso aŭ Uzantnomo',
|
||||||
number_of_users:'{{count}} Uzantoj',
|
number_of_users: '{{count}} Uzantoj',
|
||||||
number_of_subscribers:'{{count}} Abonantoj',
|
number_of_subscribers: '{{count}} Abonantoj',
|
||||||
number_of_points:'{{count}} Voĉdonoj',
|
number_of_points: '{{count}} Voĉdonoj',
|
||||||
name: 'Nomo',
|
name: 'Nomo',
|
||||||
title: 'Titolo',
|
title: 'Titolo',
|
||||||
category: 'Kategorio',
|
category: 'Kategorio',
|
||||||
|
@ -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.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
49
ui/src/translations/es.ts
vendored
49
ui/src/translations/es.ts
vendored
|
@ -5,23 +5,23 @@ export const es = {
|
||||||
no_posts: 'Sin publicaciones.',
|
no_posts: 'Sin publicaciones.',
|
||||||
create_a_post: 'Crear una publicación',
|
create_a_post: 'Crear una publicación',
|
||||||
create_post: 'Crear Publicación',
|
create_post: 'Crear Publicación',
|
||||||
number_of_posts:'{{count}} Publicaciones',
|
number_of_posts: '{{count}} Publicaciones',
|
||||||
posts: 'Publicaciones',
|
posts: 'Publicaciones',
|
||||||
related_posts: 'Estas publicaciones podrían estar relacionadas',
|
related_posts: 'Estas publicaciones podrían estar relacionadas',
|
||||||
cross_posts: 'Este link también ha sido publicado en:',
|
cross_posts: 'Este link también ha sido publicado en:',
|
||||||
cross_post: 'cross-post',
|
cross_post: 'cross-post',
|
||||||
comments: 'Comentarios',
|
comments: 'Comentarios',
|
||||||
number_of_comments:'{{count}} Comentarios',
|
number_of_comments: '{{count}} Comentarios',
|
||||||
remove_comment: 'Remover Comentarios',
|
remove_comment: 'Remover Comentarios',
|
||||||
communities: 'Comunidades',
|
communities: 'Comunidades',
|
||||||
users: 'Usuarios',
|
users: 'Usuarios',
|
||||||
create_a_community: 'Crear una comunidad',
|
create_a_community: 'Crear una comunidad',
|
||||||
create_community: 'Crear Comunidad',
|
create_community: 'Crear Comunidad',
|
||||||
remove_community: 'Remover Comunidad',
|
remove_community: 'Remover Comunidad',
|
||||||
subscribed_to_communities:'Suscrito a <1>comunidades</1>',
|
subscribed_to_communities: 'Suscrito a <1>comunidades</1>',
|
||||||
trending_communities:'<1>Comunidades</1> en tendencia',
|
trending_communities: '<1>Comunidades</1> en tendencia',
|
||||||
list_of_communities: 'Lista de comunidades',
|
list_of_communities: 'Lista de comunidades',
|
||||||
number_of_communities:'{{count}} Comunidades',
|
number_of_communities: '{{count}} Comunidades',
|
||||||
community_reqs: 'minúsculas, guión bajo, y sin espacios.',
|
community_reqs: 'minúsculas, guión bajo, y sin espacios.',
|
||||||
edit: 'editar',
|
edit: 'editar',
|
||||||
reply: 'responder',
|
reply: 'responder',
|
||||||
|
@ -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',
|
||||||
|
@ -69,9 +70,9 @@ export const es = {
|
||||||
creator: 'creador',
|
creator: 'creador',
|
||||||
username: 'Nombre de Usuario',
|
username: 'Nombre de Usuario',
|
||||||
email_or_username: 'Correo electrónico o Nombre de Usuario',
|
email_or_username: 'Correo electrónico o Nombre de Usuario',
|
||||||
number_of_users:'{{count}} Usuarios',
|
number_of_users: '{{count}} Usuarios',
|
||||||
number_of_subscribers:'{{count}} Suscriptores',
|
number_of_subscribers: '{{count}} Suscriptores',
|
||||||
number_of_points:'{{count}} Puntos',
|
number_of_points: '{{count}} Puntos',
|
||||||
number_online: '{{count}} Usaurios En Línea',
|
number_online: '{{count}} Usaurios En Línea',
|
||||||
name: 'Nombre',
|
name: 'Nombre',
|
||||||
title: 'Titulo',
|
title: 'Titulo',
|
||||||
|
@ -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.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
75
ui/src/translations/fr.ts
vendored
75
ui/src/translations/fr.ts
vendored
|
@ -5,21 +5,21 @@ export const fr = {
|
||||||
no_posts: 'Pas de sujets.',
|
no_posts: 'Pas de sujets.',
|
||||||
create_a_post: 'Créer un sujet',
|
create_a_post: 'Créer un sujet',
|
||||||
create_post: 'Créer le sujet',
|
create_post: 'Créer le sujet',
|
||||||
number_of_posts:'{{count}} Sujets',
|
number_of_posts: '{{count}} Sujets',
|
||||||
posts: 'Sujets',
|
posts: 'Sujets',
|
||||||
related_posts: 'Ces sujets peuvent être corrélés',
|
related_posts: 'Ces sujets peuvent être corrélés',
|
||||||
cross_posts: 'Ce sujet a également été posté sur :',
|
cross_posts: 'Ce sujet a également été posté sur :',
|
||||||
cross_post: 'crossposter',
|
cross_post: 'crossposter',
|
||||||
comments: 'Commentaires',
|
comments: 'Commentaires',
|
||||||
number_of_comments:'{{count}} Commentaires',
|
number_of_comments: '{{count}} Commentaires',
|
||||||
remove_comment: 'Supprimer le commentaire',
|
remove_comment: 'Supprimer le commentaire',
|
||||||
communities: 'Communautés',
|
communities: 'Communautés',
|
||||||
users: 'Utilisateurs',
|
users: 'Utilisateurs',
|
||||||
create_a_community: 'Créer une communauté',
|
create_a_community: 'Créer une communauté',
|
||||||
create_community: 'Créer la communauté',
|
create_community: 'Créer la communauté',
|
||||||
remove_community: 'Supprimer la Communauté',
|
remove_community: 'Supprimer la Communauté',
|
||||||
subscribed_to_communities:'Abonné à ces <1>communautés</1>',
|
subscribed_to_communities: 'Abonné à ces <1>communautés</1>',
|
||||||
trending_communities:'<1>Communauté</1> en vogue',
|
trending_communities: '<1>Communauté</1> en vogue',
|
||||||
list_of_communities: 'Liste des communautés',
|
list_of_communities: 'Liste des communautés',
|
||||||
number_of_communities: '{{count}} communautés',
|
number_of_communities: '{{count}} communautés',
|
||||||
community_reqs: 'en minuscule, sans espace et avec tiret du bas.',
|
community_reqs: 'en minuscule, sans espace et avec tiret du bas.',
|
||||||
|
@ -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,11 +68,11 @@ 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',
|
||||||
number_online: '{{count}} Utilisateurs en ligne',
|
number_online: '{{count}} Utilisateurs en ligne',
|
||||||
name: 'Nom',
|
name: 'Nom',
|
||||||
title: 'Titre',
|
title: 'Titre',
|
||||||
|
@ -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.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
36
ui/src/translations/nl.ts
vendored
36
ui/src/translations/nl.ts
vendored
|
@ -5,23 +5,23 @@ export const nl = {
|
||||||
no_posts: 'Geen posts.',
|
no_posts: 'Geen posts.',
|
||||||
create_a_post: 'Plaats een post',
|
create_a_post: 'Plaats een post',
|
||||||
create_post: 'Plaats post',
|
create_post: 'Plaats post',
|
||||||
number_of_posts:'{{count}} posts',
|
number_of_posts: '{{count}} posts',
|
||||||
posts: 'posts',
|
posts: 'posts',
|
||||||
related_posts: 'Deze posts kunnen gerelateerd zijn',
|
related_posts: 'Deze posts kunnen gerelateerd zijn',
|
||||||
cross_posts: 'Deze link is ook geplaatst in:',
|
cross_posts: 'Deze link is ook geplaatst in:',
|
||||||
cross_post: 'cross-post',
|
cross_post: 'cross-post',
|
||||||
comments: 'Reacties',
|
comments: 'Reacties',
|
||||||
number_of_comments:'{{count}} reacties',
|
number_of_comments: '{{count}} reacties',
|
||||||
remove_comment: 'Verwijder reactie',
|
remove_comment: 'Verwijder reactie',
|
||||||
communities: 'Communities',
|
communities: 'Communities',
|
||||||
users: 'Gebruikers',
|
users: 'Gebruikers',
|
||||||
create_a_community: 'Maak een community',
|
create_a_community: 'Maak een community',
|
||||||
create_community: 'Maak community',
|
create_community: 'Maak community',
|
||||||
remove_community: 'Verwijder community',
|
remove_community: 'Verwijder community',
|
||||||
subscribed_to_communities:'Geabonneerd op <1>communities</1>',
|
subscribed_to_communities: 'Geabonneerd op <1>communities</1>',
|
||||||
trending_communities:'Populaire <1>communities</1>',
|
trending_communities: 'Populaire <1>communities</1>',
|
||||||
list_of_communities: 'Lijst van communities',
|
list_of_communities: 'Lijst van communities',
|
||||||
number_of_communities:'{{count}} communities',
|
number_of_communities: '{{count}} communities',
|
||||||
community_reqs: 'kleine letters, onderstrepingsteken en geen spaties',
|
community_reqs: 'kleine letters, onderstrepingsteken en geen spaties',
|
||||||
edit: 'bewerk',
|
edit: 'bewerk',
|
||||||
reply: 'reageer',
|
reply: 'reageer',
|
||||||
|
@ -58,9 +58,9 @@ export const nl = {
|
||||||
create: 'maak',
|
create: 'maak',
|
||||||
username: 'Gebruikersnaam',
|
username: 'Gebruikersnaam',
|
||||||
email_or_username: 'E-mail of gebruikersnaam',
|
email_or_username: 'E-mail of gebruikersnaam',
|
||||||
number_of_users:'{{count}} gebruikers',
|
number_of_users: '{{count}} gebruikers',
|
||||||
number_of_subscribers:'{{count}} abonnees',
|
number_of_subscribers: '{{count}} abonnees',
|
||||||
number_of_points:'{{count}} punten',
|
number_of_points: '{{count}} punten',
|
||||||
name: 'Naam',
|
name: 'Naam',
|
||||||
title: 'Titel',
|
title: 'Titel',
|
||||||
category: 'Categorie',
|
category: 'Categorie',
|
||||||
|
@ -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.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
35
ui/src/translations/ru.ts
vendored
35
ui/src/translations/ru.ts
vendored
|
@ -5,19 +5,19 @@ export const ru = {
|
||||||
no_posts: 'Нет записей.',
|
no_posts: 'Нет записей.',
|
||||||
create_a_post: 'Создать запись',
|
create_a_post: 'Создать запись',
|
||||||
create_post: 'Создать запись',
|
create_post: 'Создать запись',
|
||||||
number_of_posts:'{{count}} записей',
|
number_of_posts: '{{count}} записей',
|
||||||
posts: 'Записи',
|
posts: 'Записи',
|
||||||
related_posts: 'Эти записи могут быть связаны',
|
related_posts: 'Эти записи могут быть связаны',
|
||||||
comments: 'Комментарии',
|
comments: 'Комментарии',
|
||||||
number_of_comments:'{{count}} комментариев',
|
number_of_comments: '{{count}} комментариев',
|
||||||
remove_comment: 'Удалить комментарий',
|
remove_comment: 'Удалить комментарий',
|
||||||
communities: 'Сообщества',
|
communities: 'Сообщества',
|
||||||
users: 'Пользователи',
|
users: 'Пользователи',
|
||||||
create_a_community: 'Создать сообщество',
|
create_a_community: 'Создать сообщество',
|
||||||
create_community: 'Создать сообщество',
|
create_community: 'Создать сообщество',
|
||||||
remove_community: 'Удалить сообщество',
|
remove_community: 'Удалить сообщество',
|
||||||
subscribed_to_communities:'Подписаны на <1>сообщества</1>',
|
subscribed_to_communities: 'Подписаны на <1>сообщества</1>',
|
||||||
trending_communities:'<1>Сообщества</1> в тренде',
|
trending_communities: '<1>Сообщества</1> в тренде',
|
||||||
list_of_communities: 'Список сообществ',
|
list_of_communities: 'Список сообществ',
|
||||||
community_reqs: 'строчными буквами, подчеркиваниями и без пробелов.',
|
community_reqs: 'строчными буквами, подчеркиваниями и без пробелов.',
|
||||||
edit: 'редактировать',
|
edit: 'редактировать',
|
||||||
|
@ -55,9 +55,9 @@ export const ru = {
|
||||||
create: 'создать',
|
create: 'создать',
|
||||||
username: 'Имя пользователя',
|
username: 'Имя пользователя',
|
||||||
email_or_username: 'Электронная почта или имя пользователя',
|
email_or_username: 'Электронная почта или имя пользователя',
|
||||||
number_of_users:'{{count}} пользователей',
|
number_of_users: '{{count}} пользователей',
|
||||||
number_of_subscribers:'{{count}} подписчиков',
|
number_of_subscribers: '{{count}} подписчиков',
|
||||||
number_of_points:'{{count}} баллов',
|
number_of_points: '{{count}} баллов',
|
||||||
name: 'Имя',
|
name: 'Имя',
|
||||||
title: 'Название',
|
title: 'Название',
|
||||||
category: 'Категория',
|
category: 'Категория',
|
||||||
|
@ -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:
|
||||||
|
'Системная ошибка. Попробуйте выйти из системы и вернуться обратно.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
20
ui/src/translations/sv.ts
vendored
20
ui/src/translations/sv.ts
vendored
|
@ -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.',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
26
ui/src/translations/zh.ts
vendored
26
ui/src/translations/zh.ts
vendored
|
@ -5,18 +5,18 @@ export const zh = {
|
||||||
no_posts: '没有帖子.',
|
no_posts: '没有帖子.',
|
||||||
create_a_post: '创建新帖子',
|
create_a_post: '创建新帖子',
|
||||||
create_post: '创建帖子',
|
create_post: '创建帖子',
|
||||||
number_of_posts:'{{count}} 帖子',
|
number_of_posts: '{{count}} 帖子',
|
||||||
posts: '帖子',
|
posts: '帖子',
|
||||||
related_posts: '相关的帖子',
|
related_posts: '相关的帖子',
|
||||||
comments: '评论',
|
comments: '评论',
|
||||||
number_of_comments:'{{count}} 评论',
|
number_of_comments: '{{count}} 评论',
|
||||||
remove_comment: '移除评论',
|
remove_comment: '移除评论',
|
||||||
communities: '节点',
|
communities: '节点',
|
||||||
create_a_community: '创建新节点',
|
create_a_community: '创建新节点',
|
||||||
create_community: '创建节点',
|
create_community: '创建节点',
|
||||||
remove_community: '移除节点',
|
remove_community: '移除节点',
|
||||||
subscribed_to_communities:'订阅新 <1>节点</1>',
|
subscribed_to_communities: '订阅新 <1>节点</1>',
|
||||||
trending_communities:'<1>节点</1>趋势',
|
trending_communities: '<1>节点</1>趋势',
|
||||||
list_of_communities: '节点列表',
|
list_of_communities: '节点列表',
|
||||||
community_reqs: '包含小写与下划线且没有空格的字符串.',
|
community_reqs: '包含小写与下划线且没有空格的字符串.',
|
||||||
edit: '编辑',
|
edit: '编辑',
|
||||||
|
@ -53,9 +53,9 @@ export const zh = {
|
||||||
create: '创建',
|
create: '创建',
|
||||||
username: '用户名',
|
username: '用户名',
|
||||||
email_or_username: '邮箱或用户名',
|
email_or_username: '邮箱或用户名',
|
||||||
number_of_users:'{{count}} 用户',
|
number_of_users: '{{count}} 用户',
|
||||||
number_of_subscribers:'{{count}} 订阅',
|
number_of_subscribers: '{{count}} 订阅',
|
||||||
number_of_points:'{{count}} 分',
|
number_of_points: '{{count}} 分',
|
||||||
name: '名字',
|
name: '名字',
|
||||||
title: '标题',
|
title: '标题',
|
||||||
category: '分类',
|
category: '分类',
|
||||||
|
@ -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: '系统错误. 尝试注销再登录',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
102
ui/src/utils.ts
vendored
102
ui/src/utils.ts
vendored
|
@ -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';
|
||||||
|
@ -17,11 +24,16 @@ import * as emojiShortName from 'emoji-short-name';
|
||||||
export const repoUrl = 'https://github.com/dessalines/lemmy';
|
export const repoUrl = 'https://github.com/dessalines/lemmy';
|
||||||
export const markdownHelpUrl = 'https://commonmark.org/help/';
|
export const markdownHelpUrl = 'https://commonmark.org/help/';
|
||||||
|
|
||||||
export const postRefetchSeconds: number = 60*1000;
|
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,27 +43,30 @@ 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+(.*)$/);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function (tokens: any, idx: any) {
|
render: function(tokens: any, idx: any) {
|
||||||
var m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/);
|
var m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/);
|
||||||
|
|
||||||
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) {
|
||||||
return twemoji.parse(token[idx].content);
|
return twemoji.parse(token[idx].content);
|
||||||
|
@ -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}`);
|
||||||
|
|
||||||
|
@ -72,18 +89,28 @@ export function hotRank(comment: Comment): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mdToHtml(text: string) {
|
export function mdToHtml(text: string) {
|
||||||
return {__html: md.render(text)};
|
return { __html: md.render(text) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUnixTime(text: string): number {
|
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);
|
||||||
|
@ -91,7 +118,7 @@ export function canMod(user: User, modIds: Array<number>, creator_id: number, on
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// onSelf +1 on mod actions not for yourself, IE ban, remove, etc
|
// onSelf +1 on mod actions not for yourself, IE ban, remove, etc
|
||||||
modIds = modIds.slice(0, yourIndex+(onSelf ? 0 : 1));
|
modIds = modIds.slice(0, yourIndex + (onSelf ? 0 : 1));
|
||||||
return !modIds.includes(creator_id);
|
return !modIds.includes(creator_id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -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
2
ui/src/version.ts
vendored
|
@ -1 +1 @@
|
||||||
export let version: string = "v0.3.0.4-0-gce10a34";
|
export let version: string = 'v0.3.0.4-0-gce10a34';
|
||||||
|
|
Loading…
Reference in a new issue