Browse Source

Merge branch 'upstream/v0.7.22' into chapo-dev

main
Ryexandra 1 year ago
parent
commit
2ee63f000a
  1. 1
      assets/css/choices.min.css
  2. 7
      assets/css/main.css
  3. 7
      assets/css/selectr.min.css
  4. 8
      fuse.js
  5. 5
      package.json
  6. 25
      src/components/cake-day.tsx
  7. 269
      src/components/comment-form.tsx
  8. 64
      src/components/comment-node.tsx
  9. 2
      src/components/comment-nodes.tsx
  10. 51
      src/components/communities.tsx
  11. 64
      src/components/community-form.tsx
  12. 98
      src/components/community.tsx
  13. 63
      src/components/create-community.tsx
  14. 80
      src/components/create-post.tsx
  15. 49
      src/components/create-private-message.tsx
  16. 15
      src/components/data-type-select.tsx
  17. 53
      src/components/inbox.tsx
  18. 15
      src/components/listing-type-select.tsx
  19. 12
      src/components/login.tsx
  20. 103
      src/components/main.tsx
  21. 9
      src/components/modlog.tsx
  22. 3
      src/components/navbar.tsx
  23. 25
      src/components/password_change.tsx
  24. 142
      src/components/post-form.tsx
  25. 82
      src/components/post-listing.tsx
  26. 4
      src/components/post-listings.tsx
  27. 226
      src/components/post.tsx
  28. 10
      src/components/private-message-form.tsx
  29. 209
      src/components/search.tsx
  30. 2
      src/components/sidebar.tsx
  31. 16
      src/components/site-form.tsx
  32. 10
      src/components/sort-select.tsx
  33. 38
      src/components/sponsors.tsx
  34. 3
      src/components/symbols.tsx
  35. 308
      src/components/user-details.tsx
  36. 35
      src/components/user-listing.tsx
  37. 482
      src/components/user.tsx
  38. 6
      src/i18next.ts
  39. 2
      src/index.html
  40. 15
      src/interfaces.ts
  41. 2
      src/services/WebSocketService.ts
  42. 71
      src/utils.ts
  43. 2
      src/version.ts
  44. 13
      translations/en.json
  45. 347
      translations/eu.json
  46. 31
      translations/fi.json
  47. 1
      translations/ga.json
  48. 3
      translations/it.json
  49. 1
      translations/km.json
  50. 73
      translations/sr_Latn.json
  51. 123
      translations/sv.json
  52. 30
      translations/zh.json
  53. 141
      yarn.lock

1
assets/css/choices.min.css
File diff suppressed because it is too large
View File

7
assets/css/main.css

@ -269,3 +269,10 @@ pre {
width: 0px !important;
padding: 0 !important;
}
br.big {
display: block;
content: "";
margin-top: 1rem;
}

7
assets/css/selectr.min.css
File diff suppressed because it is too large
View File

8
fuse.js

@ -6,12 +6,10 @@ const {
WebIndexPlugin,
QuantumPlugin,
} = require('fuse-box');
// const transformInferno = require('../../dist').default
const transformInferno = require('ts-transform-inferno').default;
const transformClasscat = require('ts-transform-classcat').default;
let fuse, app;
let isProduction = false;
// var setVersion = require('./set_version.js').setVersion;
Sparky.task('config', _ => {
fuse = new FuseBox({
@ -46,18 +44,18 @@ Sparky.task('config', _ => {
});
app = fuse.bundle('app').instructions('>index.tsx');
});
// Sparky.task('version', _ => setVersion());
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
Sparky.task('env', _ => (isProduction = true));
Sparky.task('copy-assets', () =>
Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static')
);
Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
fuse.dev();
fuse.dev({
fallback: 'index.html',
});
app.hmr().watch();
return fuse.run();
});
Sparky.task('prod', ['clean', 'env', 'config', 'copy-assets'], _ => {
// fuse.dev({ reload: true }); // remove after demo
return fuse.run();
});

5
package.json

