Browse Source

Fix remaining typescript errors, add tsc check to commit hook

unoptim
GreatBearShark 2 years ago
parent
commit
b7bb38b509
  1. 2
      package.json
  2. 3
      src/components/about.tsx
  3. 29
      src/components/admin-settings.tsx
  4. 11
      src/components/comment-form.tsx
  5. 70
      src/components/comment-node.tsx
  6. 83
      src/components/community.tsx
  7. 9
      src/components/inbox.tsx
  8. 1
      src/components/login.tsx
  9. 12
      src/components/main.tsx
  10. 113
      src/components/markdown-textarea.tsx
  11. 62
      src/components/modlog.tsx
  12. 100
      src/components/post-form.tsx
  13. 168
      src/components/post-listing.tsx
  14. 7
      src/components/post.tsx
  15. 3
      src/components/private-message-form.tsx
  16. 17
      src/components/private-message.tsx
  17. 16
      src/components/reports.tsx
  18. 3
      src/components/user-details.tsx
  19. 58
      src/interfaces.ts
  20. 6
      src/services/UserService.ts
  21. 50
      src/services/WebSocketService.ts
  22. 36
      src/utils.ts

2
package.json

@ -101,7 +101,7 @@
"engineStrict": true,
"husky": {
"hooks": {
"pre-commit": "cargo clippy --manifest-path ../server/Cargo.toml --all-targets --workspace -- -D warnings && lint-staged"
"pre-commit": "cargo clippy --manifest-path ../server/Cargo.toml --all-targets --workspace -- -D warnings && tsc --noEmit && lint-staged"
}
},
"lint-staged": {

3
src/components/about.tsx

@ -3,11 +3,12 @@ import { WebSocketService } from '../services';
import { i18n } from '../i18next';
import { Trans } from 'react-i18next';
import { repoUrl } from '../utils';
import { BASE_PATH } from "../isProduction";
import { BASE_PATH } from '../isProduction';
export class About extends Component<any, any> {
componentDidMount() {
document.title = `${i18n.t('about')} - ${
// @ts-ignore
WebSocketService.Instance?.site?.name || 'ChapoChat'
}`;
window.scrollTo(0, 0);

29
src/components/admin-settings.tsx

@ -62,7 +62,7 @@ class BaseAdminSettings extends Component<any, AdminSettingsState> {
siteConfigLoading: true,
};
state = this.emptyState
state = this.emptyState;
componentDidMount() {
this.subscription = WebSocketService.Instance.subject
@ -179,7 +179,7 @@ class BaseAdminSettings extends Component<any, AdminSettingsState> {
return (
<div>
<h5>{i18n.t('admin_settings')}</h5>
<form onSubmit={linkEvent(this, this.handleSiteAdminConfigSubmit)}>
<form onSubmit={this.handleSiteAdminConfigSubmit}>
<div className="form-group row">
<label
className="col-12 col-form-label"
@ -191,7 +191,7 @@ class BaseAdminSettings extends Component<any, AdminSettingsState> {
<textarea
id={this.siteConfigTextAreaId}
value={this.state.siteConfigForm.config_hjson}
onChange={linkEvent(this, this.handleSiteConfigHjsonChange)}
onChange={this.handleSiteConfigHjsonChange}
className="form-control text-monospace"
rows={3}
/>
@ -215,17 +215,22 @@ class BaseAdminSettings extends Component<any, AdminSettingsState> {
);
}
handleSiteAdminConfigSubmit(i: AdminSettings, event: any) {
handleSiteAdminConfigSubmit = (event: React.SyntheticEvent) => {
event.preventDefault();
i.state.siteConfigLoading = true;
WebSocketService.Instance.saveSiteConfig(i.state.siteConfigForm);
i.setState(i.state);
}
WebSocketService.Instance.saveSiteConfig(this.state.siteConfigForm);
this.setState({ siteConfigLoading: true });
};
handleSiteConfigHjsonChange(i: AdminSettings, event: any) {
i.state.siteConfigForm.config_hjson = event.target.value;
i.setState(i.state);
}
handleSiteConfigHjsonChange = (
event: React.ChangeEvent<HTMLTextAreaElement>
) => {
this.setState({
siteConfigForm: {
...this.state.siteConfigForm,
config_hjson: event.target.value,
},
});
};
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);

11
src/components/comment-form.tsx

@ -9,7 +9,12 @@ import {
UserOperation,
CommentResponse,
} from '../interfaces';
import { capitalizeFirstLetter, wsJsonToRes, toast } from '../utils';
import {
capitalizeFirstLetter,
wsJsonToRes,
toast,
isCommentChanged,
} from '../utils';
import { WebSocketService, UserService } from '../services';
import { i18n } from '../i18next';
import { Trans } from 'react-i18next';
@ -140,7 +145,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
// text edit only
(data.comment.creator_id == UserService.Instance.user.id &&
op == UserOperation.EditComment &&
isCommentChanged(op) &&
data.comment.content)
) {
this.setState({
@ -183,7 +188,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
if (res.op == UserOperation.CreateComment) {
let data = res.data as CommentResponse;
this.handleFinished(res.op, data);
} else if (res.op == UserOperation.EditComment) {
} else if (isCommentChanged(res.op)) {
let data = res.data as CommentResponse;
this.handleFinished(res.op, data);
}

70
src/components/comment-node.tsx

@ -18,6 +18,9 @@ import {
BanType,
CommentSortType,
SortType,
DeleteCommentForm,
RemoveCommentForm,
MarkCommentReadForm,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
@ -180,6 +183,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
className={`details comment-node border-top py-2 ${
this.isCommentNew ? 'mark' : ''
}`}
// @ts-ignore
style={borderStyle}
>
<div
@ -304,7 +308,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{this.props.markable && (
<button
className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleMarkRead)}
onClick={this.handleMarkRead}
data-tippy-content={
node.comment.read
? i18n.t('mark_as_unread')
@ -479,10 +483,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
) : (
<button
className="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
this.handleModRemoveSubmit
)}
onClick={this.handleModRemoveSubmit}
>
{i18n.t('restore')}
</button>
@ -751,10 +752,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</div>
{/* end of details */}
{this.state.showRemoveDialog && (
<form
className="form-inline"
onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
>
<form className="form-inline" onSubmit={this.handleModRemoveSubmit}>
<input
type="text"
className="form-control mr-2"
@ -994,16 +992,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
handleDeleteClick(i: CommentNode) {
let deleteForm: CommentFormI = {
content: i.props.node.comment.content,
let deleteForm: DeleteCommentForm = {
edit_id: i.props.node.comment.id,
creator_id: i.props.node.comment.creator_id,
post_id: i.props.node.comment.post_id,
parent_id: i.props.node.comment.parent_id,
deleted: !i.props.node.comment.deleted,
auth: null,
};
WebSocketService.Instance.editComment(deleteForm);
WebSocketService.Instance.deleteComment(deleteForm);
}
handleSaveCommentClick(i: CommentNode) {
@ -1095,48 +1089,38 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
i.setState(i.state);
}
handleModRemoveSubmit(i: CommentNode) {
handleModRemoveSubmit = (event: React.SyntheticEvent) => {
event.preventDefault();
let form: CommentFormI = {
content: i.props.node.comment.content,
edit_id: i.props.node.comment.id,
creator_id: i.props.node.comment.creator_id,
post_id: i.props.node.comment.post_id,
parent_id: i.props.node.comment.parent_id,
removed: !i.props.node.comment.removed,
reason: i.state.removeReason,
let form: RemoveCommentForm = {
edit_id: this.props.node.comment.id,
removed: !this.props.node.comment.removed,
reason: this.state.removeReason,
auth: null,
};
WebSocketService.Instance.editComment(form);
WebSocketService.Instance.removeComment(form);
i.state.showRemoveDialog = false;
i.setState(i.state);
}
this.setState({ showRemoveDialog: false });
};
handleMarkRead(i: CommentNode) {
handleMarkRead = () => {
// if it has a user_mention_id field, then its a mention
if (i.props.node.comment.user_mention_id) {
if (this.props.node.comment.user_mention_id) {
let form: EditUserMentionForm = {
user_mention_id: i.props.node.comment.user_mention_id,
read: !i.props.node.comment.read,
user_mention_id: this.props.node.comment.user_mention_id,
read: !this.props.node.comment.read,
};
WebSocketService.Instance.editUserMention(form);
} else {
let form: CommentFormI = {
content: i.props.node.comment.content,
edit_id: i.props.node.comment.id,
creator_id: i.props.node.comment.creator_id,
post_id: i.props.node.comment.post_id,
parent_id: i.props.node.comment.parent_id,
read: !i.props.node.comment.read,
let form: MarkCommentReadForm = {
edit_id: this.props.node.comment.id,
read: !this.props.node.comment.read,
auth: null,
};
WebSocketService.Instance.editComment(form);
WebSocketService.Instance.markCommentAsRead(form);
}
i.state.readLoading = true;
i.setState(this.state);
}
this.setState({ readLoading: true });
};
handleModBanFromCommunityShow(i: CommentNode) {
i.state.showBanDialog = !i.state.showBanDialog;

83
src/components/community.tsx

@ -48,6 +48,8 @@ import {
editPostFindRes,
commentsToFlatNodes,
setupTippy,
isCommentChanged,
isPostChanged,
} from '../utils';
import { i18n } from '../i18next';
import { Icon } from './icon';
@ -214,8 +216,8 @@ export class BaseCommunity extends Component<any, State> {
</div>
</div>
{this.state.community.subscribed ? (
<Button
variant="outline"
<Button
variant="outline"
onClick={linkEvent(
this.state.community.id,
this.handleUnsubscribe
@ -224,24 +226,25 @@ export class BaseCommunity extends Component<any, State> {
{i18n.t('unsubscribe')}
</Button>
) : (
<Button
variant="outline"
<Button
variant="outline"
onClick={linkEvent(
this.state.community.id,
this.handleSubscribe
)
}
this.handleSubscribe
)}
>
{i18n.t('subscribe')}
</Button>
)}
<div className="community-button-separator" />
<Link to={`/create_post?community=${this.state.community.name}`} style={{ display: 'block' }}>
<Button variant="primary">
{isMobile ? '+' : 'Create Post'}
</Button>
</Link>
<div className="community-button-separator" />
<Link
to={`/create_post?community=${this.state.community.name}`}
style={{ display: 'block' }}
>
<Button variant="primary">
{isMobile ? '+' : 'Create Post'}
</Button>
</Link>
</div>
</div>
</div>
@ -351,12 +354,12 @@ export class BaseCommunity extends Component<any, State> {
}
nextPage(i: BaseCommunity) {
i.updateUrl({ page: (i.state.page as number) + 1 });
i.updateUrl({ page: (i.state.page) + 1 });
window.scrollTo(0, 0);
}
prevPage(i: BaseCommunity) {
i.updateUrl({ page: (i.state.page as number) - 1 });
i.updateUrl({ page: (i.state.page) - 1 });
window.scrollTo(0, 0);
}
@ -430,20 +433,23 @@ export class BaseCommunity extends Component<any, State> {
this.fetchData();
} else if (res.op == UserOperation.GetCommunity) {
let data = res.data as GetCommunityResponse;
this.setState({
community: data.community,
moderators: data.moderators,
admins: data.admins,
sitemods: data.sitemods,
online: data.online,
}, () => {
this.fetchData();
});
this.setState(
{
community: data.community,
moderators: data.moderators,
admins: data.admins,
sitemods: data.sitemods,
online: data.online,
},
() => {
this.fetchData();
}
);
document.title = `/c/${this.state.community.name} - ${this.state.site.name}`;
} else if (res.op == UserOperation.EditCommunity) {
let data = res.data as CommunityResponse;
this.setState({
community: data.community
community: data.community,
});
} else if (res.op == UserOperation.FollowCommunity) {
let data = res.data as CommunityResponse;
@ -453,13 +459,16 @@ export class BaseCommunity extends Component<any, State> {
this.setState(this.state);
} else if (res.op == UserOperation.GetPosts) {
let data = res.data as GetPostsResponse;
this.setState({
posts: data.posts,
loading: false
}, () => {
setupTippy()
});
} else if (res.op == UserOperation.EditPost) {
this.setState(
{
posts: data.posts,
loading: false,
},
() => {
setupTippy();
}
);
} else if (isPostChanged(res.op)) {
let data = res.data as PostResponse;
editPostFindRes(data, this.state.posts);
this.setState(this.state);
@ -474,7 +483,7 @@ export class BaseCommunity extends Component<any, State> {
} else if (res.op == UserOperation.AddModToCommunity) {
let data = res.data as AddModToCommunityResponse;
this.setState({
moderators: data.moderators
moderators: data.moderators,
});
} else if (res.op == UserOperation.BanFromCommunity) {
let data = res.data as BanFromCommunityResponse;
@ -486,9 +495,9 @@ export class BaseCommunity extends Component<any, State> {
let data = res.data as GetCommentsResponse;
this.setState({
comments: data.comments,
loading: false
loading: false,
});
} else if (res.op == UserOperation.EditComment) {
} else if (isCommentChanged(res.op)) {
let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments);
this.setState(this.state);
@ -509,7 +518,7 @@ export class BaseCommunity extends Component<any, State> {
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.setState({
site: data.site
site: data.site,
});
}
}

9
src/components/inbox.tsx

@ -29,6 +29,8 @@ import {
createCommentLikeRes,
commentsToFlatNodes,
setupTippy,
isCommentChanged,
isMessageChanged,
} from '../utils';
import { CommentNodes } from './comment-nodes';
import { PrivateMessage } from './private-message';
@ -489,10 +491,7 @@ export class Inbox extends Component<any, InboxState> {
setupTippy();
}
);
} else if (
res.op == UserOperation.EditPrivateMessage ||
res.op == UserOperation.MarkPrivateMessageAsRead
) {
} else if (isMessageChanged(res.op)) {
let data = res.data as PrivateMessageResponse;
let found: PrivateMessageI = this.state.messages.find(
m => m.id === data.message.id
@ -515,7 +514,7 @@ export class Inbox extends Component<any, InboxState> {
setupTippy();
} else if (res.op == UserOperation.MarkAllAsRead) {
// Moved to be instant
} else if (res.op == UserOperation.EditComment) {
} else if (isCommentChanged(res.op)) {
let data = res.data as CommentResponse;
editCommentRes(data, this.state.replies);

1
src/components/login.tsx

@ -540,6 +540,7 @@ export class Login extends Component<any, State> {
}
initHCaptcha() {
// @ts-ignore
const widgetID = hcaptcha.render('h-captcha', {
sitekey: this.state.captcha.hcaptcha.site_key,
});

12
src/components/main.tsx

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
@ -53,6 +53,8 @@ import {
editPostFindRes,
commentsToFlatNodes,
setupTippy,
isCommentChanged,
isPostChanged,
} from '../utils';
import { BASE_PATH } from '../isProduction';
import { i18n } from '../i18next';
@ -104,7 +106,7 @@ interface UrlParams {
page?: number;
}
class Main extends Component<any, MainState> {
class Main extends Component<MainProps & RouteComponentProps, MainState> {
private subscription: Subscription;
private emptyState: MainState = {
subscribedCommunities: [],
@ -249,6 +251,7 @@ class Main extends Component<any, MainState> {
<Button
css={{ width: '100%', color: '#fff !important' }}
as={Link}
// @ts-ignore
to="/create_community"
>
{i18n.t('create_a_community')}
@ -783,7 +786,7 @@ class Main extends Component<any, MainState> {
}
}
this.setState(this.state);
} else if (res.op === UserOperation.EditPost) {
} else if (isPostChanged(res.op)) {
let data = res.data as PostResponse;
editPostFindRes(data, this.state.posts);
this.setState(this.state);
@ -831,7 +834,7 @@ class Main extends Component<any, MainState> {
comments: data.comments,
loading: false,
});
} else if (res.op == UserOperation.EditComment) {
} else if (isCommentChanged(res.op)) {
let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments);
this.setState(this.state);
@ -866,4 +869,5 @@ class Main extends Component<any, MainState> {
}
}
// @ts-ignore
export default withTranslation()(withRouter(Main));

113
src/components/markdown-textarea.tsx

@ -69,8 +69,7 @@ export class MarkdownTextArea extends Component<
this.tribute.attach(textarea);
textarea.addEventListener('tribute-replaced', () => {
this.state.content = textarea.value;
this.setState(this.state);
this.setState({ content: textarea.value });
autosize.update(textarea);
});
@ -130,7 +129,7 @@ export class MarkdownTextArea extends Component<
render() {
return (
<form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}>
<form id={this.formId} onSubmit={this.handleSubmit}>
<Prompt when={!!this.state.content} message={i18n.t('block_leaving')} />
<div className="form-group row">
<div className="col-sm-12">
@ -138,8 +137,8 @@ export class MarkdownTextArea extends Component<
id={this.id}
className={`form-control ${this.state.previewMode && 'd-none'}`}
value={this.state.content}
onChange={linkEvent(this, this.handleContentChange)}
onKeyDown={linkEvent(this, this.handleKeydown)}
onChange={this.handleContentChange}
onKeyDown={this.handleKeydown}
// onPaste={linkEvent(this, this.handleImageUploadPaste)}
required
disabled={this.props.disabled}
@ -185,7 +184,7 @@ export class MarkdownTextArea extends Component<
className={`btn btn-sm btn-secondary mr-2 ${
this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
onClick={this.handlePreviewToggle}
>
{i18n.t('preview')}
</button>
@ -213,7 +212,7 @@ export class MarkdownTextArea extends Component<
<button
className="btn btn-sm text-muted"
data-tippy-content={i18n.t('link')}
onClick={linkEvent(this, this.handleInsertLink)}
onClick={this.handleInsertLink}
>
<svg className="icon icon-inline">
<use xlinkHref="#icon-link" />
@ -320,7 +319,7 @@ export class MarkdownTextArea extends Component<
<button
className="btn btn-sm text-muted"
data-tippy-content={i18n.t('spoiler')}
onClick={linkEvent(this, this.handleInsertSpoiler)}
onClick={this.handleInsertSpoiler}
>
<svg className="icon icon-inline">
<use xlinkHref="#icon-alert-triangle" />
@ -428,39 +427,35 @@ export class MarkdownTextArea extends Component<
this.setState({ showEmojiPicker: !this.state.showEmojiPicker });
};
handleContentChange(i: MarkdownTextArea, event: any) {
i.state.content = event.target.value;
i.setState(i.state);
if (i.props.onContentChange) {
i.props.onContentChange(i.state.content);
handleContentChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
this.setState({ content: event.target.value });
if (this.props.onContentChange) {
this.props.onContentChange(this.state.content);
}
}
};
handlePreviewToggle(i: MarkdownTextArea, event: any) {
handlePreviewToggle = (event: React.SyntheticEvent) => {
event.preventDefault();
i.state.previewMode = !i.state.previewMode;
i.setState(i.state);
}
this.setState({ previewMode: !this.state.previewMode });
};
handleSubmit(i: MarkdownTextArea, event: any) {
handleSubmit = (event: React.SyntheticEvent) => {
event.preventDefault();
i.state.loading = true;
i.setState(i.state);
i.props.onSubmit(i.state.content, event);
}
this.setState({ loading: true });
this.props.onSubmit(this.state.content, event);
};
handleKeydown(i: MarkdownTextArea, event: any) {
handleKeydown = (event: React.KeyboardEvent) => {
// if enter was pressed
if (event.keyCode === 13) {
// while command (mac) or ctrl is pressed
if (event.metaKey || event.ctrlKey) {
// submit comment
i.state.loading = true;
i.setState(i.state);
i.props.onSubmit(i.state.content, event);
this.setState({ loading: true });
this.props.onSubmit(this.state.content, event);
}
}
}
};
handleReplyCancel(i: MarkdownTextArea) {
i.props.onReplyCancel();
@ -473,59 +468,56 @@ export class MarkdownTextArea extends Component<
this.toggleEmojiPicker();
};
handleInsertLink(i: MarkdownTextArea, event: any) {
handleInsertLink = (event: any) => {
event.preventDefault();
if (!i.state.content) {
i.state.content = '';
}
let textarea: any = document.getElementById(i.id);
let content = this.state.content || '';
let textarea: any = document.getElementById(this.id);
let start: number = textarea.selectionStart;
let end: number = textarea.selectionEnd;
if (start !== end) {
let selectedText = i.state.content.substring(start, end);
i.state.content = `${i.state.content.substring(
let selectedText = this.state.content.substring(start, end);
content = `${this.state.content.substring(
0,
start
)} [${selectedText}]() ${i.state.content.substring(end)}`;
)} [${selectedText}]() ${this.state.content.substring(end)}`;
textarea.focus();
setTimeout(() => (textarea.selectionEnd = end + 4), 10);
} else {
i.state.content += '[]()';
content += '[]()';
textarea.focus();
setTimeout(() => (textarea.selectionEnd -= 1), 10);
}
i.setState(i.state);
}
this.setState({ content });
};
simpleSurround(chars: string) {
simpleSurround = (chars: string) => {
this.simpleSurroundBeforeAfter(chars, chars);
}
};
simpleSurroundBeforeAfter(beforeChars: string, afterChars: string) {
if (!this.state.content) {
this.state.content = '';
}
simpleSurroundBeforeAfter = (beforeChars: string, afterChars: string) => {
let content = this.state.content || '';
let textarea: any = document.getElementById(this.id);
let start: number = textarea.selectionStart;
let end: number = textarea.selectionEnd;
if (start !== end) {
let selectedText = this.state.content.substring(start, end);
this.state.content = `${this.state.content.substring(
content = `${this.state.content.substring(
0,
start - 1
)} ${beforeChars}${selectedText}${afterChars} ${this.state.content.substring(
end + 1
)}`;
} else {
this.state.content += `${beforeChars}___${afterChars}`;
content += `${beforeChars}___${afterChars}`;
}
this.setState(this.state);
this.setState({ content });
setTimeout(() => {
autosize.update(textarea);
}, 10);
}
};
handleInsertBold(i: MarkdownTextArea, event: any) {
event.preventDefault();
@ -562,27 +554,23 @@ export class MarkdownTextArea extends Component<
i.simpleInsert('#');
}
simpleInsert(chars: string) {
if (!this.state.content) {
this.state.content = `${chars} `;
} else {
this.state.content += `\n${chars} `;
}
simpleInsert = (chars: string) => {
const content = !this.state.content ? `${chars} ` : `\n${chars} `;
let textarea: any = document.getElementById(this.id);
textarea.focus();
setTimeout(() => {
autosize.update(textarea);
}, 10);
this.setState(this.state);
}
this.setState({ content });
};
handleInsertSpoiler(i: MarkdownTextArea, event: any) {
handleInsertSpoiler = (event: React.SyntheticEvent) => {
event.preventDefault();
let beforeChars = `\n::: spoiler ${i18n.t('spoiler')}\n`;
let afterChars = '\n:::\n';
i.simpleSurroundBeforeAfter(beforeChars, afterChars);
}
this.simpleSurroundBeforeAfter(beforeChars, afterChars);
};
quoteInsert() {
let textarea: any = document.getElementById(this.id);
@ -593,8 +581,9 @@ export class MarkdownTextArea extends Component<
.split('\n')
.map(t => `> ${t}`)
.join('\n') + '\n\n';
this.state.content = quotedText;
this.setState(this.state);
this.setState({
content: quotedText,
});
// Not sure why this needs a delay
setTimeout(() => autosize.update(textarea), 10);
}

62
src/components/modlog.tsx

@ -50,13 +50,13 @@ export class Modlog extends Component<any, ModlogState> {
loading: true,
};
constructor(props: any, context: any) {
super(props, context);
state = this.emptyState;
this.state = this.emptyState;
this.state.communityId = this.props.match.params.community_id
componentDidMount() {
const communityId = this.props.match.params.community_id
? Number(this.props.match.params.community_id)
: undefined;
this.setState({ communityId });
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
@ -95,36 +95,37 @@ export class Modlog extends Component<any, ModlogState> {
);
let added = addTypeInfo(res.added, 'added');
let banned = addTypeInfo(res.banned, 'banned');
this.state.combined = [];
// this.state.combined = [];
const updatedState: any = { combined: [] };
this.state.combined.push(...removed_posts);
this.state.combined.push(...locked_posts);
this.state.combined.push(...stickied_posts);
this.state.combined.push(...removed_comments);
this.state.combined.push(...removed_communities);
this.state.combined.push(...banned_from_community);
this.state.combined.push(...added_to_community);
this.state.combined.push(...added);
this.state.combined.push(...banned);
updatedState.combined.push(...removed_posts);
updatedState.combined.push(...locked_posts);
updatedState.combined.push(...stickied_posts);
updatedState.combined.push(...removed_comments);
updatedState.combined.push(...removed_communities);
updatedState.combined.push(...banned_from_community);
updatedState.combined.push(...added_to_community);
updatedState.combined.push(...added);
updatedState.combined.push(...banned);
if (this.state.communityId && this.state.combined.length > 0) {
this.state.communityName = (this.state.combined[0]
updatedState.communityName = (this.state.combined[0]
.data as ModRemovePost).community_name;
}
// Sort them by time
this.state.combined.sort((a, b) =>
updatedState.combined.sort((a, b) =>
b.data.when_.localeCompare(a.data.when_)
);
this.setState(this.state);
this.setState(updatedState);
}
combined() {
return (
<tbody>
{this.state.combined.map(i => (
<tr key={i.data.id}>
<tr key={`${i.type_}-${i.data.id}`}>
<td>
<MomentTime data={i.data} />
</td>
@ -412,32 +413,27 @@ export class Modlog extends Component<any, ModlogState> {
{this.state.page > 1 && (
<button
className="btn btn-sm btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
onClick={this.prevPage}
>
{i18n.t('prev')}
</button>
)}
<button
className="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
<button className="btn btn-sm btn-secondary" onClick={this.nextPage}>
{i18n.t('next')}
</button>
</div>
);
}
nextPage(i: Modlog) {
i.state.page++;
i.setState(i.state);
i.refetch();
}
nextPage = () => {
this.setState({ page: this.state.page + 1 });
this.refetch();
};
prevPage(i: Modlog) {
i.state.page--;
i.setState(i.state);
i.refetch();
}
prevPage = () => {
this.setState({ page: Math.max(this.state.page - 1, 0) });
this.refetch();
};
refetch() {
let modlogForm: GetModlogForm = {

100
src/components/post-form.tsx

@ -34,14 +34,14 @@ import {
hostname,
pictrsDeleteToast,
validTitle,
isPostChanged,
} from '../utils';
// import Choices from 'choices.js';
import { i18n } from '../i18next';
import { cleanURL } from '../clean-url';
import { Icon } from './icon';
import { linkEvent } from '../linkEvent';
import matchSorter from 'match-sorter'
import matchSorter from 'match-sorter';
import {
Combobox,
@ -50,8 +50,8 @@ import {
ComboboxList,
ComboboxOption,
ComboboxOptionText,
} from "@reach/combobox";
import "@reach/combobox/styles.css";
} from '@reach/combobox';
import '@reach/combobox/styles.css';
import { Button } from 'theme-ui';
export const MAX_POST_TITLE_LENGTH = 160;
@ -59,9 +59,9 @@ export const MAX_POST_BODY_LENGTH = 20000;
export const MAX_COMMENT_LENGTH = 10000;
function CommunityInput({ communities, onSelect }) {
const [value, setValue] = useState('')
const [value, setValue] = useState('');
const results = matchSorter(communities, value, {
keys: [(item) => `${item.name}`]
keys: [item => `${item.name}`],
});
function handleChange(e) {
@ -79,8 +79,8 @@ function CommunityInput({ communities, onSelect }) {
<Combobox
aria-label="Communities"
openOnFocus
onSelect={(name) => {
const community = communities.find(comm => comm.name === name)
onSelect={name => {
const community = communities.find(comm => comm.name === name);
setValue(community.name);
onSelect(community.id);
}}
@ -93,23 +93,23 @@ function CommunityInput({ communities, onSelect }) {
placeholder={i18n.t('select_a_community')}
/>
<ComboboxPopover className="shadow-popup">
{results.length > 0 ? (
<ComboboxList persistSelection>
{results.slice(0, 10).map((result, index) => (
<ComboboxOption
key={index}
value={result.name}
/>
))}
</ComboboxList>
) : (
<div data-reach-combobox-popover style={{ fontSize: '16px', padding: 8, paddingTop: 0 }}>
No results found
</div>
)}
</ComboboxPopover>
{results.length > 0 ? (
<ComboboxList persistSelection>
{results.slice(0, 10).map((result, index) => (
<ComboboxOption key={index} value={result.name} />
))}
</ComboboxList>
) : (
<div
data-reach-combobox-popover
style={{ fontSize: '16px', padding: 8, paddingTop: 0 }}
>
No results found
</div>
)}
</ComboboxPopover>
</Combobox>
)
);
}
interface PostFormProps {
@ -260,20 +260,19 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.state.postForm.name === null ||
this.state.postForm.name.trim() === '';
const communities = this.state.communities
.filter(community => {
// don't allow crossposting to same community as original
if (this.state.crosspostCommunityId) {
// remove main community
const MAIN_COMMUNITY_ID = 2;
return (
community.id !== this.state.crosspostCommunityId &&
community.id != MAIN_COMMUNITY_ID
);
}
const communities = this.state.communities.filter(community => {
// don't allow crossposting to same community as original
if (this.state.crosspostCommunityId) {
// remove main community
const MAIN_COMMUNITY_ID = 2;
return (
community.id !== this.state.crosspostCommunityId &&
community.id != MAIN_COMMUNITY_ID
);
}
return true;
})
return true;
});
return (
<div>
<Prompt
@ -354,7 +353,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</svg>
)}
{isImage(this.state.postForm.url) && (
<img src={this.state.postForm.url} alt="post form thumbnail" className="img-fluid" />
<img
src={this.state.postForm.url}
alt="post form thumbnail"
className="img-fluid"
/>
)}
{this.state.crossPosts.length > 0 && (
<>
@ -623,9 +626,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.setState(this.state);
}
handlePostCommunityChange = (community_id) => {
this.setState({ postForm: { ...this.state.postForm, community_id, }});
}
handlePostCommunityChange = community_id => {
this.setState({ postForm: { ...this.state.postForm, community_id } });
};
handlePostNsfwChange(i: PostForm, event: any) {
i.state.postForm.nsfw = event.target.checked;
@ -752,14 +755,21 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} else if (res.op == UserOperation.ListCommunities) {
let data = res.data as ListCommunitiesResponse;
// this.state.communities = data.communities;
this.setState({ communities: data.communities })
this.setState({ communities: data.communities });
if (this.props.post) {
this.setState({ postForm: { ...this.state.postForm, community_id: this.props.post.community_id } });
this.setState({
postForm: {
...this.state.postForm,
community_id: this.props.post.community_id,
},
});
} else if (this.props.params && this.props.params.community) {
let foundCommunityId = data.communities.find(
r => r.name == this.props.params.community
).id;
this.setState({ postForm: { ...this.state.postForm, community_id: foundCommunityId } });
this.setState({
postForm: { ...this.state.postForm, community_id: foundCommunityId },
});
} else {
// By default, the null valued 'Select a Community'
}
@ -776,7 +786,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.state.loading = false;
this.props.onCreate(data.post.id);
}
} else if (res.op == UserOperation.EditPost) {
} else if (isPostChanged(res.op)) {
let data = res.data as PostResponse;
if (data.post.creator_id == UserService.Instance.user.id) {
this.state.loading = false;

168
src/components/post-listing.tsx

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
import { WebSocketService, UserService } from '../services';
import {
Post,
@ -16,6 +16,10 @@ import {
AddSitemodForm,
TransferSiteForm,
TransferCommunityForm,
DeletePostForm,
RemovePostForm,
LockPostForm,
StickyPostForm,
} from '../interfaces';
import { MomentTime } from './moment-time';
import { PostForm } from './post-form';
@ -141,7 +145,10 @@ const VoteButtons = ({
</>
);
class BasePostListing extends Component<PostListingProps, PostListingState> {
class BasePostListing extends Component<
PostListingProps & RouteComponentProps,
PostListingState
> {
private emptyState: PostListingState = {
showEdit: false,
showRemoveDialog: false,
@ -165,13 +172,7 @@ class BasePostListing extends Component<PostListingProps, PostListingState> {
localPostSaved: this.props.post.saved,
};
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.handlePostLike = this.handlePostLike.bind(this);
this.handlePostDisLike = this.handlePostDisLike.bind(this);
}
state = this.emptyState;
componentDidMount() {
// scroll to top of page when loading post listing
@ -472,10 +473,7 @@ class BasePostListing extends Component<PostListingProps, PostListingState> {
</ul>
)}
{this.state.showRemoveDialog && (
<form
className="form-inline"
onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
>
<form className="form-inline" onSubmit={this.handleModRemoveSubmit}>
<input
type="text"
className="form-control mr-2"
@ -662,8 +660,8 @@ class BasePostListing extends Component<PostListingProps, PostListingState> {
my_vote={this.state.my_vote}
enableDownvotes={this.props.enableDownvotes}
score={this.state.score}
handlePostDisLike={linkEvent(this, this.handlePostDisLike)}
handlePostLike={linkEvent(this, this.handlePostLike)}
handlePostDisLike={this.handlePostDisLike}
handlePostLike={this.handlePostLike}
pointsTippy={this.pointsTippy}
/>
</div>
@ -860,7 +858,7 @@ class BasePostListing extends Component<PostListingProps, PostListingState> {
<li className="list-inline-item">
<button
className="btn btn-sm btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleDeleteClick)}
onClick={this.handleDeleteClick}
data-tippy-content={
!post.deleted ? i18n.t('delete') : i18n.t('restore')
}
@ -911,7 +909,7 @@ class BasePostListing extends Component<PostListingProps, PostListingState> {
<li className="list-inline-item">
<button
className="btn btn-sm btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleModLock)}
onClick={this.handleModLock}
data-tippy-content={
post.locked ? i18n.t('unlock') : i18n.t('lock')
}
@ -928,7 +926,7 @@ class BasePostListing extends Component<PostListingProps, PostListingState> {
<li className="list-inline-item">
<button
className="btn btn-sm btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleModSticky)}
onClick={this.handleModSticky}
data-tippy-content={
post.stickied
? i18n.t('unsticky')
@ -959,7 +957,7 @@ class BasePostListing extends Component<PostListingProps, PostListingState> {
) : (
<span
className="pointer"
onClick={linkEvent(this, this.handleModRemoveSubmit)}
onClick={this.handleModRemoveSubmit}
>
{i18n.t('restore')}
</span>
@ -1245,67 +1243,69 @@ class BasePostListing extends Component<PostListingProps, PostListingState> {
);
}
handlePostLike(i: BasePostListing) {
handlePostLike = (i: BasePostListing) => {
if (!UserService.Instance.user) {
this.props.history.push(`/login`);
}
let new_vote = i.state.my_vote === 1 ? 0 : 1;
const newState = { ...this.state };
if (i.state.my_vote === 1) {
i.state.score--;
i.state.upvotes--;
} else if (i.state.my_vote === -1) {
i.state.downvotes--;
i.state.upvotes++;
i.state.score += 2;
newState.score--;
newState.upvotes--;
} else if (this.state.my_vote === -1) {
newState.downvotes--;
newState.upvotes++;
newState.score += 2;
} else {
i.state.upvotes++;
i.state.score++;
newState.upvotes++;
newState.score++;
}
i.state.my_vote = new_vote;
newState.my_vote = new_vote;
let form: CreatePostLikeForm = {
post_id: i.props.post.id,
score: i.state.my_vote,
post_id: this.props.post.id,
score: this.state.my_vote,
};
WebSocketService.Instance.likePost(form);
i.setState(i.state);
this.setState(newState);
setupTippy();
}
};
handlePostDisLike(i: BasePostListing) {
handlePostDisLike = () => {
if (!UserService.Instance.user) {
this.props.history.push(`/login`);
}
let new_vote = i.state.my_vote === -1 ? 0 : -1;
let new_vote = this.state.my_vote === -1 ? 0 : -1;
const newState = { ...this.state };
if (i.state.my_vote === 1) {
i.state.score -= 2;
i.state.upvotes--;
i.state.downvotes++;
} else if (i.state.my_vote === -1) {
i.state.downvotes--;
i.state.score++;
if (this.state.my_vote === 1) {
newState.score -= 2;
newState.upvotes--;
newState.downvotes++;
} else if (this.state.my_vote === -1) {
newState.downvotes--;
newState.score++;
} else {
i.state.downvotes++;
i.state.score--;
newState.downvotes++;
newState.score--;
}
i.state.my_vote = new_vote;
newState.my_vote = new_vote;
let form: CreatePostLikeForm = {
post_id: i.props.post.id,
score: i.state.my_vote,
post_id: this.props.post.id,
score: this.state.my_vote,
};
WebSocketService.Instance.likePost(form);
i.setState(i.state);
this.setState(newState);
setupTippy();
}
};
handleEditClick = () => {
this.setState({
@ -1326,20 +1326,14 @@ class BasePostListing extends Component<PostListingProps, PostListingState> {
});
};
handleDeleteClick(i: BasePostListing) {
let deleteForm: PostFormI = {
body: i.props.post.body,
community_id: i.props.post.community_id,
name: i.props.post.name,
url: i.props.post.url,
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
deleted: !i.props.post.deleted,
nsfw: i.props.post.nsfw,
handleDeleteClick = () => {
let deleteForm: DeletePostForm = {
edit_id: this.props.post.id,
deleted: !this.props.post.deleted,
auth: null,
};
WebSocketService.Instance.editPost(deleteForm);
}
WebSocketService.Instance.deletePost(deleteForm);
};
handleSavePostClick = () => {
let saved =
@ -1381,49 +1375,37 @@ class BasePostListing extends Component<PostListingProps, PostListingState> {
});
};
handleModRemoveSubmit(i: BasePostListing) {
handleModRemoveSubmit = (event: React.SyntheticEvent) => {
event.preventDefault();
let form: PostFormI = {
name: i.props.post.name,
community_id: i.props.post.community_id,
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
removed: !i.props.post.removed,
reason: i.state.removeReason,
nsfw: i.props.post.nsfw,
let form: RemovePostForm = {
edit_id: this.props.post.id,
removed: !this.props.post.removed,
reason: this.state.removeReason,
auth: null,
};
WebSocketService.Instance.edit