@ -15,7 +15,6 @@
},
"keywords": [],
"dependencies": {
"@joeattardi/emoji-button": "^2.12.1",
"@types/autosize": "^3.0.6",
"@types/js-cookie": "^2.2.6",
"@types/jwt-decode": "^2.2.1",
@ -24,6 +23,7 @@
"@types/node": "^13.11.1",
"autosize": "^4.0.2",
"bootswatch": "^4.3.1",
"choices.js": "^9.0.1",
"classcat": "^4.0.2",
"emoji-short-name": "^1.0.0",
"husky": "^4.2.5",
@ -36,7 +36,6 @@
"markdown-it": "^10.0.0",
"markdown-it-container": "^2.0.0",
"markdown-it-emoji": "^1.4.0",
"mobius1-selectr": "^2.4.13",
"moment": "^2.24.0",
"node-fetch": "^2.6.0",
"parse-domain": "^3.0.2",
@ -75,7 +74,7 @@
"engineStrict": true,
"husky": {
"hooks": {
"pre-commit": "cargo clippy --manifest-path ../server/Cargo.toml --all-targets --all-features -- -D warnings && lint-staged"
"pre-commit": "cargo clippy --manifest-path ../server/Cargo.toml --all-targets --workspace -- -D warnings && lint-staged"
}
},
"lint-staged": {

25
src/components/cake-day.tsx

@ -0,0 +1,25 @@
import { Component } from 'inferno';
import { i18n } from '../i18next';
interface CakeDayProps {
creatorName: string;
}
export class CakeDay extends Component<CakeDayProps, any> {
render() {
return (
<div
className={`mx-2 d-inline-block unselectable pointer`}
data-tippy-content={this.cakeDayTippy()}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-cake"></use>
</svg>
</div>
);
}
cakeDayTippy(): string {
return i18n.t('cake_day_info', { creator_name: this.props.creatorName });
}
}

269
src/components/comment-form.tsx

@ -1,4 +1,5 @@
import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { Prompt } from 'inferno-router';
@ -27,6 +28,7 @@ import emojiShortName from 'emoji-short-name';
import { i18n } from '../i18next';
import { TextAreaWithCounter, MAX_COMMENT_LENGTH } from './post-form';
import { stat } from 'fs';
import { T } from 'inferno-i18next';
interface CommentFormProps {
postId?: number;
@ -34,6 +36,7 @@ interface CommentFormProps {
onReplyCancel?(): any;
edit?: boolean;
disabled?: boolean;
focus?: boolean;
}
interface CommentFormState {
@ -74,7 +77,6 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
super(props, context);
this.tribute = setupTribute();
this.setupEmojiPicker();
this.state = this.emptyState;
@ -100,22 +102,51 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
}
componentDidMount() {
var textarea: any = document.getElementById(this.id);
autosize(textarea);
const isDesktop = window.innerWidth > 768;
if (isDesktop) {
textarea.focus();
let textarea: any = document.getElementById(this.id);
if (textarea) {
autosize(textarea);
const isDesktop = window.innerWidth > 768;
if (isDesktop) {
textarea.focus();
}
this.tribute.attach(textarea);
textarea.addEventListener('tribute-replaced', () => {
this.state.commentForm.content = textarea.value;
this.setState(this.state);
autosize.update(textarea);
});
// Quoting of selected text
let selectedText = window.getSelection().toString();
if (selectedText) {
let quotedText =
selectedText
.split('\n')
.map(t => `> ${t}`)
.join('\n') + '\n\n';
this.state.commentForm.content = quotedText;
this.setState(this.state);
// Not sure why this needs a delay
setTimeout(() => autosize.update(textarea), 10);
}
if (this.props.focus) {
textarea.focus();
}
}
}
componentDidUpdate() {
if (this.state.commentForm.content) {
window.onbeforeunload = () => true;
} else {
window.onbeforeunload = undefined;
}
this.tribute.attach(textarea);
textarea.addEventListener('tribute-replaced', () => {
this.state.commentForm.content = textarea.value;
this.setState(this.state);
autosize.update(textarea);
});
}
componentWillUnmount() {
this.subscription.unsubscribe();
window.onbeforeunload = null;
}
render() {
@ -130,117 +161,131 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
when={this.state.commentForm.content}
message={i18n.t('block_leaving')}
/>
<form
id={this.formId}
onSubmit={linkEvent(this, this.handleCommentSubmit)}
>
<div class="form-group row">
<div className={`col-sm-12`}>
<TextAreaWithCounter
id={this.id}
className={`form-control ${this.state.previewMode && 'd-none'}`}
value={this.state.commentForm.content}
onInput={linkEvent(this, this.handleCommentContentChange)}
onPaste={linkEvent(this, this.handleImageUploadPaste)}
required
disabled={this.props.disabled}
rows={2}
maxLength={MAX_COMMENT_LENGTH}
/>
{this.state.previewMode && (
<div
className="card card-body md-div"
dangerouslySetInnerHTML={mdToHtml(
this.state.commentForm.content
)}
{UserService.Instance.user ? (
<form
id={this.formId}
onSubmit={linkEvent(this, this.handleCommentSubmit)}
>
<div class="form-group row">
<div className={`col-sm-12`}>
<TextAreaWithCounter
id={this.id}
className={`form-control ${
this.state.previewMode && 'd-none'
}`}
value={this.state.commentForm.content}
onInput={linkEvent(this, this.handleCommentContentChange)}
onPaste={linkEvent(this, this.handleImageUploadPaste)}
required
disabled={this.props.disabled}
rows={2}
maxLength={MAX_COMMENT_LENGTH}
/>
)}
</div>
</div>
<div class="row">
<div class="col-sm-12">
<button
type="submit"
class="btn btn-sm btn-secondary mr-2"
disabled={
this.props.disabled || this.state.loading || contentEmpty
}
>
{this.state.loading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
<span>{this.state.buttonTitle}</span>
{this.state.previewMode && (
<div
className="card card-body md-div"
dangerouslySetInnerHTML={mdToHtml(
this.state.commentForm.content
)}
/>
)}
</button>
{this.state.commentForm.content && (
<button
className={`btn btn-sm mr-2 btn-secondary ${
this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
</button>
)}
{this.props.node && (
</div>
</div>
<div class="row">
<div class="col-sm-12">
<button
type="button"
type="submit"
class="btn btn-sm btn-secondary mr-2"
onClick={linkEvent(this, this.handleReplyCancel)}
disabled={this.props.disabled || this.state.loading}
>
{i18n.t('cancel')}
{this.state.loading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
<span>{this.state.buttonTitle}</span>
)}
</button>
)}
<a
href={markdownHelpUrl}
target="_blank"
class="d-inline-block float-right text-muted font-weight-bold"
title={i18n.t('formatting_help')}
rel="noopener"
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-help-circle"></use>
</svg>
</a>
<form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
<label
htmlFor={`file-upload-${this.id}`}
className={`${UserService.Instance.user && 'pointer'}`}
data-tippy-content={i18n.t('upload_image')}
{this.state.commentForm.content && (
<button
className={`btn btn-sm mr-2 btn-secondary ${
this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
</button>
)}
{this.props.node && (
<button
type="button"
class="btn btn-sm btn-secondary mr-2"
onClick={linkEvent(this, this.handleReplyCancel)}
>
{i18n.t('cancel')}
</button>
)}
<a
href={markdownHelpUrl}
target="_blank"
class="d-inline-block float-right text-muted font-weight-bold"
title={i18n.t('formatting_help')}
rel="noopener"
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-image"></use>
<use xlinkHref="#icon-help-circle"></use>
</svg>
</label>
<input
id={`file-upload-${this.id}`}
type="file"
accept="image/*"
name="file"
class="d-none"
disabled={!UserService.Instance.user}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form>
{this.state.imageLoading && (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
)}
<span
onClick={linkEvent(this, this.handleEmojiPickerClick)}
class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold"
data-tippy-content={i18n.t('emoji_picker')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-smile"></use>
</svg>
</span>
</a>
<form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
<label
htmlFor={`file-upload-${this.id}`}
className={`${UserService.Instance.user && 'pointer'}`}
data-tippy-content={i18n.t('upload_image')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-image"></use>
</svg>
</label>
<input
id={`file-upload-${this.id}`}
type="file"
accept="image/*"
name="file"
class="d-none"
disabled={!UserService.Instance.user}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form>
{this.state.imageLoading && (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
)}
<span
onClick={linkEvent(this, this.handleEmojiPickerClick)}
class="pointer unselectable d-inline-block mr-3 float-right text-muted font-weight-bold"
data-tippy-content={i18n.t('emoji_picker')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-smile"></use>
</svg>
</span>
</div>
</div>
</form>
) : (
<div class="alert alert-light" role="alert">
<svg class="icon icon-inline mr-2">
<use xlinkHref="#icon-alert-triangle"></use>
</svg>
<T i18nKey="must_login" class="d-inline">
#
<Link class="alert-link" to="/login">
#
</Link>
</T>
</div>
</form>
)}
</div>
);
}

64
src/components/comment-node.tsx

@ -27,12 +27,14 @@ import {
setupTippy,
colorList,
imagesDownsize,
replaceImageEmbeds,
} from '../utils';
import moment from 'moment';
import { MomentTime } from './moment-time';
import { CommentForm } from './comment-form';
import { CommentNodes } from './comment-nodes';
import { UserListing } from './user-listing';
import { CommunityLink } from './community-link';
import { i18n } from '../i18next';
import { replaceEmojis } from '../custom-emojis';
@ -75,6 +77,7 @@ interface CommentNodeProps {
showCommunity?: boolean;
sort?: CommentSortType;
sortType?: SortType;
enableDownvotes: boolean;
}
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
@ -174,9 +177,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
id: node.comment.creator_id,
local: node.comment.creator_local,
actor_id: node.comment.creator_actor_id,
published: node.comment.creator_published,
}}
/>
</span>
{this.isMod && (
<div className="badge badge-light d-none d-sm-inline mr-2">
{i18n.t('mod')}
@ -200,14 +205,38 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{this.props.showCommunity && (
<>
<span class="mx-1">{i18n.t('to')}</span>
<Link class="mr-2" to={`/c/${node.comment.community_name}`}>
{node.comment.community_name}
<CommunityLink
community={{
name: node.comment.community_name,
id: node.comment.community_id,
local: node.comment.community_local,
actor_id: node.comment.community_actor_id,
}}
/>
<span class="mx-2"></span>
<Link class="mr-2" to={`/post/${node.comment.post_id}`}>
{node.comment.post_name}
</Link>
</>
)}
<span
className={`unselectable pointer ${this.scoreColor}`}
<button
class="btn btn-sm text-muted"
onClick={linkEvent(this, this.handleCommentCollapse)}
>
{this.state.collapsed ? (
<svg class="icon icon-inline">
<use xlinkHref="#icon-plus-square"></use>
</svg>
) : (
<svg class="icon icon-inline">
<use xlinkHref="#icon-minus-square"></use>
</svg>
)}
</button>
{/* This is an expanding spacer for mobile */}
<div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
<button
className={`btn unselectable pointer ${this.scoreColor}`}
onClick={linkEvent(node, this.handleCommentUpvote)}
data-tippy-content={this.pointsTippy}
>
@ -215,7 +244,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<use xlinkHref="#icon-zap"></use>
</svg>
<span class="mr-1">{this.state.score}</span>
</span>
</button>
<span className="mr-1"></span>
<span>
<MomentTime data={node.comment} />
@ -228,6 +257,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
edit
onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked}
focus
/>
)}
{!this.state.showEdit && !this.state.collapsed && (
@ -237,13 +267,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
) : (
<div
className="md-div comment-text-container"
dangerouslySetInnerHTML={{
__html: imagesDownsize(
String(mdToHtml(this.commentUnlessRemoved).__html),
false,
true
),
}}
dangerouslySetInnerHTML={this.formatInnerHTML(
this.commentUnlessRemoved
)}
/>
)}
<div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
@ -287,7 +313,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<span class="ml-1">{this.state.upvotes}</span>
)}
</button>
{WebSocketService.Instance.site.enable_downvotes && (
{this.props.enableDownvotes && (
<button
className={`btn btn-link btn-animate ${
this.state.my_vote == -1
@ -700,6 +726,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
node={node}
onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked}
focus
/>
)}
{node.children && !this.state.collapsed && (
@ -711,6 +738,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
postCreatorId={this.props.postCreatorId}
sort={this.props.sort}
sortType={this.props.sortType}
enableDownvotes={this.props.enableDownvotes}
/>
)}
{/* A collapsed clearfix */}
@ -827,6 +855,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
: node.comment.content;
}
formatInnerHTML(html: string) {
html = imagesDownsize(mdToHtml(html).__html, false, true);
if (!UserService.Instance.user || !UserService.Instance.user.show_nsfw) {
html = replaceImageEmbeds(html);
}
return { __html: html };
}
handleReplyClick(i: CommentNode) {
i.state.showReply = true;
i.setState(i.state);

2
src/components/comment-nodes.tsx

@ -24,6 +24,7 @@ interface CommentNodesProps {
showCommunity?: boolean;
sort?: CommentSortType;
sortType?: SortType;
enableDownvotes: boolean;
}
export class CommentNodes extends Component<
@ -52,6 +53,7 @@ export class CommentNodes extends Component<
showCommunity={this.props.showCommunity}
sort={this.props.sort}
sortType={this.props.sortType}
enableDownvotes={this.props.enableDownvotes}
/>
))}
</div>

51
src/components/communities.tsx

@ -1,5 +1,4 @@
import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
@ -11,9 +10,10 @@ import {
ListCommunitiesForm,
SortType,
WebSocketJsonResponse,
GetSiteResponse,
} from '../interfaces';
import { WebSocketService } from '../services';
import { wsJsonToRes, toast } from '../utils';
import { wsJsonToRes, toast, getPageFromProps } from '../utils';
import { CommunityLink } from './community-link';
import { i18n } from '../i18next';
@ -27,12 +27,16 @@ interface CommunitiesState {
loading: boolean;
}
interface CommunitiesProps {
page: number;
}
export class Communities extends Component<any, CommunitiesState> {
private subscription: Subscription;
private emptyState: CommunitiesState = {
communities: [],
loading: true,
page: this.getPageFromProps(this.props),
page: getPageFromProps(this.props),
};
constructor(props: any, context: any) {
@ -47,27 +51,22 @@ export class Communities extends Component<any, CommunitiesState> {
);
this.refetch();
}
getPageFromProps(props: any): number {
return props.match.params.page ? Number(props.match.params.page) : 1;
WebSocketService.Instance.getSite();
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
componentDidMount() {
document.title = `${i18n.t('communities')} - ${
WebSocketService.Instance.site.name
}`;
static getDerivedStateFromProps(props: any): CommunitiesProps {
return {
page: getPageFromProps(props),
};
}
// Necessary for back button for some reason
componentWillReceiveProps(nextProps: any) {
if (nextProps.history.action == 'POP') {
this.state = this.emptyState;
this.state.page = this.getPageFromProps(nextProps);
componentDidUpdate(_: any, lastState: CommunitiesState) {
if (lastState.page !== this.state.page) {
this.setState({ loading: true });
this.refetch();
}
}
@ -165,7 +164,7 @@ export class Communities extends Component<any, CommunitiesState> {
</button>
)}
{this.state.communities.length == communityLimit && (
{this.state.communities.length > 0 && (
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
@ -177,22 +176,17 @@ export class Communities extends Component<any, CommunitiesState> {
);
}
updateUrl() {
this.props.history.push(`/communities/page/${this.state.page}`);
updateUrl(paramUpdates: CommunitiesProps) {
const page = paramUpdates.page || this.state.page;
this.props.history.push(`/communities/page/${page}`);
}
nextPage(i: Communities) {
i.state.page++;
i.setState(i.state);
i.updateUrl();
i.refetch();
i.updateUrl({ page: i.state.page + 1 });
}
prevPage(i: Communities) {
i.state.page--;
i.setState(i.state);
i.updateUrl();
i.refetch();
i.updateUrl({ page: i.state.page - 1 });
}
handleUnsubscribe(communityId: number) {
@ -244,6 +238,9 @@ export class Communities extends Component<any, CommunitiesState> {
found.subscribed = data.community.subscribed;
found.number_of_subscribers = data.community.number_of_subscribers;
this.setState(this.state);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
document.title = `${i18n.t('communities')} - ${data.site.name}`;
}
}
}

64
src/components/community-form.tsx

@ -8,7 +8,6 @@ import {
Category,
ListCategoriesResponse,
CommunityResponse,
GetSiteResponse,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService } from '../services';
@ -30,13 +29,13 @@ interface CommunityFormProps {
onCancel?(): any;
onCreate?(community: Community): any;
onEdit?(community: Community): any;
enableNsfw: boolean;
}
interface CommunityFormState {
communityForm: CommunityFormI;
categories: Array<Category>;
loading: boolean;
enable_nsfw: boolean;
}
export class CommunityForm extends Component<
@ -56,7 +55,6 @@ export class CommunityForm extends Component<
},
categories: [],
loading: false,
enable_nsfw: null,
};
constructor(props: any, context: any) {
@ -86,7 +84,6 @@ export class CommunityForm extends Component<
);
WebSocketService.Instance.listCategories();
WebSocketService.Instance.getSite();
}
componentDidMount() {
@ -100,8 +97,22 @@ export class CommunityForm extends Component<
});
}
componentDidUpdate() {
if (
!this.state.loading &&
(this.state.communityForm.name ||
this.state.communityForm.title ||
this.state.communityForm.description)
) {
window.onbeforeunload = () => true;
} else {
window.onbeforeunload = undefined;
}
}
componentWillUnmount() {
this.subscription.unsubscribe();
window.onbeforeunload = null;
}
render() {
@ -117,26 +128,27 @@ export class CommunityForm extends Component<
message={i18n.t('block_leaving')}
/>
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
<div class="form-group row">
<label class="col-12 col-form-label" htmlFor="community-name">
{i18n.t('name')}
</label>
<div class="col-12">
<input
type="text"
id="community-name"
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')}
/>
{!this.props.community && (
<div class="form-group row">
<label class="col-12 col-form-label" htmlFor="community-name">
{i18n.t('name')}
</label>
<div class="col-12">
<input
type="text"
id="community-name"
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 class="form-group row">
<label class="col-12 col-form-label" htmlFor="community-title">
{i18n.t('title')}
@ -187,7 +199,7 @@ export class CommunityForm extends Component<
</div>
</div>
{this.state.enable_nsfw && (
{this.props.enableNsfw && (
<div class="form-group row">
<div class="col-12">
<div class="form-check">
@ -303,10 +315,6 @@ export class CommunityForm extends Component<
let data = res.data as CommunityResponse;
this.state.loading = false;
this.props.onEdit(data.community);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enable_nsfw = data.site.enable_nsfw;
this.setState(this.state);
}
}
}

98
src/components/community.tsx

@ -23,6 +23,8 @@ import {
GetCommentsResponse,
CommentResponse,
WebSocketJsonResponse,
GetSiteResponse,
Site,
} from '../interfaces';
import { WebSocketService } from '../services';
import { PostListings } from './post-listings';
@ -60,6 +62,19 @@ interface State {
dataType: DataType;
sort: SortType;
page: number;
site: Site;
}
interface CommunityProps {
dataType: DataType;
sort: SortType;
page: number;
}
interface UrlParams {
dataType?: string;
sort?: string;
page?: number;
}
export class Community extends Component<any, State> {
@ -97,6 +112,20 @@ export class Community extends Component<any, State> {
dataType: getDataTypeFromProps(this.props),
sort: getSortTypeFromProps(this.props),
page: getPageFromProps(this.props),
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
},
};
constructor(props: any, context: any) {
@ -119,22 +148,28 @@ export class Community extends Component<any, State> {
name: this.state.communityName ? this.state.communityName : null,
};
WebSocketService.Instance.getCommunity(form);
WebSocketService.Instance.getSite();
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
// Necessary for back button for some reason
componentWillReceiveProps(nextProps: any) {
static getDerivedStateFromProps(props: any): CommunityProps {
return {
dataType: getDataTypeFromProps(props),
sort: getSortTypeFromProps(props),
page: getPageFromProps(props),
};
}
componentDidUpdate(_: any, lastState: State) {
if (
nextProps.history.action == 'POP' ||
nextProps.history.action == 'PUSH'
lastState.dataType !== this.state.dataType ||
lastState.sort !== this.state.sort ||
lastState.page !== this.state.page
) {
this.state.dataType = getDataTypeFromProps(nextProps);
this.state.sort = getSortTypeFromProps(nextProps);
this.state.page = getPageFromProps(nextProps);
this.setState(this.state);
this.setState({ loading: true });
this.fetchData();
}
}
@ -174,6 +209,7 @@ export class Community extends Component<any, State> {
moderators={this.state.moderators}
admins={this.state.admins}
online={this.state.online}
enableNsfw={this.state.site.enable_nsfw}
/>
</aside>
</div>
@ -188,6 +224,8 @@ export class Community extends Component<any, State> {
posts={this.state.posts}
removeDuplicates
sort={this.state.sort}
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/>
) : (
<CommentNodes
@ -195,6 +233,7 @@ export class Community extends Component<any, State> {
noIndent
sortType={this.state.sort}
showContext
enableDownvotes={this.state.site.enable_downvotes}
/>
);
}
@ -238,7 +277,7 @@ export class Community extends Component<any, State> {
{i18n.t('prev')}
</button>
)}
{this.state.posts.length == fetchLimit && (
{this.state.posts.length > 0 && (
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
@ -251,46 +290,33 @@ export class Community extends Component<any, State> {
}
nextPage(i: Community) {
i.state.page++;
i.setState(i.state);
i.updateUrl();
i.fetchData();
i.updateUrl({ page: i.state.page + 1 });
window.scrollTo(0, 0);
}
prevPage(i: Community) {
i.state.page--;
i.setState(i.state);
i.updateUrl();
i.fetchData();
i.updateUrl({ page: i.state.page - 1 });
window.scrollTo(0, 0);
}
handleSortChange(val: SortType) {
this.state.sort = val;
this.state.page = 1;
this.state.loading = true;
this.setState(this.state);
this.updateUrl();
this.fetchData();
this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
window.scrollTo(0, 0);
}
handleDataTypeChange(val: DataType) {
this.state.dataType = val;
this.state.page = 1;
this.state.loading = true;
this.setState(this.state);
this.updateUrl();
this.fetchData();
this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
window.scrollTo(0, 0);
}
updateUrl() {
let dataTypeStr = DataType[this.state.dataType].toLowerCase();
let sortStr = SortType[this.state.sort].toLowerCase();
updateUrl(paramUpdates: UrlParams) {
const dataTypeStr =
paramUpdates.dataType || DataType[this.state.dataType].toLowerCase();
const sortStr =
paramUpdates.sort || SortType[this.state.sort].toLowerCase();
const page = paramUpdates.page || this.state.page;
this.props.history.push(
`/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${this.state.page}`
`/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${page}`
);
}
@ -331,7 +357,7 @@ export class Community extends Component<any, State> {
this.state.moderators = data.moderators;
this.state.admins = data.admins;
this.state.online = data.online;
document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`;
document.title = `/c/${this.state.community.name} - ${this.state.site.name}`;
this.setState(this.state);
this.fetchData();
} else if (res.op == UserOperation.EditCommunity) {
@ -399,6 +425,10 @@ export class Community extends Component<any, State> {
let data = res.data as CommentResponse;
createCommentLikeRes(data, this.state.comments);
this.setState(this.state);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.site = data.site;
this.setState(this.state);
}
}
}

63
src/components/create-community.tsx

@ -1,19 +1,49 @@
import { Component } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { CommunityForm } from './community-form';
import { Community } from '../interfaces';
import { WebSocketService } from '../services';
import {
Community,
UserOperation,
WebSocketJsonResponse,
GetSiteResponse,
} from '../interfaces';
import { toast, wsJsonToRes } from '../utils';
import { WebSocketService, UserService } from '../services';
import { i18n } from '../i18next';
export class CreateCommunity extends Component<any, any> {
interface CreateCommunityState {
enableNsfw: boolean;
}
export class CreateCommunity extends Component<any, CreateCommunityState> {
private subscription: Subscription;
private emptyState: CreateCommunityState = {
enableNsfw: null,
};
constructor(props: any, context: any) {
super(props, context);
this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
this.state = this.emptyState;
if (!UserService.Instance.user) {
toast(i18n.t('not_logged_in'), 'danger');
this.context.router.history.push(`/login`);
}
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
WebSocketService.Instance.getSite();
}
componentDidMount() {
document.title = `${i18n.t('create_community')} - ${
WebSocketService.Instance.site.name
}`;
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
@ -22,7 +52,10 @@ export class CreateCommunity extends Component<any, any> {
<div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t('create_community')}</h5>
<CommunityForm onCreate={this.handleCommunityCreate} />
<CommunityForm
onCreate={this.handleCommunityCreate}
enableNsfw={this.state.enableNsfw}
/>
</div>
</div>
</div>
@ -32,4 +65,18 @@ export class CreateCommunity extends Component<any, any> {
handleCommunityCreate(community: Community) {
this.props.history.push(`/c/${community.name}`);
}
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let res = wsJsonToRes(msg);
if (msg.error) {
// Toast errors are already handled by community-form
return;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enableNsfw = data.site.enable_nsfw;
this.setState(this.state);
document.title = `${i18n.t('create_community')} - ${data.site.name}`;
}
}
}

80
src/components/create-post.tsx

@ -1,19 +1,64 @@
import { Component } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { PostForm } from './post-form';
import { WebSocketService } from '../services';
import { PostFormParams } from '../interfaces';
import { toast, wsJsonToRes } from '../utils';
import { WebSocketService, UserService } from '../services';
import {
UserOperation,
PostFormParams,
WebSocketJsonResponse,
GetSiteResponse,
Site,
} from '../interfaces';
import { i18n } from '../i18next';
export class CreatePost extends Component<any, any> {
interface CreatePostState {
site: Site;
}
export class CreatePost extends Component<any, CreatePostState> {
private subscription: Subscription;
private emptyState: CreatePostState = {
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
},
};
constructor(props: any, context: any) {
super(props, context);
this.handlePostCreate = this.handlePostCreate.bind(this);
this.state = this.emptyState;
if (!UserService.Instance.user) {
toast(i18n.t('not_logged_in'), 'danger');
this.context.router.history.push(`/login`);
}
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
WebSocketService.Instance.getSite();
}
componentDidMount() {
document.title = `${i18n.t('create_post')} - ${
WebSocketService.Instance.site.name
}`;
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
@ -22,7 +67,12 @@ export class CreatePost extends Component<any, any> {
<div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t('create_post')}</h5>
<PostForm onCreate={this.handlePostCreate} params={this.params} />
<PostForm
onCreate={this.handlePostCreate}
params={this.params}
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/>
</div>
</div>
</div>
@ -56,4 +106,18 @@ export class CreatePost extends Component<any, any> {
handlePostCreate(id: number) {
this.props.history.push(`/post/${id}`);
}
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.site = data.site;
this.setState(this.state);
document.title = `${i18n.t('create_post')} - ${data.site.name}`;
}
}
}

49
src/components/create-private-message.tsx

@ -1,22 +1,43 @@
import { Component } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { PrivateMessageForm } from './private-message-form';
import { WebSocketService } from '../services';
import { PrivateMessageFormParams } from '../interfaces';
import { toast } from '../utils';
import { WebSocketService, UserService } from '../services';
import {
UserOperation,
WebSocketJsonResponse,
GetSiteResponse,
PrivateMessageFormParams,
} from '../interfaces';
import { toast, wsJsonToRes } from '../utils';
import { i18n } from '../i18next';
export class CreatePrivateMessage extends Component<any, any> {
private subscription: Subscription;
constructor(props: any, context: any) {
super(props, context);
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
this
);
if (!UserService.Instance.user) {
toast(i18n.t('not_logged_in'), 'danger');
this.context.router.history.push(`/login`);
}
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
WebSocketService.Instance.getSite();
}
componentDidMount() {
document.title = `${i18n.t('create_private_message')} - ${
WebSocketService.Instance.site.name
}`;
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
@ -50,4 +71,18 @@ export class CreatePrivateMessage extends Component<any, any> {
// Navigate to the front
this.props.history.push(`/`);
}
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
document.title = `${i18n.t('create_private_message')} - ${
data.site.name
}`;
}
}
}

15
src/components/data-type-select.tsx

@ -25,6 +25,12 @@ export class DataTypeSelect extends Component<
this.state = this.emptyState;
}
static getDerivedStateFromProps(props: any): DataTypeSelectProps {
return {
type_: props.type_,
};
}
render() {
return (
<div class="btn-group btn-group-toggle">
@ -42,8 +48,9 @@ export class DataTypeSelect extends Component<
{i18n.t('posts')}
</label>
<label
className={`pointer btn btn-sm btn-secondary ${this.state.type_ ==
DataType.Comment && 'active'}`}
className={`pointer btn btn-sm btn-secondary ${
this.state.type_ == DataType.Comment && 'active'
}`}
>
<input
type="radio"
@ -58,8 +65,6 @@ export class DataTypeSelect extends Component<
}
handleTypeChange(i: DataTypeSelect, event: any) {
i.state.type_ = Number(event.target.value);
i.setState(i.state);
i.props.onChange(i.state.type_);
i.props.onChange(Number(event.target.value));
}
}

53
src/components/inbox.tsx

@ -16,6 +16,7 @@ import {
GetPrivateMessagesForm,
PrivateMessagesResponse,
PrivateMessageResponse,
GetSiteResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
@ -56,6 +57,7 @@ interface InboxState {
messages: Array<PrivateMessageI>;