Merge branch 'LemmyNet:main' into multiple-images-upload

This commit is contained in:
sam365724 2022-09-30 22:36:55 +02:00 committed by GitHub
commit 059dfbd3a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 4220 additions and 3787 deletions

View file

@ -8,7 +8,8 @@
], ],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended",
"plugin:inferno/recommended"
], ],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {

@ -1 +1 @@
Subproject commit 7ac48ae98271b3b573e28c90b87f9704492e0b62 Subproject commit ae3132fef13542ab7fc337361bf484183d12d786

View file

@ -25,13 +25,13 @@
"emoji-short-name": "^2.0.0", "emoji-short-name": "^2.0.0",
"express": "~4.18.1", "express": "~4.18.1",
"i18next": "^21.8.14", "i18next": "^21.8.14",
"inferno": "^7.4.11", "inferno": "^8.0.3",
"inferno-create-element": "^7.4.11", "inferno-create-element": "^8.0.3",
"inferno-helmet": "^5.2.1", "inferno-helmet": "^5.2.1",
"inferno-hydrate": "^7.4.11", "inferno-hydrate": "^8.0.3",
"inferno-i18next-dess": "^0.0.1", "inferno-i18next-dess": "0.0.2",
"inferno-router": "^7.4.11", "inferno-router": "^8.0.3",
"inferno-server": "^7.4.11", "inferno-server": "^8.0.3",
"isomorphic-cookie": "^1.2.4", "isomorphic-cookie": "^1.2.4",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
@ -55,7 +55,7 @@
"@babel/plugin-proposal-decorators": "^7.18.9", "@babel/plugin-proposal-decorators": "^7.18.9",
"@babel/plugin-transform-runtime": "^7.18.9", "@babel/plugin-transform-runtime": "^7.18.9",
"@babel/plugin-transform-typescript": "^7.18.8", "@babel/plugin-transform-typescript": "^7.18.8",
"@babel/preset-env": "7.18.9", "@babel/preset-env": "7.19.3",
"@babel/preset-typescript": "^7.18.6", "@babel/preset-typescript": "^7.18.6",
"@babel/runtime": "^7.18.9", "@babel/runtime": "^7.18.9",
"@sniptt/monads": "^0.5.10", "@sniptt/monads": "^0.5.10",
@ -74,10 +74,11 @@
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.1", "css-loader": "^6.7.1",
"eslint": "^8.20.0", "eslint": "^8.20.0",
"eslint-plugin-inferno": "^7.31.8",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.1", "husky": "^8.0.1",
"import-sort-style-module": "^6.0.0", "import-sort-style-module": "^6.0.0",
"lemmy-js-client": "0.17.0-rc.43", "lemmy-js-client": "0.17.0-rc.46",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"mini-css-extract-plugin": "^2.6.1", "mini-css-extract-plugin": "^2.6.1",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
@ -92,10 +93,10 @@
"sortpack": "^2.3.0", "sortpack": "^2.3.0",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"terser": "^5.14.2", "terser": "^5.14.2",
"typescript": "^4.7.4", "typescript": "^4.8.4",
"webpack": "5.74.0", "webpack": "5.74.0",
"webpack-cli": "^4.10.0", "webpack-cli": "^4.10.0",
"webpack-dev-server": "4.9.3", "webpack-dev-server": "4.11.1",
"webpack-node-externals": "^3.0.0" "webpack-node-externals": "^3.0.0"
}, },
"engines": { "engines": {

View file

@ -53,7 +53,7 @@ section {
} }
body { body {
margin:0; margin:0;
font-family:Lato,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; font-family:Lato,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Cantarell,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
font-size:.9375rem; font-size:.9375rem;
font-weight:400; font-weight:400;
line-height:1.5; line-height:1.5;

File diff suppressed because one or more lines are too long

View file

@ -42,7 +42,7 @@ export class App extends Component<any, any> {
none: <></>, none: <></>,
})} })}
<Navbar siteRes={siteRes} /> <Navbar siteRes={siteRes} />
<div class="mt-4 p-0 fl-1"> <div className="mt-4 p-0 fl-1">
<Switch> <Switch>
{routes.map(({ path, exact, component: C, ...rest }) => ( {routes.map(({ path, exact, component: C, ...rest }) => (
<Route <Route

View file

@ -16,16 +16,16 @@ export class Footer extends Component<FooterProps, any> {
render() { render() {
return ( return (
<nav class="container navbar navbar-expand-md navbar-light navbar-bg p-3"> <nav className="container navbar navbar-expand-md navbar-light navbar-bg p-3">
<div className="navbar-collapse"> <div className="navbar-collapse">
<ul class="navbar-nav ml-auto"> <ul className="navbar-nav ml-auto">
{this.props.site.version !== VERSION && ( {this.props.site.version !== VERSION && (
<li class="nav-item"> <li className="nav-item">
<span class="nav-link">UI: {VERSION}</span> <span className="nav-link">UI: {VERSION}</span>
</li> </li>
)} )}
<li class="nav-item"> <li className="nav-item">
<span class="nav-link">BE: {this.props.site.version}</span> <span className="nav-link">BE: {this.props.site.version}</span>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<NavLink className="nav-link" to="/modlog"> <NavLink className="nav-link" to="/modlog">
@ -42,23 +42,23 @@ export class Footer extends Component<FooterProps, any> {
</li> </li>
)} )}
{this.props.site.federated_instances && ( {this.props.site.federated_instances && (
<li class="nav-item"> <li className="nav-item">
<NavLink className="nav-link" to="/instances"> <NavLink className="nav-link" to="/instances">
{i18n.t("instances")} {i18n.t("instances")}
</NavLink> </NavLink>
</li> </li>
)} )}
<li class="nav-item"> <li className="nav-item">
<a className="nav-link" href={docsUrl}> <a className="nav-link" href={docsUrl}>
{i18n.t("docs")} {i18n.t("docs")}
</a> </a>
</li> </li>
<li class="nav-item"> <li className="nav-item">
<a className="nav-link" href={repoUrl}> <a className="nav-link" href={repoUrl}>
{i18n.t("code")} {i18n.t("code")}
</a> </a>
</li> </li>
<li class="nav-item"> <li className="nav-item">
<a className="nav-link" href={joinLemmyUrl}> <a className="nav-link" href={joinLemmyUrl}>
{i18n.t("join_lemmy")} {i18n.t("join_lemmy")}
</a> </a>

View file

@ -1,4 +1,4 @@
import { None, Some } from "@sniptt/monads"; import { None } from "@sniptt/monads";
import { Component, createRef, linkEvent, RefObject } from "inferno"; import { Component, createRef, linkEvent, RefObject } from "inferno";
import { NavLink } from "inferno-router"; import { NavLink } from "inferno-router";
import { import {
@ -21,6 +21,7 @@ import { UserService, WebSocketService } from "../../services";
import { import {
amAdmin, amAdmin,
auth, auth,
canCreateCommunity,
donateLemmyUrl, donateLemmyUrl,
isBrowser, isBrowser,
notifyComment, notifyComment,
@ -144,8 +145,8 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
// TODO class active corresponding to current page // TODO class active corresponding to current page
navbar() { navbar() {
return ( return (
<nav class="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3"> <nav className="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
<div class="container"> <div className="container">
{this.props.siteRes.site_view.match({ {this.props.siteRes.site_view.match({
some: siteView => ( some: siteView => (
<NavLink <NavLink
@ -166,7 +167,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
})} })}
{UserService.Instance.myUserInfo.isSome() && ( {UserService.Instance.myUserInfo.isSome() && (
<> <>
<ul class="navbar-nav ml-auto"> <ul className="navbar-nav ml-auto">
<li className="nav-item"> <li className="nav-item">
<NavLink <NavLink
to="/inbox" to="/inbox"
@ -179,7 +180,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
> >
<Icon icon="bell" /> <Icon icon="bell" />
{this.state.unreadInboxCount > 0 && ( {this.state.unreadInboxCount > 0 && (
<span class="mx-1 badge badge-light"> <span className="mx-1 badge badge-light">
{numToSI(this.state.unreadInboxCount)} {numToSI(this.state.unreadInboxCount)}
</span> </span>
)} )}
@ -187,7 +188,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</li> </li>
</ul> </ul>
{this.moderatesSomething && ( {this.moderatesSomething && (
<ul class="navbar-nav ml-1"> <ul className="navbar-nav ml-1">
<li className="nav-item"> <li className="nav-item">
<NavLink <NavLink
to="/reports" to="/reports"
@ -200,7 +201,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
> >
<Icon icon="shield" /> <Icon icon="shield" />
{this.state.unreadReportCount > 0 && ( {this.state.unreadReportCount > 0 && (
<span class="mx-1 badge badge-light"> <span className="mx-1 badge badge-light">
{numToSI(this.state.unreadReportCount)} {numToSI(this.state.unreadReportCount)}
</span> </span>
)} )}
@ -208,8 +209,8 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</li> </li>
</ul> </ul>
)} )}
{this.amAdmin && ( {amAdmin() && (
<ul class="navbar-nav ml-1"> <ul className="navbar-nav ml-1">
<li className="nav-item"> <li className="nav-item">
<NavLink <NavLink
to="/registration_applications" to="/registration_applications"
@ -224,7 +225,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
> >
<Icon icon="clipboard" /> <Icon icon="clipboard" />
{this.state.unreadApplicationCount > 0 && ( {this.state.unreadApplicationCount > 0 && (
<span class="mx-1 badge badge-light"> <span className="mx-1 badge badge-light">
{numToSI(this.state.unreadApplicationCount)} {numToSI(this.state.unreadApplicationCount)}
</span> </span>
)} )}
@ -235,7 +236,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</> </>
)} )}
<button <button
class="navbar-toggler border-0 p-1" className="navbar-toggler border-0 p-1"
type="button" type="button"
aria-label="menu" aria-label="menu"
onClick={linkEvent(this, this.handleToggleExpandNavbar)} onClick={linkEvent(this, this.handleToggleExpandNavbar)}
@ -246,8 +247,8 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
<div <div
className={`${!this.state.expanded && "collapse"} navbar-collapse`} className={`${!this.state.expanded && "collapse"} navbar-collapse`}
> >
<ul class="navbar-nav my-2 mr-auto"> <ul className="navbar-nav my-2 mr-auto">
<li class="nav-item"> <li className="nav-item">
<NavLink <NavLink
to="/communities" to="/communities"
className="nav-link" className="nav-link"
@ -257,11 +258,15 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
{i18n.t("communities")} {i18n.t("communities")}
</NavLink> </NavLink>
</li> </li>
<li class="nav-item"> <li className="nav-item">
{/* TODO make sure this works: https://github.com/infernojs/inferno/issues/1608 */}
<NavLink <NavLink
to={{ to={{
pathname: "/create_post", pathname: "/create_post",
prevPath: this.currentLocation, search: "",
hash: "",
key: "",
state: { prevPath: this.currentLocation },
}} }}
className="nav-link" className="nav-link"
onMouseUp={linkEvent(this, this.handleHideExpandNavbar)} onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
@ -270,8 +275,8 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
{i18n.t("create_post")} {i18n.t("create_post")}
</NavLink> </NavLink>
</li> </li>
{this.canCreateCommunity && ( {canCreateCommunity(this.props.siteRes) && (
<li class="nav-item"> <li className="nav-item">
<NavLink <NavLink
to="/create_community" to="/create_community"
className="nav-link" className="nav-link"
@ -282,7 +287,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</NavLink> </NavLink>
</li> </li>
)} )}
<li class="nav-item"> <li className="nav-item">
<a <a
className="nav-link" className="nav-link"
title={i18n.t("support_lemmy")} title={i18n.t("support_lemmy")}
@ -292,8 +297,8 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</a> </a>
</li> </li>
</ul> </ul>
<ul class="navbar-nav my-2"> <ul className="navbar-nav my-2">
{this.amAdmin && ( {amAdmin() && (
<li className="nav-item"> <li className="nav-item">
<NavLink <NavLink
to="/admin" to="/admin"
@ -310,12 +315,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
/^\/search/ /^\/search/
) && ( ) && (
<form <form
class="form-inline mr-2" className="form-inline mr-2"
onSubmit={linkEvent(this, this.handleSearchSubmit)} onSubmit={linkEvent(this, this.handleSearchSubmit)}
> >
<input <input
id="search-input" id="search-input"
class={`form-control mr-0 search-input ${ className={`form-control mr-0 search-input ${
this.state.toggleSearch ? "show-input" : "hide-input" this.state.toggleSearch ? "show-input" : "hide-input"
}`} }`}
onInput={linkEvent(this, this.handleSearchParam)} onInput={linkEvent(this, this.handleSearchParam)}
@ -325,13 +330,13 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
placeholder={i18n.t("search")} placeholder={i18n.t("search")}
onBlur={linkEvent(this, this.handleSearchBlur)} onBlur={linkEvent(this, this.handleSearchBlur)}
></input> ></input>
<label class="sr-only" htmlFor="search-input"> <label className="sr-only" htmlFor="search-input">
{i18n.t("search")} {i18n.t("search")}
</label> </label>
<button <button
name="search-btn" name="search-btn"
onClick={linkEvent(this, this.handleSearchBtn)} onClick={linkEvent(this, this.handleSearchBtn)}
class="px-1 btn btn-link" className="px-1 btn btn-link"
style="color: var(--gray)" style="color: var(--gray)"
aria-label={i18n.t("search")} aria-label={i18n.t("search")}
> >
@ -341,7 +346,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
)} )}
{UserService.Instance.myUserInfo.isSome() ? ( {UserService.Instance.myUserInfo.isSome() ? (
<> <>
<ul class="navbar-nav my-2"> <ul className="navbar-nav my-2">
<li className="nav-item"> <li className="nav-item">
<NavLink <NavLink
className="nav-link" className="nav-link"
@ -354,7 +359,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
> >
<Icon icon="bell" /> <Icon icon="bell" />
{this.state.unreadInboxCount > 0 && ( {this.state.unreadInboxCount > 0 && (
<span class="ml-1 badge badge-light"> <span className="ml-1 badge badge-light">
{numToSI(this.state.unreadInboxCount)} {numToSI(this.state.unreadInboxCount)}
</span> </span>
)} )}
@ -362,7 +367,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</li> </li>
</ul> </ul>
{this.moderatesSomething && ( {this.moderatesSomething && (
<ul class="navbar-nav my-2"> <ul className="navbar-nav my-2">
<li className="nav-item"> <li className="nav-item">
<NavLink <NavLink
className="nav-link" className="nav-link"
@ -375,7 +380,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
> >
<Icon icon="shield" /> <Icon icon="shield" />
{this.state.unreadReportCount > 0 && ( {this.state.unreadReportCount > 0 && (
<span class="ml-1 badge badge-light"> <span className="ml-1 badge badge-light">
{numToSI(this.state.unreadReportCount)} {numToSI(this.state.unreadReportCount)}
</span> </span>
)} )}
@ -383,8 +388,8 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</li> </li>
</ul> </ul>
)} )}
{this.amAdmin && ( {amAdmin() && (
<ul class="navbar-nav my-2"> <ul className="navbar-nav my-2">
<li className="nav-item"> <li className="nav-item">
<NavLink <NavLink
to="/registration_applications" to="/registration_applications"
@ -399,7 +404,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
> >
<Icon icon="clipboard" /> <Icon icon="clipboard" />
{this.state.unreadApplicationCount > 0 && ( {this.state.unreadApplicationCount > 0 && (
<span class="mx-1 badge badge-light"> <span className="mx-1 badge badge-light">
{numToSI(this.state.unreadApplicationCount)} {numToSI(this.state.unreadApplicationCount)}
</span> </span>
)} )}
@ -411,10 +416,10 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
.map(m => m.local_user_view.person) .map(m => m.local_user_view.person)
.match({ .match({
some: person => ( some: person => (
<ul class="navbar-nav"> <ul className="navbar-nav">
<li class="nav-item dropdown"> <li className="nav-item dropdown">
<button <button
class="nav-link btn btn-link dropdown-toggle" className="nav-link btn btn-link dropdown-toggle"
onClick={linkEvent(this, this.handleToggleDropdown)} onClick={linkEvent(this, this.handleToggleDropdown)}
id="navbarDropdown" id="navbarDropdown"
role="button" role="button"
@ -433,7 +438,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</button> </button>
{this.state.showDropdown && ( {this.state.showDropdown && (
<div <div
class="dropdown-content" className="dropdown-content"
onMouseLeave={linkEvent( onMouseLeave={linkEvent(
this, this,
this.handleToggleDropdown this.handleToggleDropdown
@ -460,7 +465,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</NavLink> </NavLink>
</li> </li>
<li> <li>
<hr class="dropdown-divider" /> <hr className="dropdown-divider" />
</li> </li>
<li className="nav-item"> <li className="nav-item">
<button <button
@ -484,7 +489,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
})} })}
</> </>
) : ( ) : (
<ul class="navbar-nav my-2"> <ul className="navbar-nav my-2">
<li className="nav-item"> <li className="nav-item">
<NavLink <NavLink
to="/login" to="/login"
@ -520,20 +525,8 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
); );
} }
get amAdmin(): boolean {
return amAdmin(Some(this.props.siteRes.admins));
}
get canCreateCommunity(): boolean {
let adminOnly = this.props.siteRes.site_view
.map(s => s.site.community_creation_admin_only)
.unwrapOr(false);
return !adminOnly || this.amAdmin;
}
handleToggleExpandNavbar(i: Navbar) { handleToggleExpandNavbar(i: Navbar) {
i.state.expanded = !i.state.expanded; i.setState({ expanded: !i.state.expanded });
i.setState(i.state);
} }
handleHideExpandNavbar(i: Navbar) { handleHideExpandNavbar(i: Navbar) {
@ -541,8 +534,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
} }
handleSearchParam(i: Navbar, event: any) { handleSearchParam(i: Navbar, event: any) {
i.state.searchParam = event.target.value; i.setState({ searchParam: event.target.value });
i.setState(i.state);
} }
handleSearchSubmit(i: Navbar, event: any) { handleSearchSubmit(i: Navbar, event: any) {
@ -563,8 +555,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
handleSearchBlur(i: Navbar, event: any) { handleSearchBlur(i: Navbar, event: any) {
if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) { if (!(event.relatedTarget && event.relatedTarget.name !== "search-btn")) {
i.state.toggleSearch = false; i.setState({ toggleSearch: false });
i.setState(i.state);
} }
} }
@ -574,8 +565,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
} }
handleToggleDropdown(i: Navbar) { handleToggleDropdown(i: Navbar) {
i.state.showDropdown = !i.state.showDropdown; i.setState({ showDropdown: !i.state.showDropdown });
i.setState(i.state);
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -601,25 +591,28 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
msg, msg,
GetUnreadCountResponse GetUnreadCountResponse
); );
this.state.unreadInboxCount = this.setState({
data.replies + data.mentions + data.private_messages; unreadInboxCount: data.replies + data.mentions + data.private_messages,
this.setState(this.state); });
this.sendUnreadCount(); this.sendUnreadCount();
} else if (op == UserOperation.GetReportCount) { } else if (op == UserOperation.GetReportCount) {
let data = wsJsonToRes<GetReportCountResponse>( let data = wsJsonToRes<GetReportCountResponse>(
msg, msg,
GetReportCountResponse GetReportCountResponse
); );
this.state.unreadReportCount = data.post_reports + data.comment_reports; this.setState({
this.setState(this.state); unreadReportCount:
data.post_reports +
data.comment_reports +
data.private_message_reports.unwrapOr(0),
});
this.sendReportUnread(); this.sendReportUnread();
} else if (op == UserOperation.GetUnreadRegistrationApplicationCount) { } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
let data = wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>( let data = wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(
msg, msg,
GetUnreadRegistrationApplicationCountResponse GetUnreadRegistrationApplicationCountResponse
); );
this.state.unreadApplicationCount = data.registration_applications; this.setState({ unreadApplicationCount: data.registration_applications });
this.setState(this.state);
this.sendApplicationUnread(); this.sendApplicationUnread();
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
@ -627,8 +620,9 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
UserService.Instance.myUserInfo.match({ UserService.Instance.myUserInfo.match({
some: mui => { some: mui => {
if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) { if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) {
this.state.unreadInboxCount++; this.setState({
this.setState(this.state); unreadInboxCount: this.state.unreadInboxCount + 1,
});
this.sendUnreadCount(); this.sendUnreadCount();
notifyComment(data.comment_view, this.context.router); notifyComment(data.comment_view, this.context.router);
} }
@ -647,8 +641,9 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
data.private_message_view.recipient.id == data.private_message_view.recipient.id ==
mui.local_user_view.person.id mui.local_user_view.person.id
) { ) {
this.state.unreadInboxCount++; this.setState({
this.setState(this.state); unreadInboxCount: this.state.unreadInboxCount + 1,
});
this.sendUnreadCount(); this.sendUnreadCount();
notifyPrivateMessage( notifyPrivateMessage(
data.private_message_view, data.private_message_view,
@ -677,7 +672,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
}); });
WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm)); WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
if (this.amAdmin) { if (amAdmin()) {
console.log("Fetching applications..."); console.log("Fetching applications...");
let applicationCountForm = new GetUnreadRegistrationApplicationCount({ let applicationCountForm = new GetUnreadRegistrationApplicationCount({

View file

@ -13,7 +13,7 @@ export class NoMatch extends Component<any, any> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
<h1>404</h1> <h1>404</h1>
{this.errCode && ( {this.errCode && (
<h3> <h3>

View file

@ -7,6 +7,7 @@ import {
CommentResponse, CommentResponse,
CreateComment, CreateComment,
EditComment, EditComment,
Language,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
@ -17,6 +18,7 @@ import { UserService, WebSocketService } from "../../services";
import { import {
auth, auth,
capitalizeFirstLetter, capitalizeFirstLetter,
myFirstDiscussionLanguageId,
wsClient, wsClient,
wsSubscribe, wsSubscribe,
} from "../../utils"; } from "../../utils";
@ -32,6 +34,7 @@ interface CommentFormProps {
disabled?: boolean; disabled?: boolean;
focus?: boolean; focus?: boolean;
onReplyCancel?(): any; onReplyCancel?(): any;
allLanguages: Language[];
} }
interface CommentFormState { interface CommentFormState {
@ -74,11 +77,19 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
this.props.edit ? Some(node.comment_view.comment.content) : None, this.props.edit ? Some(node.comment_view.comment.content) : None,
right: () => None, right: () => None,
}); });
let selectedLang = this.props.node
.left()
.map(n => n.comment_view.comment.language_id)
.or(myFirstDiscussionLanguageId(UserService.Instance.myUserInfo));
return ( return (
<div class="mb-3"> <div className="mb-3">
{UserService.Instance.myUserInfo.isSome() ? ( {UserService.Instance.myUserInfo.isSome() ? (
<MarkdownTextArea <MarkdownTextArea
initialContent={initialContent} initialContent={initialContent}
initialLanguageId={selectedLang}
showLanguage
buttonTitle={Some(this.state.buttonTitle)} buttonTitle={Some(this.state.buttonTitle)}
maxLength={None} maxLength={None}
finished={this.state.finished} finished={this.state.finished}
@ -88,9 +99,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
onSubmit={this.handleCommentSubmit} onSubmit={this.handleCommentSubmit}
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
placeholder={Some(i18n.t("comment_here"))} placeholder={Some(i18n.t("comment_here"))}
allLanguages={this.props.allLanguages}
/> />
) : ( ) : (
<div class="alert alert-warning" role="alert"> <div className="alert alert-warning" role="alert">
<Icon icon="alert-triangle" classes="icon-inline mr-2" /> <Icon icon="alert-triangle" classes="icon-inline mr-2" />
<T i18nKey="must_login" class="d-inline"> <T i18nKey="must_login" class="d-inline">
# #
@ -104,27 +116,34 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
); );
} }
handleCommentSubmit(msg: { val: string; formId: string }) { handleCommentSubmit(msg: {
val: Option<string>;
formId: string;
languageId: Option<number>;
}) {
let content = msg.val; let content = msg.val;
this.state.formId = Some(msg.formId); let language_id = msg.languageId;
this.setState({ formId: Some(msg.formId) });
this.props.node.match({ this.props.node.match({
left: node => { left: node => {
if (this.props.edit) { if (this.props.edit) {
let form = new EditComment({ let form = new EditComment({
content: Some(content), content,
distinguished: None, distinguished: None,
form_id: this.state.formId, form_id: this.state.formId,
comment_id: node.comment_view.comment.id, comment_id: node.comment_view.comment.id,
language_id,
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.editComment(form)); WebSocketService.Instance.send(wsClient.editComment(form));
} else { } else {
let form = new CreateComment({ let form = new CreateComment({
content, content: content.unwrap(),
form_id: this.state.formId, form_id: this.state.formId,
post_id: node.comment_view.post.id, post_id: node.comment_view.post.id,
parent_id: Some(node.comment_view.comment.id), parent_id: Some(node.comment_view.comment.id),
language_id,
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.createComment(form)); WebSocketService.Instance.send(wsClient.createComment(form));
@ -132,10 +151,11 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
}, },
right: postId => { right: postId => {
let form = new CreateComment({ let form = new CreateComment({
content, content: content.unwrap(),
form_id: this.state.formId, form_id: this.state.formId,
post_id: postId, post_id: postId,
parent_id: None, parent_id: None,
language_id,
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.createComment(form)); WebSocketService.Instance.send(wsClient.createComment(form));

View file

@ -17,6 +17,7 @@ import {
DeleteComment, DeleteComment,
EditComment, EditComment,
GetComments, GetComments,
Language,
ListingType, ListingType,
MarkCommentReplyAsRead, MarkCommentReplyAsRead,
MarkPersonMentionAsRead, MarkPersonMentionAsRead,
@ -101,6 +102,7 @@ interface CommentNodeProps {
showCommunity?: boolean; showCommunity?: boolean;
enableDownvotes: boolean; enableDownvotes: boolean;
viewType: CommentViewType; viewType: CommentViewType;
allLanguages: Language[];
} }
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
@ -147,13 +149,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
// TODO see if there's a better way to do this, and all willReceiveProps // TODO see if there's a better way to do this, and all willReceiveProps
componentWillReceiveProps(nextProps: CommentNodeProps) { componentWillReceiveProps(nextProps: CommentNodeProps) {
let cv = nextProps.node.comment_view; let cv = nextProps.node.comment_view;
this.state.my_vote = cv.my_vote; this.setState({
this.state.upvotes = cv.counts.upvotes; my_vote: cv.my_vote,
this.state.downvotes = cv.counts.downvotes; upvotes: cv.counts.upvotes,
this.state.score = cv.counts.score; downvotes: cv.counts.downvotes,
this.state.readLoading = false; score: cv.counts.score,
this.state.saveLoading = false; readLoading: false,
this.setState(this.state); saveLoading: false,
});
} }
render() { render() {
@ -227,10 +230,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
> >
<div <div
class={`${!this.props.noIndent && this.props.node.depth && "ml-2"}`} className={`${
!this.props.noIndent && this.props.node.depth && "ml-2"
}`}
> >
<div class="d-flex flex-wrap align-items-center text-muted small"> <div className="d-flex flex-wrap align-items-center text-muted small">
<span class="mr-2"> <span className="mr-2">
<PersonListing person={cv.creator} /> <PersonListing person={cv.creator} />
</span> </span>
{cv.comment.distinguished && ( {cv.comment.distinguished && (
@ -263,16 +268,16 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
)} )}
{this.props.showCommunity && ( {this.props.showCommunity && (
<> <>
<span class="mx-1">{i18n.t("to")}</span> <span className="mx-1">{i18n.t("to")}</span>
<CommunityLink community={cv.community} /> <CommunityLink community={cv.community} />
<span class="mx-2"></span> <span className="mx-2"></span>
<Link className="mr-2" to={`/post/${cv.post.id}`}> <Link className="mr-2" to={`/post/${cv.post.id}`}>
{cv.post.name} {cv.post.name}
</Link> </Link>
</> </>
)} )}
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
onClick={linkEvent(this, this.handleCommentCollapse)} onClick={linkEvent(this, this.handleCommentCollapse)}
aria-label={this.expandText} aria-label={this.expandText}
data-tippy-content={this.expandText} data-tippy-content={this.expandText}
@ -294,7 +299,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
data-tippy-content={this.pointsTippy} data-tippy-content={this.pointsTippy}
> >
<span <span
class="mr-1 font-weight-bold" className="mr-1 font-weight-bold"
aria-label={i18n.t("number_of_points", { aria-label={i18n.t("number_of_points", {
count: this.state.score, count: this.state.score,
formattedCount: this.state.score, formattedCount: this.state.score,
@ -321,6 +326,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked} disabled={this.props.locked}
focus focus
allLanguages={this.props.allLanguages}
/> />
)} )}
{!this.state.showEdit && !this.state.collapsed && ( {!this.state.showEdit && !this.state.collapsed && (
@ -335,11 +341,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
)} )}
/> />
)} )}
<div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold"> <div className="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold">
{this.props.showContext && this.linkBtn()} {this.props.showContext && this.linkBtn()}
{this.props.markable && ( {this.props.markable && (
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleMarkRead)} onClick={linkEvent(this, this.handleMarkRead)}
data-tippy-content={ data-tippy-content={
this.commentReplyOrMentionRead this.commentReplyOrMentionRead
@ -380,7 +386,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<Icon icon="arrow-up1" classes="icon-inline" /> <Icon icon="arrow-up1" classes="icon-inline" />
{showScores() && {showScores() &&
this.state.upvotes !== this.state.score && ( this.state.upvotes !== this.state.score && (
<span class="ml-1"> <span className="ml-1">
{numToSI(this.state.upvotes)} {numToSI(this.state.upvotes)}
</span> </span>
)} )}
@ -399,14 +405,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<Icon icon="arrow-down1" classes="icon-inline" /> <Icon icon="arrow-down1" classes="icon-inline" />
{showScores() && {showScores() &&
this.state.upvotes !== this.state.score && ( this.state.upvotes !== this.state.score && (
<span class="ml-1"> <span className="ml-1">
{numToSI(this.state.downvotes)} {numToSI(this.state.downvotes)}
</span> </span>
)} )}
</button> </button>
)} )}
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleReplyClick)} onClick={linkEvent(this, this.handleReplyClick)}
data-tippy-content={i18n.t("reply")} data-tippy-content={i18n.t("reply")}
aria-label={i18n.t("reply")} aria-label={i18n.t("reply")}
@ -426,7 +432,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<> <>
{!this.myComment && ( {!this.myComment && (
<> <>
<button class="btn btn-link btn-animate"> <button className="btn btn-link btn-animate">
<Link <Link
className="text-muted" className="text-muted"
to={`/create_private_message/recipient/${cv.creator.id}`} to={`/create_private_message/recipient/${cv.creator.id}`}
@ -436,7 +442,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</Link> </Link>
</button> </button>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleShowReportDialog this.handleShowReportDialog
@ -449,7 +455,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<Icon icon="flag" /> <Icon icon="flag" />
</button> </button>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleBlockUserClick this.handleBlockUserClick
@ -462,7 +468,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</> </>
)} )}
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleSaveCommentClick this.handleSaveCommentClick
@ -501,7 +507,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{this.myComment && ( {this.myComment && (
<> <>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleEditClick this.handleEditClick
@ -512,7 +518,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<Icon icon="edit" classes="icon-inline" /> <Icon icon="edit" classes="icon-inline" />
</button> </button>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleDeleteClick this.handleDeleteClick
@ -538,7 +544,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{(canModOnSelf || canAdminOnSelf) && ( {(canModOnSelf || canAdminOnSelf) && (
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleDistinguishClick this.handleDistinguishClick
@ -570,7 +576,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<> <>
{!cv.comment.removed ? ( {!cv.comment.removed ? (
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleModRemoveShow this.handleModRemoveShow
@ -581,7 +587,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</button> </button>
) : ( ) : (
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleModRemoveSubmit this.handleModRemoveSubmit
@ -599,7 +605,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{!isMod_ && {!isMod_ &&
(!cv.creator_banned_from_community ? ( (!cv.creator_banned_from_community ? (
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleModBanFromCommunityShow this.handleModBanFromCommunityShow
@ -610,7 +616,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</button> </button>
) : ( ) : (
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleModBanFromCommunitySubmit this.handleModBanFromCommunitySubmit
@ -623,7 +629,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{!cv.creator_banned_from_community && {!cv.creator_banned_from_community &&
(!this.state.showConfirmAppointAsMod ? ( (!this.state.showConfirmAppointAsMod ? (
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleShowConfirmAppointAsMod this.handleShowConfirmAppointAsMod
@ -641,13 +647,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
) : ( ) : (
<> <>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
aria-label={i18n.t("are_you_sure")} aria-label={i18n.t("are_you_sure")}
> >
{i18n.t("are_you_sure")} {i18n.t("are_you_sure")}
</button> </button>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleAddModToCommunity this.handleAddModToCommunity
@ -657,7 +663,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{i18n.t("yes")} {i18n.t("yes")}
</button> </button>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleCancelConfirmAppointAsMod this.handleCancelConfirmAppointAsMod
@ -676,7 +682,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
cv.creator.local && cv.creator.local &&
(!this.state.showConfirmTransferCommunity ? ( (!this.state.showConfirmTransferCommunity ? (
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleShowConfirmTransferCommunity this.handleShowConfirmTransferCommunity
@ -688,13 +694,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
) : ( ) : (
<> <>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
aria-label={i18n.t("are_you_sure")} aria-label={i18n.t("are_you_sure")}
> >
{i18n.t("are_you_sure")} {i18n.t("are_you_sure")}
</button> </button>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleTransferCommunity this.handleTransferCommunity
@ -704,7 +710,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{i18n.t("yes")} {i18n.t("yes")}
</button> </button>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this this
@ -722,7 +728,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{!isAdmin_ && ( {!isAdmin_ && (
<> <>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handlePurgePersonShow this.handlePurgePersonShow
@ -732,7 +738,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{i18n.t("purge_user")} {i18n.t("purge_user")}
</button> </button>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handlePurgeCommentShow this.handlePurgeCommentShow
@ -744,7 +750,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{!isBanned(cv.creator) ? ( {!isBanned(cv.creator) ? (
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleModBanShow this.handleModBanShow
@ -755,7 +761,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</button> </button>
) : ( ) : (
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleModBanSubmit this.handleModBanSubmit
@ -771,7 +777,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
cv.creator.local && cv.creator.local &&
(!this.state.showConfirmAppointAsAdmin ? ( (!this.state.showConfirmAppointAsAdmin ? (
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleShowConfirmAppointAsAdmin this.handleShowConfirmAppointAsAdmin
@ -788,11 +794,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</button> </button>
) : ( ) : (
<> <>
<button class="btn btn-link btn-animate text-muted"> <button className="btn btn-link btn-animate text-muted">
{i18n.t("are_you_sure")} {i18n.t("are_you_sure")}
</button> </button>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleAddAdmin this.handleAddAdmin
@ -802,7 +808,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{i18n.t("yes")} {i18n.t("yes")}
</button> </button>
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleCancelConfirmAppointAsAdmin this.handleCancelConfirmAppointAsAdmin
@ -833,7 +839,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
style={`border-left: 2px ${moreRepliesBorderColor} solid !important`} style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
> >
<button <button
class="btn btn-link text-muted" className="btn btn-link text-muted"
onClick={linkEvent(this, this.handleFetchChildren)} onClick={linkEvent(this, this.handleFetchChildren)}
> >
{i18n.t("x_more_replies", { {i18n.t("x_more_replies", {
@ -847,11 +853,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{/* end of details */} {/* end of details */}
{this.state.showRemoveDialog && ( {this.state.showRemoveDialog && (
<form <form
class="form-inline" className="form-inline"
onSubmit={linkEvent(this, this.handleModRemoveSubmit)} onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
> >
<label <label
class="sr-only" className="sr-only"
htmlFor={`mod-remove-reason-${cv.comment.id}`} htmlFor={`mod-remove-reason-${cv.comment.id}`}
> >
{i18n.t("reason")} {i18n.t("reason")}
@ -859,14 +865,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<input <input
type="text" type="text"
id={`mod-remove-reason-${cv.comment.id}`} id={`mod-remove-reason-${cv.comment.id}`}
class="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={toUndefined(this.state.removeReason)} value={toUndefined(this.state.removeReason)}
onInput={linkEvent(this, this.handleModRemoveReasonChange)} onInput={linkEvent(this, this.handleModRemoveReasonChange)}
/> />
<button <button
type="submit" type="submit"
class="btn btn-secondary" className="btn btn-secondary"
aria-label={i18n.t("remove_comment")} aria-label={i18n.t("remove_comment")}
> >
{i18n.t("remove_comment")} {i18n.t("remove_comment")}
@ -875,24 +881,27 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
)} )}
{this.state.showReportDialog && ( {this.state.showReportDialog && (
<form <form
class="form-inline" className="form-inline"
onSubmit={linkEvent(this, this.handleReportSubmit)} onSubmit={linkEvent(this, this.handleReportSubmit)}
> >
<label class="sr-only" htmlFor={`report-reason-${cv.comment.id}`}> <label
className="sr-only"
htmlFor={`report-reason-${cv.comment.id}`}
>
{i18n.t("reason")} {i18n.t("reason")}
</label> </label>
<input <input
type="text" type="text"
required required
id={`report-reason-${cv.comment.id}`} id={`report-reason-${cv.comment.id}`}
class="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={this.state.reportReason} value={this.state.reportReason}
onInput={linkEvent(this, this.handleReportReasonChange)} onInput={linkEvent(this, this.handleReportReasonChange)}
/> />
<button <button
type="submit" type="submit"
class="btn btn-secondary" className="btn btn-secondary"
aria-label={i18n.t("create_report")} aria-label={i18n.t("create_report")}
> >
{i18n.t("create_report")} {i18n.t("create_report")}
@ -901,9 +910,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
)} )}
{this.state.showBanDialog && ( {this.state.showBanDialog && (
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}> <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
<div class="form-group row col-12"> <div className="form-group row col-12">
<label <label
class="col-form-label" className="col-form-label"
htmlFor={`mod-ban-reason-${cv.comment.id}`} htmlFor={`mod-ban-reason-${cv.comment.id}`}
> >
{i18n.t("reason")} {i18n.t("reason")}
@ -911,13 +920,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<input <input
type="text" type="text"
id={`mod-ban-reason-${cv.comment.id}`} id={`mod-ban-reason-${cv.comment.id}`}
class="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={toUndefined(this.state.banReason)} value={toUndefined(this.state.banReason)}
onInput={linkEvent(this, this.handleModBanReasonChange)} onInput={linkEvent(this, this.handleModBanReasonChange)}
/> />
<label <label
class="col-form-label" className="col-form-label"
htmlFor={`mod-ban-expires-${cv.comment.id}`} htmlFor={`mod-ban-expires-${cv.comment.id}`}
> >
{i18n.t("expires")} {i18n.t("expires")}
@ -925,22 +934,22 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<input <input
type="number" type="number"
id={`mod-ban-expires-${cv.comment.id}`} id={`mod-ban-expires-${cv.comment.id}`}
class="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("number_of_days")} placeholder={i18n.t("number_of_days")}
value={toUndefined(this.state.banExpireDays)} value={toUndefined(this.state.banExpireDays)}
onInput={linkEvent(this, this.handleModBanExpireDaysChange)} onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
/> />
<div class="form-group"> <div className="form-group">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="mod-ban-remove-data" id="mod-ban-remove-data"
type="checkbox" type="checkbox"
checked={this.state.removeData} checked={this.state.removeData}
onChange={linkEvent(this, this.handleModRemoveDataChange)} onChange={linkEvent(this, this.handleModRemoveDataChange)}
/> />
<label <label
class="form-check-label" className="form-check-label"
htmlFor="mod-ban-remove-data" htmlFor="mod-ban-remove-data"
title={i18n.t("remove_content_more")} title={i18n.t("remove_content_more")}
> >
@ -954,10 +963,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{/* <label class="col-form-label">Expires</label> */} {/* <label class="col-form-label">Expires</label> */}
{/* <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 className="form-group row">
<button <button
type="submit" type="submit"
class="btn btn-secondary" className="btn btn-secondary"
aria-label={i18n.t("ban")} aria-label={i18n.t("ban")}
> >
{i18n.t("ban")} {cv.creator.name} {i18n.t("ban")} {cv.creator.name}
@ -969,24 +978,24 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{this.state.showPurgeDialog && ( {this.state.showPurgeDialog && (
<form onSubmit={linkEvent(this, this.handlePurgeSubmit)}> <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
<PurgeWarning /> <PurgeWarning />
<label class="sr-only" htmlFor="purge-reason"> <label className="sr-only" htmlFor="purge-reason">
{i18n.t("reason")} {i18n.t("reason")}
</label> </label>
<input <input
type="text" type="text"
id="purge-reason" id="purge-reason"
class="form-control my-3" className="form-control my-3"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={toUndefined(this.state.purgeReason)} value={toUndefined(this.state.purgeReason)}
onInput={linkEvent(this, this.handlePurgeReasonChange)} onInput={linkEvent(this, this.handlePurgeReasonChange)}
/> />
<div class="form-group row col-12"> <div className="form-group row col-12">
{this.state.purgeLoading ? ( {this.state.purgeLoading ? (
<Spinner /> <Spinner />
) : ( ) : (
<button <button
type="submit" type="submit"
class="btn btn-secondary" className="btn btn-secondary"
aria-label={purgeTypeText} aria-label={purgeTypeText}
> >
{purgeTypeText} {purgeTypeText}
@ -1001,6 +1010,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked} disabled={this.props.locked}
focus focus
allLanguages={this.props.allLanguages}
/> />
)} )}
{!this.state.collapsed && node.children.length > 0 && ( {!this.state.collapsed && node.children.length > 0 && (
@ -1012,10 +1022,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
maxCommentsShown={None} maxCommentsShown={None}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
viewType={this.props.viewType} viewType={this.props.viewType}
allLanguages={this.props.allLanguages}
/> />
)} )}
{/* A collapsed clearfix */} {/* A collapsed clearfix */}
{this.state.collapsed && <div class="row col-12"></div>} {this.state.collapsed && <div className="row col-12"></div>}
</div> </div>
); );
} }
@ -1090,13 +1101,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
handleReplyClick(i: CommentNode) { handleReplyClick(i: CommentNode) {
i.state.showReply = true; i.setState({ showReply: true });
i.setState(i.state);
} }
handleEditClick(i: CommentNode) { handleEditClick(i: CommentNode) {
i.state.showEdit = true; i.setState({ showEdit: true });
i.setState(i.state);
} }
handleBlockUserClick(i: CommentNode) { handleBlockUserClick(i: CommentNode) {
@ -1129,14 +1138,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
WebSocketService.Instance.send(wsClient.saveComment(form)); WebSocketService.Instance.send(wsClient.saveComment(form));
i.state.saveLoading = true; i.setState({ saveLoading: true });
i.setState(this.state);
} }
handleReplyCancel() { handleReplyCancel() {
this.state.showReply = false; this.setState({ showReply: false, showEdit: false });
this.state.showEdit = false;
this.setState(this.state);
} }
handleCommentUpvote(event: any) { handleCommentUpvote(event: any) {
@ -1145,18 +1151,24 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
let newVote = myVote == 1 ? 0 : 1; let newVote = myVote == 1 ? 0 : 1;
if (myVote == 1) { if (myVote == 1) {
this.state.score--; this.setState({
this.state.upvotes--; score: this.state.score - 1,
upvotes: this.state.upvotes - 1,
});
} else if (myVote == -1) { } else if (myVote == -1) {
this.state.downvotes--; this.setState({
this.state.upvotes++; downvotes: this.state.downvotes - 1,
this.state.score += 2; upvotes: this.state.upvotes + 1,
score: this.state.score + 2,
});
} else { } else {
this.state.upvotes++; this.setState({
this.state.score++; score: this.state.score + 1,
upvotes: this.state.upvotes + 1,
});
} }
this.state.my_vote = Some(newVote); this.setState({ my_vote: Some(newVote) });
let form = new CreateCommentLike({ let form = new CreateCommentLike({
comment_id: this.props.node.comment_view.comment.id, comment_id: this.props.node.comment_view.comment.id,
@ -1164,7 +1176,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.likeComment(form)); WebSocketService.Instance.send(wsClient.likeComment(form));
this.setState(this.state);
setupTippy(); setupTippy();
} }
@ -1174,18 +1185,24 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
let newVote = myVote == -1 ? 0 : -1; let newVote = myVote == -1 ? 0 : -1;
if (myVote == 1) { if (myVote == 1) {
this.state.score -= 2; this.setState({
this.state.upvotes--; downvotes: this.state.downvotes + 1,
this.state.downvotes++; upvotes: this.state.upvotes - 1,
score: this.state.score - 2,
});
} else if (myVote == -1) { } else if (myVote == -1) {
this.state.downvotes--; this.setState({
this.state.score++; downvotes: this.state.downvotes - 1,
score: this.state.score + 1,
});
} else { } else {
this.state.downvotes++; this.setState({
this.state.score--; downvotes: this.state.downvotes + 1,
score: this.state.score - 1,
});
} }
this.state.my_vote = Some(newVote); this.setState({ my_vote: Some(newVote) });
let form = new CreateCommentLike({ let form = new CreateCommentLike({
comment_id: this.props.node.comment_view.comment.id, comment_id: this.props.node.comment_view.comment.id,
@ -1194,18 +1211,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}); });
WebSocketService.Instance.send(wsClient.likeComment(form)); WebSocketService.Instance.send(wsClient.likeComment(form));
this.setState(this.state);
setupTippy(); setupTippy();
} }
handleShowReportDialog(i: CommentNode) { handleShowReportDialog(i: CommentNode) {
i.state.showReportDialog = !i.state.showReportDialog; i.setState({ showReportDialog: !i.state.showReportDialog });
i.setState(i.state);
} }
handleReportReasonChange(i: CommentNode, event: any) { handleReportReasonChange(i: CommentNode, event: any) {
i.state.reportReason = event.target.value; i.setState({ reportReason: event.target.value });
i.setState(i.state);
} }
handleReportSubmit(i: CommentNode) { handleReportSubmit(i: CommentNode) {
@ -1217,24 +1231,22 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}); });
WebSocketService.Instance.send(wsClient.createCommentReport(form)); WebSocketService.Instance.send(wsClient.createCommentReport(form));
i.state.showReportDialog = false; i.setState({ showReportDialog: false });
i.setState(i.state);
} }
handleModRemoveShow(i: CommentNode) { handleModRemoveShow(i: CommentNode) {
i.state.showRemoveDialog = !i.state.showRemoveDialog; i.setState({
i.state.showBanDialog = false; showRemoveDialog: !i.state.showRemoveDialog,
i.setState(i.state); showBanDialog: false,
});
} }
handleModRemoveReasonChange(i: CommentNode, event: any) { handleModRemoveReasonChange(i: CommentNode, event: any) {
i.state.removeReason = Some(event.target.value); i.setState({ removeReason: Some(event.target.value) });
i.setState(i.state);
} }
handleModRemoveDataChange(i: CommentNode, event: any) { handleModRemoveDataChange(i: CommentNode, event: any) {
i.state.removeData = event.target.checked; i.setState({ removeData: event.target.checked });
i.setState(i.state);
} }
handleModRemoveSubmit(i: CommentNode) { handleModRemoveSubmit(i: CommentNode) {
@ -1247,8 +1259,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}); });
WebSocketService.Instance.send(wsClient.removeComment(form)); WebSocketService.Instance.send(wsClient.removeComment(form));
i.state.showRemoveDialog = false; i.setState({ showRemoveDialog: false });
i.setState(i.state);
} }
handleDistinguishClick(i: CommentNode) { handleDistinguishClick(i: CommentNode) {
@ -1258,6 +1269,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
form_id: None, // TODO not sure about this form_id: None, // TODO not sure about this
content: None, content: None,
distinguished: Some(!comment.distinguished), distinguished: Some(!comment.distinguished),
language_id: Some(comment.language_id),
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.editComment(form)); WebSocketService.Instance.send(wsClient.editComment(form));
@ -1293,43 +1305,40 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form)); WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
} }
i.state.readLoading = true; i.setState({ readLoading: true });
i.setState(this.state);
} }
handleModBanFromCommunityShow(i: CommentNode) { handleModBanFromCommunityShow(i: CommentNode) {
i.state.showBanDialog = true; i.setState({
i.state.banType = BanType.Community; showBanDialog: true,
i.state.showRemoveDialog = false; banType: BanType.Community,
i.setState(i.state); showRemoveDialog: false,
});
} }
handleModBanShow(i: CommentNode) { handleModBanShow(i: CommentNode) {
i.state.showBanDialog = true; i.setState({
i.state.banType = BanType.Site; showBanDialog: true,
i.state.showRemoveDialog = false; banType: BanType.Site,
i.setState(i.state); showRemoveDialog: false,
});
} }
handleModBanReasonChange(i: CommentNode, event: any) { handleModBanReasonChange(i: CommentNode, event: any) {
i.state.banReason = Some(event.target.value); i.setState({ banReason: Some(event.target.value) });
i.setState(i.state);
} }
handleModBanExpireDaysChange(i: CommentNode, event: any) { handleModBanExpireDaysChange(i: CommentNode, event: any) {
i.state.banExpireDays = Some(event.target.value); i.setState({ banExpireDays: Some(event.target.value) });
i.setState(i.state);
} }
handleModBanFromCommunitySubmit(i: CommentNode) { handleModBanFromCommunitySubmit(i: CommentNode) {
i.state.banType = BanType.Community; i.setState({ banType: BanType.Community });
i.setState(i.state);
i.handleModBanBothSubmit(i); i.handleModBanBothSubmit(i);
} }
handleModBanSubmit(i: CommentNode) { handleModBanSubmit(i: CommentNode) {
i.state.banType = BanType.Site; i.setState({ banType: BanType.Site });
i.setState(i.state);
i.handleModBanBothSubmit(i); i.handleModBanBothSubmit(i);
} }
@ -1340,7 +1349,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
// If its an unban, restore all their data // If its an unban, restore all their data
let ban = !cv.creator_banned_from_community; let ban = !cv.creator_banned_from_community;
if (ban == false) { if (ban == false) {
i.state.removeData = false; i.setState({ removeData: false });
} }
let form = new BanFromCommunity({ let form = new BanFromCommunity({
person_id: cv.creator.id, person_id: cv.creator.id,
@ -1356,7 +1365,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
// If its an unban, restore all their data // If its an unban, restore all their data
let ban = !cv.creator.banned; let ban = !cv.creator.banned;
if (ban == false) { if (ban == false) {
i.state.removeData = false; i.setState({ removeData: false });
} }
let form = new BanPerson({ let form = new BanPerson({
person_id: cv.creator.id, person_id: cv.creator.id,
@ -1369,27 +1378,27 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
WebSocketService.Instance.send(wsClient.banPerson(form)); WebSocketService.Instance.send(wsClient.banPerson(form));
} }
i.state.showBanDialog = false; i.setState({ showBanDialog: false });
i.setState(i.state);
} }
handlePurgePersonShow(i: CommentNode) { handlePurgePersonShow(i: CommentNode) {
i.state.showPurgeDialog = true; i.setState({
i.state.purgeType = PurgeType.Person; showPurgeDialog: true,
i.state.showRemoveDialog = false; purgeType: PurgeType.Person,
i.setState(i.state); showRemoveDialog: false,
});
} }
handlePurgeCommentShow(i: CommentNode) { handlePurgeCommentShow(i: CommentNode) {
i.state.showPurgeDialog = true; i.setState({
i.state.purgeType = PurgeType.Comment; showPurgeDialog: true,
i.state.showRemoveDialog = false; purgeType: PurgeType.Comment,
i.setState(i.state); showRemoveDialog: false,
});
} }
handlePurgeReasonChange(i: CommentNode, event: any) { handlePurgeReasonChange(i: CommentNode, event: any) {
i.state.purgeReason = Some(event.target.value); i.setState({ purgeReason: Some(event.target.value) });
i.setState(i.state);
} }
handlePurgeSubmit(i: CommentNode, event: any) { handlePurgeSubmit(i: CommentNode, event: any) {
@ -1411,18 +1420,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
WebSocketService.Instance.send(wsClient.purgeComment(form)); WebSocketService.Instance.send(wsClient.purgeComment(form));
} }
i.state.purgeLoading = true; i.setState({ purgeLoading: true });
i.setState(i.state);
} }
handleShowConfirmAppointAsMod(i: CommentNode) { handleShowConfirmAppointAsMod(i: CommentNode) {
i.state.showConfirmAppointAsMod = true; i.setState({ showConfirmAppointAsMod: true });
i.setState(i.state);
} }
handleCancelConfirmAppointAsMod(i: CommentNode) { handleCancelConfirmAppointAsMod(i: CommentNode) {
i.state.showConfirmAppointAsMod = false; i.setState({ showConfirmAppointAsMod: false });
i.setState(i.state);
} }
handleAddModToCommunity(i: CommentNode) { handleAddModToCommunity(i: CommentNode) {
@ -1434,18 +1440,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.addModToCommunity(form)); WebSocketService.Instance.send(wsClient.addModToCommunity(form));
i.state.showConfirmAppointAsMod = false; i.setState({ showConfirmAppointAsMod: false });
i.setState(i.state);
} }
handleShowConfirmAppointAsAdmin(i: CommentNode) { handleShowConfirmAppointAsAdmin(i: CommentNode) {
i.state.showConfirmAppointAsAdmin = true; i.setState({ showConfirmAppointAsAdmin: true });
i.setState(i.state);
} }
handleCancelConfirmAppointAsAdmin(i: CommentNode) { handleCancelConfirmAppointAsAdmin(i: CommentNode) {
i.state.showConfirmAppointAsAdmin = false; i.setState({ showConfirmAppointAsAdmin: false });
i.setState(i.state);
} }
handleAddAdmin(i: CommentNode) { handleAddAdmin(i: CommentNode) {
@ -1456,18 +1459,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.addAdmin(form)); WebSocketService.Instance.send(wsClient.addAdmin(form));
i.state.showConfirmAppointAsAdmin = false; i.setState({ showConfirmAppointAsAdmin: false });
i.setState(i.state);
} }
handleShowConfirmTransferCommunity(i: CommentNode) { handleShowConfirmTransferCommunity(i: CommentNode) {
i.state.showConfirmTransferCommunity = true; i.setState({ showConfirmTransferCommunity: true });
i.setState(i.state);
} }
handleCancelShowConfirmTransferCommunity(i: CommentNode) { handleCancelShowConfirmTransferCommunity(i: CommentNode) {
i.state.showConfirmTransferCommunity = false; i.setState({ showConfirmTransferCommunity: false });
i.setState(i.state);
} }
handleTransferCommunity(i: CommentNode) { handleTransferCommunity(i: CommentNode) {
@ -1478,18 +1478,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.transferCommunity(form)); WebSocketService.Instance.send(wsClient.transferCommunity(form));
i.state.showConfirmTransferCommunity = false; i.setState({ showConfirmTransferCommunity: false });
i.setState(i.state);
} }
handleShowConfirmTransferSite(i: CommentNode) { handleShowConfirmTransferSite(i: CommentNode) {
i.state.showConfirmTransferSite = true; i.setState({ showConfirmTransferSite: true });
i.setState(i.state);
} }
handleCancelShowConfirmTransferSite(i: CommentNode) { handleCancelShowConfirmTransferSite(i: CommentNode) {
i.state.showConfirmTransferSite = false; i.setState({ showConfirmTransferSite: false });
i.setState(i.state);
} }
get isCommentNew(): boolean { get isCommentNew(): boolean {
@ -1499,19 +1496,16 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
handleCommentCollapse(i: CommentNode) { handleCommentCollapse(i: CommentNode) {
i.state.collapsed = !i.state.collapsed; i.setState({ collapsed: !i.state.collapsed });
i.setState(i.state);
setupTippy(); setupTippy();
} }
handleViewSource(i: CommentNode) { handleViewSource(i: CommentNode) {
i.state.viewSource = !i.state.viewSource; i.setState({ viewSource: !i.state.viewSource });
i.setState(i.state);
} }
handleShowAdvanced(i: CommentNode) { handleShowAdvanced(i: CommentNode) {
i.state.showAdvanced = !i.state.showAdvanced; i.setState({ showAdvanced: !i.state.showAdvanced });
i.setState(i.state);
setupTippy(); setupTippy();
} }

View file

@ -3,6 +3,7 @@ import { Component } from "inferno";
import { import {
CommentNode as CommentNodeI, CommentNode as CommentNodeI,
CommunityModeratorView, CommunityModeratorView,
Language,
PersonViewSafe, PersonViewSafe,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { CommentViewType } from "../../interfaces"; import { CommentViewType } from "../../interfaces";
@ -22,6 +23,7 @@ interface CommentNodesProps {
showCommunity?: boolean; showCommunity?: boolean;
enableDownvotes?: boolean; enableDownvotes?: boolean;
viewType: CommentViewType; viewType: CommentViewType;
allLanguages: Language[];
} }
export class CommentNodes extends Component<CommentNodesProps, any> { export class CommentNodes extends Component<CommentNodesProps, any> {
@ -51,6 +53,7 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
showCommunity={this.props.showCommunity} showCommunity={this.props.showCommunity}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
viewType={this.props.viewType} viewType={this.props.viewType}
allLanguages={this.props.allLanguages}
/> />
))} ))}
</div> </div>

View file

@ -64,6 +64,7 @@ export class CommentReport extends Component<CommentReportProps, any> {
enableDownvotes={true} enableDownvotes={true}
viewOnly={true} viewOnly={true}
showCommunity={true} showCommunity={true}
allLanguages={[]}
/> />
<div> <div>
{i18n.t("reporter")}: <PersonListing person={r.creator} /> {i18n.t("reporter")}: <PersonListing person={r.creator} />

View file

@ -14,7 +14,7 @@ export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
render() { render() {
return ( return (
<div class="position-relative mb-2"> <div className="position-relative mb-2">
{this.props.banner.match({ {this.props.banner.match({
some: banner => <PictrsImage src={banner} banner alt="" />, some: banner => <PictrsImage src={banner} banner alt="" />,
none: <></>, none: <></>,

View file

@ -41,7 +41,7 @@ export class CommentSortSelect extends Component<
name={this.id} name={this.id}
value={this.state.sort} value={this.state.sort}
onChange={linkEvent(this, this.handleSortChange)} onChange={linkEvent(this, this.handleSortChange)}
class="custom-select w-auto mr-2 mb-2" className="custom-select w-auto mr-2 mb-2"
aria-label={i18n.t("sort_type")} aria-label={i18n.t("sort_type")}
> >
<option disabled aria-hidden="true"> <option disabled aria-hidden="true">

View file

@ -32,7 +32,7 @@ export class DataTypeSelect extends Component<
render() { render() {
return ( return (
<div class="btn-group btn-group-toggle flex-wrap mb-2"> <div className="btn-group btn-group-toggle flex-wrap mb-2">
<label <label
className={`pointer btn btn-outline-secondary className={`pointer btn btn-outline-secondary
${this.state.type_ == DataType.Post && "active"} ${this.state.type_ == DataType.Post && "active"}

View file

@ -19,10 +19,10 @@ export class HtmlTags extends Component<HtmlTagsProps, any> {
return ( return (
<Helmet title={this.props.title}> <Helmet title={this.props.title}>
{["title", "og:title", "twitter:title"].map(t => ( {["title", "og:title", "twitter:title"].map(t => (
<meta property={t} content={this.props.title} /> <meta key={t} property={t} content={this.props.title} />
))} ))}
{["og:url", "twitter:url"].map(u => ( {["og:url", "twitter:url"].map(u => (
<meta property={u} content={url} /> <meta key={u} property={u} content={url} />
))} ))}
{/* Open Graph / Facebook */} {/* Open Graph / Facebook */}
@ -35,6 +35,7 @@ export class HtmlTags extends Component<HtmlTagsProps, any> {
{this.props.description.isSome() && {this.props.description.isSome() &&
["description", "og:description", "twitter:description"].map(n => ( ["description", "og:description", "twitter:description"].map(n => (
<meta <meta
key={n}
name={n} name={n}
content={md.renderInline(this.props.description.unwrap())} content={md.renderInline(this.props.description.unwrap())}
/> />
@ -42,7 +43,7 @@ export class HtmlTags extends Component<HtmlTagsProps, any> {
{this.props.image.isSome() && {this.props.image.isSome() &&
["og:image", "twitter:image"].map(p => ( ["og:image", "twitter:image"].map(p => (
<meta property={p} content={this.props.image.unwrap()} /> <meta key={p} property={p} content={this.props.image.unwrap()} />
))} ))}
</Helmet> </Helmet>
); );

View file

@ -17,13 +17,13 @@ export class Icon extends Component<IconProps, any> {
render() { render() {
return ( return (
<svg <svg
class={classNames("icon", this.props.classes, { className={classNames("icon", this.props.classes, {
"icon-inline": this.props.inline, "icon-inline": this.props.inline,
small: this.props.small, small: this.props.small,
})} })}
> >
<use xlinkHref={`#icon-${this.props.icon}`}></use> <use xlinkHref={`#icon-${this.props.icon}`}></use>
<div class="sr-only"> <div className="sr-only">
<title>{this.props.icon}</title> <title>{this.props.icon}</title>
</div> </div>
</svg> </svg>
@ -57,7 +57,7 @@ export class PurgeWarning extends Component<any, any> {
render() { render() {
return ( return (
<div class="mt-2 alert alert-danger" role="alert"> <div className="mt-2 alert alert-danger" role="alert">
<Icon icon="alert-triangle" classes="icon-inline mr-2" /> <Icon icon="alert-triangle" classes="icon-inline mr-2" />
{i18n.t("purge_warning")} {i18n.t("purge_warning")}
</div> </div>

View file

@ -34,14 +34,14 @@ export class ImageUploadForm extends Component<
render() { render() {
return ( return (
<form class="d-inline"> <form className="d-inline">
<label <label
htmlFor={this.id} htmlFor={this.id}
class="pointer text-muted small font-weight-bold" className="pointer text-muted small font-weight-bold"
> >
{this.props.imageSrc.match({ {this.props.imageSrc.match({
some: imageSrc => ( some: imageSrc => (
<span class="d-inline-block position-relative"> <span className="d-inline-block position-relative">
<img <img
src={imageSrc} src={imageSrc}
height={this.props.rounded ? 60 : ""} height={this.props.rounded ? 60 : ""}
@ -59,7 +59,9 @@ export class ImageUploadForm extends Component<
</span> </span>
), ),
none: ( none: (
<span class="btn btn-secondary">{this.props.uploadTitle}</span> <span className="btn btn-secondary">
{this.props.uploadTitle}
</span>
), ),
})} })}
</label> </label>
@ -68,7 +70,7 @@ export class ImageUploadForm extends Component<
type="file" type="file"
accept="image/*,video/*" accept="image/*,video/*"
name={this.id} name={this.id}
class="d-none" className="d-none"
disabled={UserService.Instance.myUserInfo.isNone()} disabled={UserService.Instance.myUserInfo.isNone()}
onChange={linkEvent(this, this.handleImageUpload)} onChange={linkEvent(this, this.handleImageUpload)}
/> />
@ -82,8 +84,7 @@ export class ImageUploadForm extends Component<
const formData = new FormData(); const formData = new FormData();
formData.append("images[]", file); formData.append("images[]", file);
i.state.loading = true; i.setState({ loading: true });
i.setState(i.state);
fetch(pictrsUri, { fetch(pictrsUri, {
method: "POST", method: "POST",
@ -96,18 +97,15 @@ export class ImageUploadForm extends Component<
if (res.msg == "ok") { if (res.msg == "ok") {
let hash = res.files[0].file; let hash = res.files[0].file;
let url = `${pictrsUri}/${hash}`; let url = `${pictrsUri}/${hash}`;
i.state.loading = false; i.setState({ loading: false });
i.setState(i.state);
i.props.onUpload(url); i.props.onUpload(url);
} else { } else {
i.state.loading = false; i.setState({ loading: false });
i.setState(i.state);
toast(JSON.stringify(res), "danger"); toast(JSON.stringify(res), "danger");
} }
}) })
.catch(error => { .catch(error => {
i.state.loading = false; i.setState({ loading: false });
i.setState(i.state);
console.error(error); console.error(error);
toast(error, "danger"); toast(error, "danger");
}); });
@ -115,8 +113,7 @@ export class ImageUploadForm extends Component<
handleRemoveImage(i: ImageUploadForm, event: any) { handleRemoveImage(i: ImageUploadForm, event: any) {
event.preventDefault(); event.preventDefault();
i.state.loading = true; i.setState({ loading: true });
i.setState(i.state);
i.props.onRemove(); i.props.onRemove();
} }
} }

View file

@ -0,0 +1,109 @@
import { Option } from "@sniptt/monads";
import classNames from "classnames";
import { Component, linkEvent } from "inferno";
import { Language } from "lemmy-js-client";
import { i18n } from "../../i18next";
import { randomStr } from "../../utils";
import { Icon } from "./icon";
interface LanguageSelectProps {
allLanguages: Language[];
selectedLanguageIds: Option<number[]>;
multiple: boolean;
onChange(val: number[]): any;
}
export class LanguageSelect extends Component<LanguageSelectProps, any> {
private id = `language-select-${randomStr()}`;
constructor(props: any, context: any) {
super(props, context);
}
componentDidMount() {
this.setSelectedValues();
}
// Necessary because there is no HTML way to set selected for multiple in value=
setSelectedValues() {
this.props.selectedLanguageIds.map(toString).match({
some: ids => {
var select = (document.getElementById(this.id) as HTMLSelectElement)
.options;
for (let i = 0; i < select.length; i++) {
let o = select[i];
if (ids.includes(o.value)) {
o.selected = true;
}
}
},
none: void 0,
});
}
render() {
let selectedLangs = this.props.selectedLanguageIds;
return (
<div className="form-group row">
<label
className={classNames("col-form-label", {
"col-sm-3": this.props.multiple,
"col-sm-2": !this.props.multiple,
})}
htmlFor={this.id}
>
{i18n.t(this.props.multiple ? "language_plural" : "language")}
</label>
<div
className={classNames("input-group", {
"col-sm-9": this.props.multiple,
"col-sm-10": !this.props.multiple,
})}
>
<select
className="form-control custom-select"
id={this.id}
onChange={linkEvent(this, this.handleLanguageChange)}
aria-label="action"
multiple={this.props.multiple}
>
{this.props.allLanguages.map(l => (
<option
key={l.id}
value={l.id}
selected={selectedLangs.unwrapOr([]).includes(l.id)}
>
{l.name}
</option>
))}
</select>
{this.props.multiple && (
<div className="input-group-append">
<button
className="input-group-text"
onClick={linkEvent(this, this.handleDeselectAll)}
>
<Icon icon="x" />
</button>
</div>
)}
</div>
</div>
);
}
handleLanguageChange(i: LanguageSelect, event: any) {
let options: HTMLOptionElement[] = Array.from(event.target.options);
let selected: number[] = options
.filter(o => o.selected)
.map(o => Number(o.value));
i.props.onChange(selected);
}
handleDeselectAll(i: LanguageSelect, event: any) {
event.preventDefault();
i.props.onChange([]);
}
}

View file

@ -40,7 +40,7 @@ export class ListingTypeSelect extends Component<
render() { render() {
return ( return (
<div class="btn-group btn-group-toggle flex-wrap mb-2"> <div className="btn-group btn-group-toggle flex-wrap mb-2">
{this.props.showSubscribed && ( {this.props.showSubscribed && (
<label <label
title={i18n.t("subscribed_description")} title={i18n.t("subscribed_description")}

View file

@ -2,7 +2,7 @@ import { None, Option, Some } from "@sniptt/monads";
import autosize from "autosize"; import autosize from "autosize";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Prompt } from "inferno-router"; import { Prompt } from "inferno-router";
import { toUndefined } from "lemmy-js-client"; import { Language, toUndefined } from "lemmy-js-client";
import { pictrsUri } from "../../env"; import { pictrsUri } from "../../env";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService } from "../../services"; import { UserService } from "../../services";
@ -18,9 +18,11 @@ import {
toast, toast,
} from "../../utils"; } from "../../utils";
import { Icon, Spinner } from "./icon"; import { Icon, Spinner } from "./icon";
import { LanguageSelect } from "./language-select";
interface MarkdownTextAreaProps { interface MarkdownTextAreaProps {
initialContent: Option<string>; initialContent: Option<string>;
initialLanguageId: Option<number>;
placeholder: Option<string>; placeholder: Option<string>;
buttonTitle: Option<string>; buttonTitle: Option<string>;
maxLength: Option<number>; maxLength: Option<number>;
@ -28,14 +30,21 @@ interface MarkdownTextAreaProps {
focus?: boolean; focus?: boolean;
disabled?: boolean; disabled?: boolean;
finished?: boolean; finished?: boolean;
showLanguage?: boolean;
hideNavigationWarnings?: boolean; hideNavigationWarnings?: boolean;
onContentChange?(val: string): any; onContentChange?(val: string): any;
onReplyCancel?(): any; onReplyCancel?(): any;
onSubmit?(msg: { val: string; formId: string }): any; onSubmit?(msg: {
val: Option<string>;
formId: string;
languageId: Option<number>;
}): any;
allLanguages: Language[];
} }
interface MarkdownTextAreaState { interface MarkdownTextAreaState {
content: Option<string>; content: Option<string>;
languageId: Option<number>;
previewMode: boolean; previewMode: boolean;
loading: boolean; loading: boolean;
imageLoading: boolean; imageLoading: boolean;
@ -50,6 +59,7 @@ export class MarkdownTextArea extends Component<
private tribute: any; private tribute: any;
private emptyState: MarkdownTextAreaState = { private emptyState: MarkdownTextAreaState = {
content: this.props.initialContent, content: this.props.initialContent,
languageId: this.props.initialLanguageId,
previewMode: false, previewMode: false,
loading: false, loading: false,
imageLoading: false, imageLoading: false,
@ -58,6 +68,8 @@ export class MarkdownTextArea extends Component<
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.handleLanguageChange = this.handleLanguageChange.bind(this);
if (isBrowser()) { if (isBrowser()) {
this.tribute = setupTribute(); this.tribute = setupTribute();
} }
@ -70,8 +82,7 @@ export class MarkdownTextArea extends Component<
autosize(textarea); autosize(textarea);
this.tribute.attach(textarea); this.tribute.attach(textarea);
textarea.addEventListener("tribute-replaced", () => { textarea.addEventListener("tribute-replaced", () => {
this.state.content = Some(textarea.value); this.setState({ content: Some(textarea.value) });
this.setState(this.state);
autosize.update(textarea); autosize.update(textarea);
}); });
@ -96,10 +107,7 @@ export class MarkdownTextArea extends Component<
componentWillReceiveProps(nextProps: MarkdownTextAreaProps) { componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
if (nextProps.finished) { if (nextProps.finished) {
this.state.previewMode = false; this.setState({ previewMode: false, loading: false, content: None });
this.state.loading = false;
this.state.content = None;
this.setState(this.state);
if (this.props.replyType) { if (this.props.replyType) {
this.props.onReplyCancel(); this.props.onReplyCancel();
} }
@ -108,7 +116,6 @@ export class MarkdownTextArea extends Component<
let form: any = document.getElementById(this.formId); let form: any = document.getElementById(this.formId);
form.reset(); form.reset();
setTimeout(() => autosize.update(textarea), 10); setTimeout(() => autosize.update(textarea), 10);
this.setState(this.state);
} }
} }
@ -125,7 +132,7 @@ export class MarkdownTextArea extends Component<
} }
message={i18n.t("block_leaving")} message={i18n.t("block_leaving")}
/> />
<div class="form-group row"> <div className="form-group row">
<div className={`col-sm-12`}> <div className={`col-sm-12`}>
<textarea <textarea
id={this.id} id={this.id}
@ -150,17 +157,29 @@ export class MarkdownTextArea extends Component<
none: <></>, none: <></>,
})} })}
</div> </div>
<label class="sr-only" htmlFor={this.id}> <label className="sr-only" htmlFor={this.id}>
{i18n.t("body")} {i18n.t("body")}
</label> </label>
</div> </div>
<div class="row"> {this.props.showLanguage && (
<div class="col-sm-12 d-flex flex-wrap"> <div className="row justify-content-end">
<div className="col-sm-8">
<LanguageSelect
allLanguages={this.props.allLanguages}
selectedLanguageIds={this.state.languageId.map(Array.of)}
multiple={false}
onChange={this.handleLanguageChange}
/>
</div>
</div>
)}
<div className="row">
<div className="col-sm-12 d-flex flex-wrap">
{this.props.buttonTitle.match({ {this.props.buttonTitle.match({
some: buttonTitle => ( some: buttonTitle => (
<button <button
type="submit" type="submit"
class="btn btn-sm btn-secondary mr-2" className="btn btn-sm btn-secondary mr-2"
disabled={this.props.disabled || this.state.loading} disabled={this.props.disabled || this.state.loading}
> >
{this.state.loading ? ( {this.state.loading ? (
@ -175,7 +194,7 @@ export class MarkdownTextArea extends Component<
{this.props.replyType && ( {this.props.replyType && (
<button <button
type="button" type="button"
class="btn btn-sm btn-secondary mr-2" className="btn btn-sm btn-secondary mr-2"
onClick={linkEvent(this, this.handleReplyCancel)} onClick={linkEvent(this, this.handleReplyCancel)}
> >
{i18n.t("cancel")} {i18n.t("cancel")}
@ -192,9 +211,9 @@ export class MarkdownTextArea extends Component<
</button> </button>
)} )}
{/* A flex expander */} {/* A flex expander */}
<div class="flex-grow-1"></div> <div className="flex-grow-1"></div>
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
data-tippy-content={i18n.t("bold")} data-tippy-content={i18n.t("bold")}
aria-label={i18n.t("bold")} aria-label={i18n.t("bold")}
onClick={linkEvent(this, this.handleInsertBold)} onClick={linkEvent(this, this.handleInsertBold)}
@ -202,7 +221,7 @@ export class MarkdownTextArea extends Component<
<Icon icon="bold" classes="icon-inline" /> <Icon icon="bold" classes="icon-inline" />
</button> </button>
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
data-tippy-content={i18n.t("italic")} data-tippy-content={i18n.t("italic")}
aria-label={i18n.t("italic")} aria-label={i18n.t("italic")}
onClick={linkEvent(this, this.handleInsertItalic)} onClick={linkEvent(this, this.handleInsertItalic)}
@ -210,14 +229,14 @@ export class MarkdownTextArea extends Component<
<Icon icon="italic" classes="icon-inline" /> <Icon icon="italic" classes="icon-inline" />
</button> </button>
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
data-tippy-content={i18n.t("link")} data-tippy-content={i18n.t("link")}
aria-label={i18n.t("link")} aria-label={i18n.t("link")}
onClick={linkEvent(this, this.handleInsertLink)} onClick={linkEvent(this, this.handleInsertLink)}
> >
<Icon icon="link" classes="icon-inline" /> <Icon icon="link" classes="icon-inline" />
</button> </button>
<form class="btn btn-sm text-muted font-weight-bold"> <form className="btn btn-sm text-muted font-weight-bold">
<label <label
htmlFor={`file-upload-${this.id}`} htmlFor={`file-upload-${this.id}`}
className={`mb-0 ${ className={`mb-0 ${
@ -236,13 +255,13 @@ export class MarkdownTextArea extends Component<
type="file" type="file"
accept="image/*,video/*" accept="image/*,video/*"
name="file" name="file"
class="d-none" className="d-none"
disabled={UserService.Instance.myUserInfo.isNone()} disabled={UserService.Instance.myUserInfo.isNone()}
onChange={linkEvent(this, this.handleImageUpload)} onChange={linkEvent(this, this.handleImageUpload)}
/> />
</form> </form>
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
data-tippy-content={i18n.t("header")} data-tippy-content={i18n.t("header")}
aria-label={i18n.t("header")} aria-label={i18n.t("header")}
onClick={linkEvent(this, this.handleInsertHeader)} onClick={linkEvent(this, this.handleInsertHeader)}
@ -250,7 +269,7 @@ export class MarkdownTextArea extends Component<
<Icon icon="header" classes="icon-inline" /> <Icon icon="header" classes="icon-inline" />
</button> </button>
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
data-tippy-content={i18n.t("strikethrough")} data-tippy-content={i18n.t("strikethrough")}
aria-label={i18n.t("strikethrough")} aria-label={i18n.t("strikethrough")}
onClick={linkEvent(this, this.handleInsertStrikethrough)} onClick={linkEvent(this, this.handleInsertStrikethrough)}
@ -258,7 +277,7 @@ export class MarkdownTextArea extends Component<
<Icon icon="strikethrough" classes="icon-inline" /> <Icon icon="strikethrough" classes="icon-inline" />
</button> </button>
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
data-tippy-content={i18n.t("quote")} data-tippy-content={i18n.t("quote")}
aria-label={i18n.t("quote")} aria-label={i18n.t("quote")}
onClick={linkEvent(this, this.handleInsertQuote)} onClick={linkEvent(this, this.handleInsertQuote)}
@ -266,7 +285,7 @@ export class MarkdownTextArea extends Component<
<Icon icon="format_quote" classes="icon-inline" /> <Icon icon="format_quote" classes="icon-inline" />
</button> </button>
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
data-tippy-content={i18n.t("list")} data-tippy-content={i18n.t("list")}
aria-label={i18n.t("list")} aria-label={i18n.t("list")}
onClick={linkEvent(this, this.handleInsertList)} onClick={linkEvent(this, this.handleInsertList)}
@ -274,7 +293,7 @@ export class MarkdownTextArea extends Component<
<Icon icon="list" classes="icon-inline" /> <Icon icon="list" classes="icon-inline" />
</button> </button>
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
data-tippy-content={i18n.t("code")} data-tippy-content={i18n.t("code")}
aria-label={i18n.t("code")} aria-label={i18n.t("code")}
onClick={linkEvent(this, this.handleInsertCode)} onClick={linkEvent(this, this.handleInsertCode)}
@ -282,7 +301,7 @@ export class MarkdownTextArea extends Component<
<Icon icon="code" classes="icon-inline" /> <Icon icon="code" classes="icon-inline" />
</button> </button>
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
data-tippy-content={i18n.t("subscript")} data-tippy-content={i18n.t("subscript")}
aria-label={i18n.t("subscript")} aria-label={i18n.t("subscript")}
onClick={linkEvent(this, this.handleInsertSubscript)} onClick={linkEvent(this, this.handleInsertSubscript)}
@ -290,7 +309,7 @@ export class MarkdownTextArea extends Component<
<Icon icon="subscript" classes="icon-inline" /> <Icon icon="subscript" classes="icon-inline" />
</button> </button>
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
data-tippy-content={i18n.t("superscript")} data-tippy-content={i18n.t("superscript")}
aria-label={i18n.t("superscript")} aria-label={i18n.t("superscript")}
onClick={linkEvent(this, this.handleInsertSuperscript)} onClick={linkEvent(this, this.handleInsertSuperscript)}
@ -298,7 +317,7 @@ export class MarkdownTextArea extends Component<
<Icon icon="superscript" classes="icon-inline" /> <Icon icon="superscript" classes="icon-inline" />
</button> </button>
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
data-tippy-content={i18n.t("spoiler")} data-tippy-content={i18n.t("spoiler")}
aria-label={i18n.t("spoiler")} aria-label={i18n.t("spoiler")}
onClick={linkEvent(this, this.handleInsertSpoiler)} onClick={linkEvent(this, this.handleInsertSpoiler)}
@ -307,7 +326,7 @@ export class MarkdownTextArea extends Component<
</button> </button>
<a <a
href={markdownHelpUrl} href={markdownHelpUrl}
class="btn btn-sm text-muted font-weight-bold" className="btn btn-sm text-muted font-weight-bold"
title={i18n.t("formatting_help")} title={i18n.t("formatting_help")}
rel={relTags} rel={relTags}
> >
@ -338,8 +357,7 @@ export class MarkdownTextArea extends Component<
const formData = new FormData(); const formData = new FormData();
formData.append("images[]", file); formData.append("images[]", file);
i.state.imageLoading = true; i.setState({ imageLoading: true });
i.setState(i.state);
fetch(pictrsUri, { fetch(pictrsUri, {
method: "POST", method: "POST",
@ -355,15 +373,16 @@ export class MarkdownTextArea extends Component<
let deleteToken = res.files[0].delete_token; let deleteToken = res.files[0].delete_token;
let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`; let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`;
let imageMarkdown = `![](${url})`; let imageMarkdown = `![](${url})`;
i.state.content = Some( i.setState({
i.state.content.match({ content: Some(
some: content => `${content}\n${imageMarkdown}`, i.state.content.match({
none: imageMarkdown, some: content => `${content}\n${imageMarkdown}`,
}) none: imageMarkdown,
); })
i.state.imageLoading = false; ),
imageLoading: false,
});
i.contentChange(); i.contentChange();
i.setState(i.state);
let textarea: any = document.getElementById(i.id); let textarea: any = document.getElementById(i.id);
autosize.update(textarea); autosize.update(textarea);
pictrsDeleteToast( pictrsDeleteToast(
@ -373,14 +392,12 @@ export class MarkdownTextArea extends Component<
deleteUrl deleteUrl
); );
} else { } else {
i.state.imageLoading = false; i.setState({ imageLoading: false });
i.setState(i.state);
toast(JSON.stringify(res), "danger"); toast(JSON.stringify(res), "danger");
} }
}) })
.catch(error => { .catch(error => {
i.state.imageLoading = false; i.setState({ imageLoading: false });
i.setState(i.state);
console.error(error); console.error(error);
toast(error, "danger"); toast(error, "danger");
}); });
@ -393,22 +410,27 @@ export class MarkdownTextArea extends Component<
} }
handleContentChange(i: MarkdownTextArea, event: any) { handleContentChange(i: MarkdownTextArea, event: any) {
i.state.content = Some(event.target.value); i.setState({ content: Some(event.target.value) });
i.contentChange(); i.contentChange();
i.setState(i.state);
} }
handlePreviewToggle(i: MarkdownTextArea, event: any) { handlePreviewToggle(i: MarkdownTextArea, event: any) {
event.preventDefault(); event.preventDefault();
i.state.previewMode = !i.state.previewMode; i.setState({ previewMode: !i.state.previewMode });
i.setState(i.state); }
handleLanguageChange(val: number[]) {
this.setState({ languageId: Some(val[0]) });
} }
handleSubmit(i: MarkdownTextArea, event: any) { handleSubmit(i: MarkdownTextArea, event: any) {
event.preventDefault(); event.preventDefault();
i.state.loading = true; i.setState({ loading: true });
i.setState(i.state); let msg = {
let msg = { val: toUndefined(i.state.content), formId: i.formId }; val: i.state.content,
formId: i.formId,
languageId: i.state.languageId,
};
i.props.onSubmit(msg); i.props.onSubmit(msg);
} }
@ -424,27 +446,28 @@ export class MarkdownTextArea extends Component<
let end: number = textarea.selectionEnd; let end: number = textarea.selectionEnd;
if (i.state.content.isNone()) { if (i.state.content.isNone()) {
i.state.content = Some(""); i.setState({ content: Some("") });
} }
let content = i.state.content.unwrap(); let content = i.state.content.unwrap();
if (start !== end) { if (start !== end) {
let selectedText = content.substring(start, end); let selectedText = content.substring(start, end);
i.state.content = Some( i.setState({
`${content.substring(0, start)}[${selectedText}]()${content.substring( content: Some(
end `${content.substring(0, start)}[${selectedText}]()${content.substring(
)}` end
); )}`
),
});
textarea.focus(); textarea.focus();
setTimeout(() => (textarea.selectionEnd = end + 3), 10); setTimeout(() => (textarea.selectionEnd = end + 3), 10);
} else { } else {
i.state.content = Some(`${content} []()`); i.setState({ content: Some(`${content} []()`) });
textarea.focus(); textarea.focus();
setTimeout(() => (textarea.selectionEnd -= 1), 10); setTimeout(() => (textarea.selectionEnd -= 1), 10);
} }
i.contentChange(); i.contentChange();
i.setState(i.state);
} }
simpleSurround(chars: string) { simpleSurround(chars: string) {
@ -461,7 +484,7 @@ export class MarkdownTextArea extends Component<
emptyChars = "___" emptyChars = "___"
) { ) {
if (this.state.content.isNone()) { if (this.state.content.isNone()) {
this.state.content = Some(""); this.setState({ content: Some("") });
} }
let textarea: any = document.getElementById(this.id); let textarea: any = document.getElementById(this.id);
let start: number = textarea.selectionStart; let start: number = textarea.selectionStart;
@ -471,19 +494,20 @@ export class MarkdownTextArea extends Component<
if (start !== end) { if (start !== end) {
let selectedText = content.substring(start, end); let selectedText = content.substring(start, end);
this.state.content = Some( this.setState({
`${content.substring( content: Some(
0, `${content.substring(
start 0,
)}${beforeChars}${selectedText}${afterChars}${content.substring(end)}` start
); )}${beforeChars}${selectedText}${afterChars}${content.substring(end)}`
),
});
} else { } else {
this.state.content = Some( this.setState({
`${content}${beforeChars}${emptyChars}${afterChars}` content: Some(`${content}${beforeChars}${emptyChars}${afterChars}`),
); });
} }
this.contentChange(); this.contentChange();
this.setState(this.state);
textarea.focus(); textarea.focus();
@ -555,9 +579,11 @@ export class MarkdownTextArea extends Component<
simpleInsert(chars: string) { simpleInsert(chars: string) {
if (this.state.content.isNone()) { if (this.state.content.isNone()) {
this.state.content = Some(`${chars} `); this.setState({ content: Some(`${chars} `) });
} else { } else {
this.state.content = Some(`${this.state.content.unwrap()}\n${chars} `); this.setState({
content: Some(`${this.state.content.unwrap()}\n${chars} `),
});
} }
let textarea: any = document.getElementById(this.id); let textarea: any = document.getElementById(this.id);
@ -566,7 +592,6 @@ export class MarkdownTextArea extends Component<
autosize.update(textarea); autosize.update(textarea);
}, 10); }, 10);
this.contentChange(); this.contentChange();
this.setState(this.state);
} }
handleInsertSpoiler(i: MarkdownTextArea, event: any) { handleInsertSpoiler(i: MarkdownTextArea, event: any) {
@ -586,13 +611,14 @@ export class MarkdownTextArea extends Component<
.map(t => `> ${t}`) .map(t => `> ${t}`)
.join("\n") + "\n\n"; .join("\n") + "\n\n";
if (this.state.content.isNone()) { if (this.state.content.isNone()) {
this.state.content = Some(""); this.setState({ content: Some("") });
} else { } else {
this.state.content = Some(`${this.state.content.unwrap()}\n`); this.setState({ content: Some(`${this.state.content.unwrap()}\n`) });
} }
this.state.content = Some(`${this.state.content.unwrap()}${quotedText}`); this.setState({
content: Some(`${this.state.content.unwrap()}${quotedText}`),
});
this.contentChange(); this.contentChange();
this.setState(this.state);
// Not sure why this needs a delay // Not sure why this needs a delay
setTimeout(() => autosize.update(textarea), 10); setTimeout(() => autosize.update(textarea), 10);
} }

View file

@ -12,16 +12,16 @@ export class Paginator extends Component<PaginatorProps, any> {
} }
render() { render() {
return ( return (
<div class="my-2"> <div className="my-2">
<button <button
class="btn btn-secondary mr-2" className="btn btn-secondary mr-2"
disabled={this.props.page == 1} disabled={this.props.page == 1}
onClick={linkEvent(this, this.handlePrev)} onClick={linkEvent(this, this.handlePrev)}
> >
{i18n.t("prev")} {i18n.t("prev")}
</button> </button>
<button <button
class="btn btn-secondary" className="btn btn-secondary"
onClick={linkEvent(this, this.handleNext)} onClick={linkEvent(this, this.handleNext)}
> >
{i18n.t("next")} {i18n.t("next")}

View file

@ -88,18 +88,20 @@ export class RegistrationApplication extends Component<
})} })}
{this.state.denyExpanded && ( {this.state.denyExpanded && (
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label"> <label className="col-sm-2 col-form-label">
{i18n.t("deny_reason")} {i18n.t("deny_reason")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.denyReason} initialContent={this.state.denyReason}
initialLanguageId={None}
onContentChange={this.handleDenyReasonChange} onContentChange={this.handleDenyReasonChange}
placeholder={None} placeholder={None}
buttonTitle={None} buttonTitle={None}
maxLength={None} maxLength={None}
hideNavigationWarnings hideNavigationWarnings
allLanguages={[]}
/> />
</div> </div>
</div> </div>
@ -157,7 +159,6 @@ export class RegistrationApplication extends Component<
} }
handleDenyReasonChange(val: string) { handleDenyReasonChange(val: string) {
this.state.denyReason = Some(val); this.setState({ denyReason: Some(val) });
this.setState(this.state);
} }
} }

View file

@ -40,23 +40,27 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
name={this.id} name={this.id}
value={this.state.sort} value={this.state.sort}
onChange={linkEvent(this, this.handleSortChange)} onChange={linkEvent(this, this.handleSortChange)}
class="custom-select w-auto mr-2 mb-2" className="custom-select w-auto mr-2 mb-2"
aria-label={i18n.t("sort_type")} aria-label={i18n.t("sort_type")}
> >
<option disabled aria-hidden="true"> <option disabled aria-hidden="true">
{i18n.t("sort_type")} {i18n.t("sort_type")}
</option> </option>
{!this.props.hideHot && [ {!this.props.hideHot && [
<option value={SortType.Hot}>{i18n.t("hot")}</option>, <option key={SortType.Hot} value={SortType.Hot}>
<option value={SortType.Active}>{i18n.t("active")}</option>, {i18n.t("hot")}
</option>,
<option key={SortType.Active} value={SortType.Active}>
{i18n.t("active")}
</option>,
]} ]}
<option value={SortType.New}>{i18n.t("new")}</option> <option value={SortType.New}>{i18n.t("new")}</option>
<option value={SortType.Old}>{i18n.t("old")}</option> <option value={SortType.Old}>{i18n.t("old")}</option>
{!this.props.hideMostComments && [ {!this.props.hideMostComments && [
<option value={SortType.MostComments}> <option key={SortType.MostComments} value={SortType.MostComments}>
{i18n.t("most_comments")} {i18n.t("most_comments")}
</option>, </option>,
<option value={SortType.NewComments}> <option key={SortType.NewComments} value={SortType.NewComments}>
{i18n.t("new_comments")} {i18n.t("new_comments")}
</option>, </option>,
]} ]}

View file

@ -17,7 +17,7 @@ export const SYMBOLS = (
viewBox="0 0 196.52 196.52" viewBox="0 0 196.52 196.52"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<g color="#000" font-weight="400" font-family="sans-serif"> <g color="#000" fontWeight="400" fontFamily="sans-serif">
<path <path
d="M47.924 72.797a18.228 18.228 0 0 1-7.796 7.76l42.799 42.965 10.318-5.23zm56.453 56.67l-10.319 5.23 21.686 21.77a18.228 18.228 0 0 1 7.798-7.76z" d="M47.924 72.797a18.228 18.228 0 0 1-7.796 7.76l42.799 42.965 10.318-5.23zm56.453 56.67l-10.319 5.23 21.686 21.77a18.228 18.228 0 0 1 7.798-7.76z"
overflow="visible" overflow="visible"
@ -69,7 +69,7 @@ export const SYMBOLS = (
fill="#dbb210" fill="#dbb210"
/> />
</g> </g>
<g transform="rotate(3.118 600.365 106.46)" fill-opacity=".996"> <g transform="rotate(3.118 600.365 106.46)" fillOpacity=".996">
<circle cx="106.266" cy="51.536" r="16.571" fill="#ffca00" /> <circle cx="106.266" cy="51.536" r="16.571" fill="#ffca00" />
<circle cx="171.428" cy="110.193" r="16.571" fill="#64ff00" /> <circle cx="171.428" cy="110.193" r="16.571" fill="#64ff00" />
<circle cx="135.764" cy="190.277" r="16.571" fill="#00a3ff" /> <circle cx="135.764" cy="190.277" r="16.571" fill="#00a3ff" />

View file

@ -75,8 +75,11 @@ export class Communities extends Component<any, CommunitiesState> {
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
let listRes = Some(this.isoData.routeData[0] as ListCommunitiesResponse); let listRes = Some(this.isoData.routeData[0] as ListCommunitiesResponse);
this.state.listCommunitiesResponse = listRes; this.state = {
this.state.loading = false; ...this.state,
listCommunitiesResponse: listRes,
loading: false,
};
} else { } else {
this.refetch(); this.refetch();
} }
@ -114,7 +117,7 @@ export class Communities extends Component<any, CommunitiesState> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
@ -127,10 +130,10 @@ export class Communities extends Component<any, CommunitiesState> {
</h5> </h5>
) : ( ) : (
<div> <div>
<div class="row"> <div className="row">
<div class="col-md-6"> <div className="col-md-6">
<h4>{i18n.t("list_of_communities")}</h4> <h4>{i18n.t("list_of_communities")}</h4>
<span class="mb-2"> <span className="mb-2">
<ListingTypeSelect <ListingTypeSelect
type_={this.state.listingType} type_={this.state.listingType}
showLocal={showLocal(this.isoData)} showLocal={showLocal(this.isoData)}
@ -139,24 +142,27 @@ export class Communities extends Component<any, CommunitiesState> {
/> />
</span> </span>
</div> </div>
<div class="col-md-6"> <div className="col-md-6">
<div class="float-md-right">{this.searchForm()}</div> <div className="float-md-right">{this.searchForm()}</div>
</div> </div>
</div> </div>
<div class="table-responsive"> <div className="table-responsive">
<table id="community_table" class="table table-sm table-hover"> <table
<thead class="pointer"> id="community_table"
className="table table-sm table-hover"
>
<thead className="pointer">
<tr> <tr>
<th>{i18n.t("name")}</th> <th>{i18n.t("name")}</th>
<th class="text-right">{i18n.t("subscribers")}</th> <th className="text-right">{i18n.t("subscribers")}</th>
<th class="text-right"> <th className="text-right">
{i18n.t("users")} / {i18n.t("month")} {i18n.t("users")} / {i18n.t("month")}
</th> </th>
<th class="text-right d-none d-lg-table-cell"> <th className="text-right d-none d-lg-table-cell">
{i18n.t("posts")} {i18n.t("posts")}
</th> </th>
<th class="text-right d-none d-lg-table-cell"> <th className="text-right d-none d-lg-table-cell">
{i18n.t("comments")} {i18n.t("comments")}
</th> </th>
<th></th> <th></th>
@ -167,26 +173,26 @@ export class Communities extends Component<any, CommunitiesState> {
.map(l => l.communities) .map(l => l.communities)
.unwrapOr([]) .unwrapOr([])
.map(cv => ( .map(cv => (
<tr> <tr key={cv.community.id}>
<td> <td>
<CommunityLink community={cv.community} /> <CommunityLink community={cv.community} />
</td> </td>
<td class="text-right"> <td className="text-right">
{numToSI(cv.counts.subscribers)} {numToSI(cv.counts.subscribers)}
</td> </td>
<td class="text-right"> <td className="text-right">
{numToSI(cv.counts.users_active_month)} {numToSI(cv.counts.users_active_month)}
</td> </td>
<td class="text-right d-none d-lg-table-cell"> <td className="text-right d-none d-lg-table-cell">
{numToSI(cv.counts.posts)} {numToSI(cv.counts.posts)}
</td> </td>
<td class="text-right d-none d-lg-table-cell"> <td className="text-right d-none d-lg-table-cell">
{numToSI(cv.counts.comments)} {numToSI(cv.counts.comments)}
</td> </td>
<td class="text-right"> <td className="text-right">
{cv.subscribed == SubscribedType.Subscribed && ( {cv.subscribed == SubscribedType.Subscribed && (
<button <button
class="btn btn-link d-inline-block" className="btn btn-link d-inline-block"
onClick={linkEvent( onClick={linkEvent(
cv.community.id, cv.community.id,
this.handleUnsubscribe this.handleUnsubscribe
@ -197,7 +203,7 @@ export class Communities extends Component<any, CommunitiesState> {
)} )}
{cv.subscribed == SubscribedType.NotSubscribed && ( {cv.subscribed == SubscribedType.NotSubscribed && (
<button <button
class="btn btn-link d-inline-block" className="btn btn-link d-inline-block"
onClick={linkEvent( onClick={linkEvent(
cv.community.id, cv.community.id,
this.handleSubscribe this.handleSubscribe
@ -207,7 +213,7 @@ export class Communities extends Component<any, CommunitiesState> {
</button> </button>
)} )}
{cv.subscribed == SubscribedType.Pending && ( {cv.subscribed == SubscribedType.Pending && (
<div class="text-warning d-inline-block"> <div className="text-warning d-inline-block">
{i18n.t("subscribe_pending")} {i18n.t("subscribe_pending")}
</div> </div>
)} )}
@ -230,23 +236,23 @@ export class Communities extends Component<any, CommunitiesState> {
searchForm() { searchForm() {
return ( return (
<form <form
class="form-inline" className="form-inline"
onSubmit={linkEvent(this, this.handleSearchSubmit)} onSubmit={linkEvent(this, this.handleSearchSubmit)}
> >
<input <input
type="text" type="text"
id="communities-search" id="communities-search"
class="form-control mr-2 mb-2" className="form-control mr-2 mb-2"
value={this.state.searchText} value={this.state.searchText}
placeholder={`${i18n.t("search")}...`} placeholder={`${i18n.t("search")}...`}
onInput={linkEvent(this, this.handleSearchChange)} onInput={linkEvent(this, this.handleSearchChange)}
required required
minLength={3} minLength={3}
/> />
<label class="sr-only" htmlFor="communities-search"> <label className="sr-only" htmlFor="communities-search">
{i18n.t("search")} {i18n.t("search")}
</label> </label>
<button type="submit" class="btn btn-secondary mr-2 mb-2"> <button type="submit" className="btn btn-secondary mr-2 mb-2">
<span>{i18n.t("search")}</span> <span>{i18n.t("search")}</span>
</button> </button>
</form> </form>
@ -343,10 +349,8 @@ export class Communities extends Component<any, CommunitiesState> {
msg, msg,
ListCommunitiesResponse ListCommunitiesResponse
); );
this.state.listCommunitiesResponse = Some(data); this.setState({ listCommunitiesResponse: Some(data), loading: false });
this.state.loading = false;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state);
} else if (op == UserOperation.FollowCommunity) { } else if (op == UserOperation.FollowCommunity) {
let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse); let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
this.state.listCommunitiesResponse.match({ this.state.listCommunitiesResponse.match({

View file

@ -73,9 +73,14 @@ export class CommunityForm extends Component<
this.handleBannerUpload = this.handleBannerUpload.bind(this); this.handleBannerUpload = this.handleBannerUpload.bind(this);
this.handleBannerRemove = this.handleBannerRemove.bind(this); this.handleBannerRemove = this.handleBannerRemove.bind(this);
this.props.community_view.match({ this.parseMessage = this.parseMessage.bind(this);
some: cv => { this.subscription = wsSubscribe(this.parseMessage);
this.state.communityForm = new CreateCommunity({
if (this.props.community_view.isSome()) {
let cv = this.props.community_view.unwrap();
this.state = {
...this.state,
communityForm: new CreateCommunity({
name: cv.community.name, name: cv.community.name,
title: cv.community.title, title: cv.community.title,
description: cv.community.description, description: cv.community.description,
@ -86,13 +91,9 @@ export class CommunityForm extends Component<
cv.community.posting_restricted_to_mods cv.community.posting_restricted_to_mods
), ),
auth: undefined, auth: undefined,
}); }),
}, };
none: void 0, }
});
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
} }
componentDidUpdate() { componentDidUpdate() {
@ -127,24 +128,24 @@ export class CommunityForm extends Component<
/> />
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}> <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
{this.props.community_view.isNone() && ( {this.props.community_view.isNone() && (
<div class="form-group row"> <div className="form-group row">
<label <label
class="col-12 col-sm-2 col-form-label" className="col-12 col-sm-2 col-form-label"
htmlFor="community-name" htmlFor="community-name"
> >
{i18n.t("name")} {i18n.t("name")}
<span <span
class="position-absolute pointer unselectable ml-2 text-muted" className="position-absolute pointer unselectable ml-2 text-muted"
data-tippy-content={i18n.t("name_explain")} data-tippy-content={i18n.t("name_explain")}
> >
<Icon icon="help-circle" classes="icon-inline" /> <Icon icon="help-circle" classes="icon-inline" />
</span> </span>
</label> </label>
<div class="col-12 col-sm-10"> <div className="col-12 col-sm-10">
<input <input
type="text" type="text"
id="community-name" id="community-name"
class="form-control" className="form-control"
value={this.state.communityForm.name} value={this.state.communityForm.name}
onInput={linkEvent(this, this.handleCommunityNameChange)} onInput={linkEvent(this, this.handleCommunityNameChange)}
required required
@ -155,35 +156,35 @@ export class CommunityForm extends Component<
</div> </div>
</div> </div>
)} )}
<div class="form-group row"> <div className="form-group row">
<label <label
class="col-12 col-sm-2 col-form-label" className="col-12 col-sm-2 col-form-label"
htmlFor="community-title" htmlFor="community-title"
> >
{i18n.t("display_name")} {i18n.t("display_name")}
<span <span
class="position-absolute pointer unselectable ml-2 text-muted" className="position-absolute pointer unselectable ml-2 text-muted"
data-tippy-content={i18n.t("display_name_explain")} data-tippy-content={i18n.t("display_name_explain")}
> >
<Icon icon="help-circle" classes="icon-inline" /> <Icon icon="help-circle" classes="icon-inline" />
</span> </span>
</label> </label>
<div class="col-12 col-sm-10"> <div className="col-12 col-sm-10">
<input <input
type="text" type="text"
id="community-title" id="community-title"
value={this.state.communityForm.title} value={this.state.communityForm.title}
onInput={linkEvent(this, this.handleCommunityTitleChange)} onInput={linkEvent(this, this.handleCommunityTitleChange)}
class="form-control" className="form-control"
required required
minLength={3} minLength={3}
maxLength={100} maxLength={100}
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-12 col-sm-2">{i18n.t("icon")}</label> <label className="col-12 col-sm-2">{i18n.t("icon")}</label>
<div class="col-12 col-sm-10"> <div className="col-12 col-sm-10">
<ImageUploadForm <ImageUploadForm
uploadTitle={i18n.t("upload_icon")} uploadTitle={i18n.t("upload_icon")}
imageSrc={this.state.communityForm.icon} imageSrc={this.state.communityForm.icon}
@ -193,9 +194,9 @@ export class CommunityForm extends Component<
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-12 col-sm-2">{i18n.t("banner")}</label> <label className="col-12 col-sm-2">{i18n.t("banner")}</label>
<div class="col-12 col-sm-10"> <div className="col-12 col-sm-10">
<ImageUploadForm <ImageUploadForm
uploadTitle={i18n.t("upload_banner")} uploadTitle={i18n.t("upload_banner")}
imageSrc={this.state.communityForm.banner} imageSrc={this.state.communityForm.banner}
@ -204,30 +205,32 @@ export class CommunityForm extends Component<
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-12 col-sm-2 col-form-label" htmlFor={this.id}> <label className="col-12 col-sm-2 col-form-label" htmlFor={this.id}>
{i18n.t("sidebar")} {i18n.t("sidebar")}
</label> </label>
<div class="col-12 col-sm-10"> <div className="col-12 col-sm-10">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.communityForm.description} initialContent={this.state.communityForm.description}
initialLanguageId={None}
placeholder={Some("description")} placeholder={Some("description")}
buttonTitle={None} buttonTitle={None}
maxLength={None} maxLength={None}
onContentChange={this.handleCommunityDescriptionChange} onContentChange={this.handleCommunityDescriptionChange}
allLanguages={[]}
/> />
</div> </div>
</div> </div>
{this.props.enableNsfw && ( {this.props.enableNsfw && (
<div class="form-group row"> <div className="form-group row">
<legend class="col-form-label col-sm-2 pt-0"> <legend className="col-form-label col-sm-2 pt-0">
{i18n.t("nsfw")} {i18n.t("nsfw")}
</legend> </legend>
<div class="col-10"> <div className="col-10">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input position-static" className="form-check-input position-static"
id="community-nsfw" id="community-nsfw"
type="checkbox" type="checkbox"
checked={toUndefined(this.state.communityForm.nsfw)} checked={toUndefined(this.state.communityForm.nsfw)}
@ -237,14 +240,14 @@ export class CommunityForm extends Component<
</div> </div>
</div> </div>
)} )}
<div class="form-group row"> <div className="form-group row">
<legend class="col-form-label col-6 pt-0"> <legend className="col-form-label col-6 pt-0">
{i18n.t("only_mods_can_post_in_community")} {i18n.t("only_mods_can_post_in_community")}
</legend> </legend>
<div class="col-6"> <div className="col-6">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input position-static" className="form-check-input position-static"
id="community-only-mods-can-post" id="community-only-mods-can-post"
type="checkbox" type="checkbox"
checked={toUndefined( checked={toUndefined(
@ -258,11 +261,11 @@ export class CommunityForm extends Component<
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<div class="col-12"> <div className="col-12">
<button <button
type="submit" type="submit"
class="btn btn-secondary mr-2" className="btn btn-secondary mr-2"
disabled={this.state.loading} disabled={this.state.loading}
> >
{this.state.loading ? ( {this.state.loading ? (
@ -276,7 +279,7 @@ export class CommunityForm extends Component<
{this.props.community_view.isSome() && ( {this.props.community_view.isSome() && (
<button <button
type="button" type="button"
class="btn btn-secondary" className="btn btn-secondary"
onClick={linkEvent(this, this.handleCancel)} onClick={linkEvent(this, this.handleCancel)}
> >
{i18n.t("cancel")} {i18n.t("cancel")}
@ -291,7 +294,7 @@ export class CommunityForm extends Component<
handleCreateCommunitySubmit(i: CommunityForm, event: any) { handleCreateCommunitySubmit(i: CommunityForm, event: any) {
event.preventDefault(); event.preventDefault();
i.state.loading = true; i.setState({ loading: true });
let cForm = i.state.communityForm; let cForm = i.state.communityForm;
cForm.auth = auth().unwrap(); cForm.auth = auth().unwrap();
@ -330,17 +333,18 @@ export class CommunityForm extends Component<
} }
handleCommunityDescriptionChange(val: string) { handleCommunityDescriptionChange(val: string) {
this.state.communityForm.description = Some(val); this.setState(s => ((s.communityForm.description = Some(val)), s));
this.setState(this.state);
} }
handleCommunityNsfwChange(i: CommunityForm, event: any) { handleCommunityNsfwChange(i: CommunityForm, event: any) {
i.state.communityForm.nsfw = event.target.checked; i.state.communityForm.nsfw = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
} }
handleCommunityPostingRestrictedToMods(i: CommunityForm, event: any) { handleCommunityPostingRestrictedToMods(i: CommunityForm, event: any) {
i.state.communityForm.posting_restricted_to_mods = event.target.checked; i.state.communityForm.posting_restricted_to_mods = Some(
event.target.checked
);
i.setState(i.state); i.setState(i.state);
} }
@ -349,23 +353,19 @@ export class CommunityForm extends Component<
} }
handleIconUpload(url: string) { handleIconUpload(url: string) {
this.state.communityForm.icon = Some(url); this.setState(s => ((s.communityForm.icon = Some(url)), s));
this.setState(this.state);
} }
handleIconRemove() { handleIconRemove() {
this.state.communityForm.icon = Some(""); this.setState(s => ((s.communityForm.icon = Some("")), s));
this.setState(this.state);
} }
handleBannerUpload(url: string) { handleBannerUpload(url: string) {
this.state.communityForm.banner = Some(url); this.setState(s => ((s.communityForm.banner = Some(url)), s));
this.setState(this.state);
} }
handleBannerRemove() { handleBannerRemove() {
this.state.communityForm.banner = Some(""); this.setState(s => ((s.communityForm.banner = Some("")), s));
this.setState(this.state);
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -374,12 +374,10 @@ export class CommunityForm extends Component<
if (msg.error) { if (msg.error) {
// Errors handled by top level pages // Errors handled by top level pages
// toast(i18n.t(msg.error), "danger"); // toast(i18n.t(msg.error), "danger");
this.state.loading = false; this.setState({ loading: false });
this.setState(this.state);
return; return;
} else if (op == UserOperation.CreateCommunity) { } else if (op == UserOperation.CreateCommunity) {
let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse); let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
this.state.loading = false;
this.props.onCreate(data.community_view); this.props.onCreate(data.community_view);
// Update myUserInfo // Update myUserInfo
@ -401,7 +399,7 @@ export class CommunityForm extends Component<
}); });
} else if (op == UserOperation.EditCommunity) { } else if (op == UserOperation.EditCommunity) {
let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse); let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
this.state.loading = false; this.setState({ loading: false });
this.props.onEdit(data.community_view); this.props.onEdit(data.community_view);
let community = data.community_view.community; let community = data.community_view.community;

View file

@ -64,7 +64,7 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
some: icon => <PictrsImage src={icon} icon />, some: icon => <PictrsImage src={icon} icon />,
none: <></>, none: <></>,
})} })}
<span class="overflow-wrap-anywhere">{displayName}</span> <span className="overflow-wrap-anywhere">{displayName}</span>
</> </>
); );
} }

View file

@ -49,7 +49,9 @@ import {
getDataTypeFromProps, getDataTypeFromProps,
getPageFromProps, getPageFromProps,
getSortTypeFromProps, getSortTypeFromProps,
isPostBlocked,
notifyPost, notifyPost,
nsfwCheck,
postToCommentSortType, postToCommentSortType,
relTags, relTags,
restoreScrollPosition, restoreScrollPosition,
@ -139,24 +141,27 @@ export class Community extends Component<any, State> {
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state.communityRes = Some( this.state = {
this.isoData.routeData[0] as GetCommunityResponse ...this.state,
); communityRes: Some(this.isoData.routeData[0] as GetCommunityResponse),
};
let postsRes = Some(this.isoData.routeData[1] as GetPostsResponse); let postsRes = Some(this.isoData.routeData[1] as GetPostsResponse);
let commentsRes = Some(this.isoData.routeData[2] as GetCommentsResponse); let commentsRes = Some(this.isoData.routeData[2] as GetCommentsResponse);
postsRes.match({ if (postsRes.isSome()) {
some: pvs => (this.state.posts = pvs.posts), this.state = { ...this.state, posts: postsRes.unwrap().posts };
none: void 0, }
});
commentsRes.match({
some: cvs => (this.state.comments = cvs.comments),
none: void 0,
});
this.state.communityLoading = false; if (commentsRes.isSome()) {
this.state.postsLoading = false; this.state = { ...this.state, comments: commentsRes.unwrap().comments };
this.state.commentsLoading = false; }
this.state = {
...this.state,
communityLoading: false,
postsLoading: false,
commentsLoading: false,
};
} else { } else {
this.fetchCommunity(); this.fetchCommunity();
this.fetchData(); this.fetchData();
@ -278,7 +283,7 @@ export class Community extends Component<any, State> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
{this.state.communityLoading ? ( {this.state.communityLoading ? (
<h5> <h5>
<Spinner large /> <Spinner large />
@ -294,12 +299,12 @@ export class Community extends Component<any, State> {
image={res.community_view.community.icon} image={res.community_view.community.icon}
/> />
<div class="row"> <div className="row">
<div class="col-12 col-md-8"> <div className="col-12 col-md-8">
{this.communityInfo()} {this.communityInfo()}
<div class="d-block d-md-none"> <div className="d-block d-md-none">
<button <button
class="btn btn-secondary d-inline-block mb-2 mr-3" className="btn btn-secondary d-inline-block mb-2 mr-3"
onClick={linkEvent(this, this.handleShowSidebarMobile)} onClick={linkEvent(this, this.handleShowSidebarMobile)}
> >
{i18n.t("sidebar")}{" "} {i18n.t("sidebar")}{" "}
@ -344,7 +349,7 @@ export class Community extends Component<any, State> {
onChange={this.handlePageChange} onChange={this.handlePageChange}
/> />
</div> </div>
<div class="d-none d-md-block col-md-4"> <div className="d-none d-md-block col-md-4">
<Sidebar <Sidebar
community_view={res.community_view} community_view={res.community_view}
moderators={res.moderators} moderators={res.moderators}
@ -388,6 +393,7 @@ export class Community extends Component<any, State> {
removeDuplicates removeDuplicates
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)} enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
/> />
) )
) : this.state.commentsLoading ? ( ) : this.state.commentsLoading ? (
@ -404,6 +410,7 @@ export class Community extends Component<any, State> {
moderators={this.state.communityRes.map(r => r.moderators)} moderators={this.state.communityRes.map(r => r.moderators)}
admins={Some(this.state.siteRes.admins)} admins={Some(this.state.siteRes.admins)}
maxCommentsShown={None} maxCommentsShown={None}
allLanguages={this.state.siteRes.all_languages}
/> />
); );
} }
@ -413,9 +420,9 @@ export class Community extends Component<any, State> {
.map(r => r.community_view.community) .map(r => r.community_view.community)
.match({ .match({
some: community => ( some: community => (
<div class="mb-2"> <div className="mb-2">
<BannerIconHeader banner={community.banner} icon={community.icon} /> <BannerIconHeader banner={community.banner} icon={community.icon} />
<h5 class="mb-0 overflow-wrap-anywhere">{community.title}</h5> <h5 className="mb-0 overflow-wrap-anywhere">{community.title}</h5>
<CommunityLink <CommunityLink
community={community} community={community}
realLink realLink
@ -434,14 +441,14 @@ export class Community extends Component<any, State> {
communityRSSUrl(r.community_view.community.actor_id, this.state.sort) communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
); );
return ( return (
<div class="mb-3"> <div className="mb-3">
<span class="mr-3"> <span className="mr-3">
<DataTypeSelect <DataTypeSelect
type_={this.state.dataType} type_={this.state.dataType}
onChange={this.handleDataTypeChange} onChange={this.handleDataTypeChange}
/> />
</span> </span>
<span class="mr-2"> <span className="mr-2">
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} /> <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
</span> </span>
{communityRss.match({ {communityRss.match({
@ -475,8 +482,7 @@ export class Community extends Component<any, State> {
} }
handleShowSidebarMobile(i: Community) { handleShowSidebarMobile(i: Community) {
i.state.showSidebarMobile = !i.state.showSidebarMobile; i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
i.setState(i.state);
} }
updateUrl(paramUpdates: UrlParams) { updateUrl(paramUpdates: UrlParams) {
@ -543,9 +549,7 @@ export class Community extends Component<any, State> {
this.fetchData(); this.fetchData();
} else if (op == UserOperation.GetCommunity) { } else if (op == UserOperation.GetCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse); let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
this.state.communityRes = Some(data); this.setState({ communityRes: Some(data), communityLoading: false });
this.state.communityLoading = false;
this.setState(this.state);
// TODO why is there no auth in this form? // TODO why is there no auth in this form?
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.communityJoin({ wsClient.communityJoin({
@ -576,9 +580,7 @@ export class Community extends Component<any, State> {
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetPosts) { } else if (op == UserOperation.GetPosts) {
let data = wsJsonToRes<GetPostsResponse>(msg, GetPostsResponse); let data = wsJsonToRes<GetPostsResponse>(msg, GetPostsResponse);
this.state.posts = data.posts; this.setState({ posts: data.posts, postsLoading: false });
this.state.postsLoading = false;
this.setState(this.state);
restoreScrollPosition(this.context); restoreScrollPosition(this.context);
setupTippy(); setupTippy();
} else if ( } else if (
@ -594,15 +596,24 @@ export class Community extends Component<any, State> {
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePost) { } else if (op == UserOperation.CreatePost) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg, PostResponse);
this.state.posts.unshift(data.post_view);
let showPostNotifs = UserService.Instance.myUserInfo
.map(m => m.local_user_view.local_user.show_new_post_notifs)
.unwrapOr(false);
// Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
//
if ( if (
UserService.Instance.myUserInfo this.state.page == 1 &&
.map(m => m.local_user_view.local_user.show_new_post_notifs) nsfwCheck(data.post_view) &&
.unwrapOr(false) !isPostBlocked(data.post_view)
) { ) {
notifyPost(data.post_view, this.context.router); this.state.posts.unshift(data.post_view);
if (showPostNotifs) {
notifyPost(data.post_view, this.context.router);
}
this.setState(this.state);
} }
this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) { } else if (op == UserOperation.CreatePostLike) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg, PostResponse);
createPostLikeFindRes(data.post_view, this.state.posts); createPostLikeFindRes(data.post_view, this.state.posts);
@ -631,9 +642,7 @@ export class Community extends Component<any, State> {
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetComments) { } else if (op == UserOperation.GetComments) {
let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse); let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse);
this.state.comments = data.comments; this.setState({ comments: data.comments, commentsLoading: false });
this.state.commentsLoading = false;
this.setState(this.state);
} else if ( } else if (
op == UserOperation.EditComment || op == UserOperation.EditComment ||
op == UserOperation.DeleteComment || op == UserOperation.DeleteComment ||

View file

@ -56,7 +56,7 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
@ -68,8 +68,8 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
<div class="row"> <div className="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t("create_community")}</h5> <h5>{i18n.t("create_community")}</h5>
<CommunityForm <CommunityForm
community_view={None} community_view={None}

View file

@ -91,16 +91,17 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
sidebar() { sidebar() {
return ( return (
<div> <div>
<div class="card border-secondary mb-3"> <div className="card border-secondary mb-3">
<div class="card-body"> <div className="card-body">
{this.communityTitle()} {this.communityTitle()}
{this.adminButtons()} {this.adminButtons()}
{this.subscribe()} {this.subscribe()}
{this.canPost && this.createPost()} {this.canPost && this.createPost()}
{this.blockCommunity()}
</div> </div>
</div> </div>
<div class="card border-secondary mb-3"> <div className="card border-secondary mb-3">
<div class="card-body"> <div className="card-body">
{this.description()} {this.description()}
{this.badges()} {this.badges()}
{this.mods()} {this.mods()}
@ -119,10 +120,10 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{this.props.showIcon && ( {this.props.showIcon && (
<BannerIconHeader icon={community.icon} banner={community.banner} /> <BannerIconHeader icon={community.icon} banner={community.banner} />
)} )}
<span class="mr-2">{community.title}</span> <span className="mr-2">{community.title}</span>
{subscribed == SubscribedType.Subscribed && ( {subscribed == SubscribedType.Subscribed && (
<button <button
class="btn btn-secondary btn-sm mr-2" className="btn btn-secondary btn-sm mr-2"
onClick={linkEvent(this, this.handleUnsubscribe)} onClick={linkEvent(this, this.handleUnsubscribe)}
> >
<Icon icon="check" classes="icon-inline text-success mr-1" /> <Icon icon="check" classes="icon-inline text-success mr-1" />
@ -131,7 +132,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
)} )}
{subscribed == SubscribedType.Pending && ( {subscribed == SubscribedType.Pending && (
<button <button
class="btn btn-warning mr-2" className="btn btn-warning mr-2"
onClick={linkEvent(this, this.handleUnsubscribe)} onClick={linkEvent(this, this.handleUnsubscribe)}
> >
{i18n.t("subscribe_pending")} {i18n.t("subscribe_pending")}
@ -168,7 +169,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
let community_view = this.props.community_view; let community_view = this.props.community_view;
let counts = community_view.counts; let counts = community_view.counts;
return ( return (
<ul class="my-1 list-inline"> <ul className="my-1 list-inline">
<li className="list-inline-item badge badge-secondary"> <li className="list-inline-item badge badge-secondary">
{i18n.t("number_online", { {i18n.t("number_online", {
count: this.props.online, count: this.props.online,
@ -259,10 +260,10 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
mods() { mods() {
return ( return (
<ul class="list-inline small"> <ul className="list-inline small">
<li class="list-inline-item">{i18n.t("mods")}: </li> <li className="list-inline-item">{i18n.t("mods")}: </li>
{this.props.moderators.map(mod => ( {this.props.moderators.map(mod => (
<li class="list-inline-item"> <li key={mod.moderator.id} className="list-inline-item">
<PersonListing person={mod.moderator} /> <PersonListing person={mod.moderator} />
</li> </li>
))} ))}
@ -273,53 +274,59 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
createPost() { createPost() {
let cv = this.props.community_view; let cv = this.props.community_view;
return ( return (
cv.subscribed == SubscribedType.Subscribed && ( <Link
<Link className={`btn btn-secondary btn-block mb-2 ${
className={`btn btn-secondary btn-block mb-2 ${ cv.community.deleted || cv.community.removed ? "no-click" : ""
cv.community.deleted || cv.community.removed ? "no-click" : "" }`}
}`} to={`/create_post?community_id=${cv.community.id}`}
to={`/create_post?community_id=${cv.community.id}`} >
> {i18n.t("create_a_post")}
{i18n.t("create_a_post")} </Link>
</Link>
)
); );
} }
subscribe() { subscribe() {
let community_view = this.props.community_view; let community_view = this.props.community_view;
let blocked = this.props.community_view.blocked;
return ( return (
<div class="mb-2"> <div className="mb-2">
{community_view.subscribed == SubscribedType.NotSubscribed && ( {community_view.subscribed == SubscribedType.NotSubscribed && (
<> <button
<button className="btn btn-secondary btn-block"
class="btn btn-secondary btn-block" onClick={linkEvent(this, this.handleSubscribe)}
onClick={linkEvent(this, this.handleSubscribe)} >
> {i18n.t("subscribe")}
{i18n.t("subscribe")} </button>
</button>
{blocked ? (
<button
class="btn btn-danger btn-block"
onClick={linkEvent(this, this.handleUnblock)}
>
{i18n.t("unblock_community")}
</button>
) : (
<button
class="btn btn-danger btn-block"
onClick={linkEvent(this, this.handleBlock)}
>
{i18n.t("block_community")}
</button>
)}
</>
)} )}
</div> </div>
); );
} }
blockCommunity() {
let community_view = this.props.community_view;
let blocked = this.props.community_view.blocked;
return (
<div className="mb-2">
{community_view.subscribed == SubscribedType.NotSubscribed &&
(blocked ? (
<button
className="btn btn-danger btn-block"
onClick={linkEvent(this, this.handleUnblock)}
>
{i18n.t("unblock_community")}
</button>
) : (
<button
className="btn btn-danger btn-block"
onClick={linkEvent(this, this.handleBlock)}
>
{i18n.t("block_community")}
</button>
))}
</div>
);
}
description() { description() {
let description = this.props.community_view.community.description; let description = this.props.community_view.community.description;
return description.match({ return description.match({
@ -334,12 +341,12 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
let community_view = this.props.community_view; let community_view = this.props.community_view;
return ( return (
<> <>
<ul class="list-inline mb-1 text-muted font-weight-bold"> <ul className="list-inline mb-1 text-muted font-weight-bold">
{amMod(Some(this.props.moderators)) && ( {amMod(Some(this.props.moderators)) && (
<> <>
<li className="list-inline-item-action"> <li className="list-inline-item-action">
<button <button
class="btn btn-link text-muted d-inline-block" className="btn btn-link text-muted d-inline-block"
onClick={linkEvent(this, this.handleEditClick)} onClick={linkEvent(this, this.handleEditClick)}
data-tippy-content={i18n.t("edit")} data-tippy-content={i18n.t("edit")}
aria-label={i18n.t("edit")} aria-label={i18n.t("edit")}
@ -351,7 +358,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
(!this.state.showConfirmLeaveModTeam ? ( (!this.state.showConfirmLeaveModTeam ? (
<li className="list-inline-item-action"> <li className="list-inline-item-action">
<button <button
class="btn btn-link text-muted d-inline-block" className="btn btn-link text-muted d-inline-block"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleShowConfirmLeaveModTeamClick this.handleShowConfirmLeaveModTeamClick
@ -367,7 +374,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</li> </li>
<li className="list-inline-item-action"> <li className="list-inline-item-action">
<button <button
class="btn btn-link text-muted d-inline-block" className="btn btn-link text-muted d-inline-block"
onClick={linkEvent(this, this.handleLeaveModTeamClick)} onClick={linkEvent(this, this.handleLeaveModTeamClick)}
> >
{i18n.t("yes")} {i18n.t("yes")}
@ -375,7 +382,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</li> </li>
<li className="list-inline-item-action"> <li className="list-inline-item-action">
<button <button
class="btn btn-link text-muted d-inline-block" className="btn btn-link text-muted d-inline-block"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleCancelLeaveModTeamClick this.handleCancelLeaveModTeamClick
@ -389,7 +396,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{amTopMod(Some(this.props.moderators)) && ( {amTopMod(Some(this.props.moderators)) && (
<li className="list-inline-item-action"> <li className="list-inline-item-action">
<button <button
class="btn btn-link text-muted d-inline-block" className="btn btn-link text-muted d-inline-block"
onClick={linkEvent(this, this.handleDeleteClick)} onClick={linkEvent(this, this.handleDeleteClick)}
data-tippy-content={ data-tippy-content={
!community_view.community.deleted !community_view.community.deleted
@ -413,25 +420,25 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
)} )}
</> </>
)} )}
{amAdmin(Some(this.props.admins)) && ( {amAdmin() && (
<li className="list-inline-item"> <li className="list-inline-item">
{!this.props.community_view.community.removed ? ( {!this.props.community_view.community.removed ? (
<button <button
class="btn btn-link text-muted d-inline-block" className="btn btn-link text-muted d-inline-block"
onClick={linkEvent(this, this.handleModRemoveShow)} onClick={linkEvent(this, this.handleModRemoveShow)}
> >
{i18n.t("remove")} {i18n.t("remove")}
</button> </button>
) : ( ) : (
<button <button
class="btn btn-link text-muted d-inline-block" className="btn btn-link text-muted d-inline-block"
onClick={linkEvent(this, this.handleModRemoveSubmit)} onClick={linkEvent(this, this.handleModRemoveSubmit)}
> >
{i18n.t("restore")} {i18n.t("restore")}
</button> </button>
)} )}
<button <button
class="btn btn-link text-muted d-inline-block" className="btn btn-link text-muted d-inline-block"
onClick={linkEvent(this, this.handlePurgeCommunityShow)} onClick={linkEvent(this, this.handlePurgeCommunityShow)}
aria-label={i18n.t("purge_community")} aria-label={i18n.t("purge_community")}
> >
@ -442,14 +449,14 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</ul> </ul>
{this.state.showRemoveDialog && ( {this.state.showRemoveDialog && (
<form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}> <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
<div class="form-group"> <div className="form-group">
<label class="col-form-label" htmlFor="remove-reason"> <label className="col-form-label" htmlFor="remove-reason">
{i18n.t("reason")} {i18n.t("reason")}
</label> </label>
<input <input
type="text" type="text"
id="remove-reason" id="remove-reason"
class="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("optional")} placeholder={i18n.t("optional")}
value={toUndefined(this.state.removeReason)} value={toUndefined(this.state.removeReason)}
onInput={linkEvent(this, this.handleModRemoveReasonChange)} onInput={linkEvent(this, this.handleModRemoveReasonChange)}
@ -460,8 +467,8 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{/* <label class="col-form-label">Expires</label> */} {/* <label class="col-form-label">Expires</label> */}
{/* <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"> <div className="form-group">
<button type="submit" class="btn btn-secondary"> <button type="submit" className="btn btn-secondary">
{i18n.t("remove_community")} {i18n.t("remove_community")}
</button> </button>
</div> </div>
@ -469,29 +476,29 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
)} )}
{this.state.showPurgeDialog && ( {this.state.showPurgeDialog && (
<form onSubmit={linkEvent(this, this.handlePurgeSubmit)}> <form onSubmit={linkEvent(this, this.handlePurgeSubmit)}>
<div class="form-group"> <div className="form-group">
<PurgeWarning /> <PurgeWarning />
</div> </div>
<div class="form-group"> <div className="form-group">
<label class="sr-only" htmlFor="purge-reason"> <label className="sr-only" htmlFor="purge-reason">
{i18n.t("reason")} {i18n.t("reason")}
</label> </label>
<input <input
type="text" type="text"
id="purge-reason" id="purge-reason"
class="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={toUndefined(this.state.purgeReason)} value={toUndefined(this.state.purgeReason)}
onInput={linkEvent(this, this.handlePurgeReasonChange)} onInput={linkEvent(this, this.handlePurgeReasonChange)}
/> />
</div> </div>
<div class="form-group"> <div className="form-group">
{this.state.purgeLoading ? ( {this.state.purgeLoading ? (
<Spinner /> <Spinner />
) : ( ) : (
<button <button
type="submit" type="submit"
class="btn btn-secondary" className="btn btn-secondary"
aria-label={i18n.t("purge_community")} aria-label={i18n.t("purge_community")}
> >
{i18n.t("purge_community")} {i18n.t("purge_community")}
@ -505,18 +512,15 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
} }
handleEditClick(i: Sidebar) { handleEditClick(i: Sidebar) {
i.state.showEdit = true; i.setState({ showEdit: true });
i.setState(i.state);
} }
handleEditCommunity() { handleEditCommunity() {
this.state.showEdit = false; this.setState({ showEdit: false });
this.setState(this.state);
} }
handleEditCancel() { handleEditCancel() {
this.state.showEdit = false; this.setState({ showEdit: false });
this.setState(this.state);
} }
handleDeleteClick(i: Sidebar, event: any) { handleDeleteClick(i: Sidebar, event: any) {
@ -530,8 +534,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
} }
handleShowConfirmLeaveModTeamClick(i: Sidebar) { handleShowConfirmLeaveModTeamClick(i: Sidebar) {
i.state.showConfirmLeaveModTeam = true; i.setState({ showConfirmLeaveModTeam: true });
i.setState(i.state);
} }
handleLeaveModTeamClick(i: Sidebar) { handleLeaveModTeamClick(i: Sidebar) {
@ -544,16 +547,14 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.addModToCommunity(form)); WebSocketService.Instance.send(wsClient.addModToCommunity(form));
i.state.showConfirmLeaveModTeam = false; i.setState({ showConfirmLeaveModTeam: false });
i.setState(i.state);
}, },
none: void 0, none: void 0,
}); });
} }
handleCancelLeaveModTeamClick(i: Sidebar) { handleCancelLeaveModTeamClick(i: Sidebar) {
i.state.showConfirmLeaveModTeam = false; i.setState({ showConfirmLeaveModTeam: false });
i.setState(i.state);
} }
handleUnsubscribe(i: Sidebar, event: any) { handleUnsubscribe(i: Sidebar, event: any) {
@ -599,23 +600,20 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
return ( return (
!this.props.community_view.community.posting_restricted_to_mods || !this.props.community_view.community.posting_restricted_to_mods ||
amMod(Some(this.props.moderators)) || amMod(Some(this.props.moderators)) ||
amAdmin(Some(this.props.admins)) amAdmin()
); );
} }
handleModRemoveShow(i: Sidebar) { handleModRemoveShow(i: Sidebar) {
i.state.showRemoveDialog = true; i.setState({ showRemoveDialog: true });
i.setState(i.state);
} }
handleModRemoveReasonChange(i: Sidebar, event: any) { handleModRemoveReasonChange(i: Sidebar, event: any) {
i.state.removeReason = Some(event.target.value); i.setState({ removeReason: Some(event.target.value) });
i.setState(i.state);
} }
handleModRemoveExpiresChange(i: Sidebar, event: any) { handleModRemoveExpiresChange(i: Sidebar, event: any) {
i.state.removeExpires = Some(event.target.value); i.setState({ removeExpires: Some(event.target.value) });
i.setState(i.state);
} }
handleModRemoveSubmit(i: Sidebar, event: any) { handleModRemoveSubmit(i: Sidebar, event: any) {
@ -629,19 +627,15 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
}); });
WebSocketService.Instance.send(wsClient.removeCommunity(removeForm)); WebSocketService.Instance.send(wsClient.removeCommunity(removeForm));
i.state.showRemoveDialog = false; i.setState({ showRemoveDialog: false });
i.setState(i.state);
} }
handlePurgeCommunityShow(i: Sidebar) { handlePurgeCommunityShow(i: Sidebar) {
i.state.showPurgeDialog = true; i.setState({ showPurgeDialog: true, showRemoveDialog: false });
i.state.showRemoveDialog = false;
i.setState(i.state);
} }
handlePurgeReasonChange(i: Sidebar, event: any) { handlePurgeReasonChange(i: Sidebar, event: any) {
i.state.purgeReason = Some(event.target.value); i.setState({ purgeReason: Some(event.target.value) });
i.setState(i.state);
} }
handlePurgeSubmit(i: Sidebar, event: any) { handlePurgeSubmit(i: Sidebar, event: any) {
@ -654,8 +648,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
}); });
WebSocketService.Instance.send(wsClient.purgeCommunity(form)); WebSocketService.Instance.send(wsClient.purgeCommunity(form));
i.state.purgeLoading = true; i.setState({ purgeLoading: true });
i.setState(i.state);
} }
handleBlock(i: Sidebar, event: any) { handleBlock(i: Sidebar, event: any) {

View file

@ -59,10 +59,11 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state.banned = ( this.state = {
this.isoData.routeData[0] as BannedPersonsResponse ...this.state,
).banned; banned: (this.isoData.routeData[0] as BannedPersonsResponse).banned,
this.state.loading = false; loading: false,
};
} else { } else {
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getBannedPersons({ wsClient.getBannedPersons({
@ -103,14 +104,14 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
<div class="row"> <div className="row">
<div class="col-12 col-md-6"> <div className="col-12 col-md-6">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
@ -127,7 +128,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
none: <></>, none: <></>,
})} })}
</div> </div>
<div class="col-12 col-md-6"> <div className="col-12 col-md-6">
{this.admins()} {this.admins()}
{this.bannedUsers()} {this.bannedUsers()}
</div> </div>
@ -141,9 +142,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
return ( return (
<> <>
<h5>{capitalizeFirstLetter(i18n.t("admins"))}</h5> <h5>{capitalizeFirstLetter(i18n.t("admins"))}</h5>
<ul class="list-unstyled"> <ul className="list-unstyled">
{this.state.siteRes.admins.map(admin => ( {this.state.siteRes.admins.map(admin => (
<li class="list-inline-item"> <li key={admin.person.id} className="list-inline-item">
<PersonListing person={admin.person} /> <PersonListing person={admin.person} />
</li> </li>
))} ))}
@ -157,7 +158,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
return ( return (
<button <button
onClick={linkEvent(this, this.handleLeaveAdminTeam)} onClick={linkEvent(this, this.handleLeaveAdminTeam)}
class="btn btn-danger mb-2" className="btn btn-danger mb-2"
> >
{this.state.leaveAdminTeamLoading ? ( {this.state.leaveAdminTeamLoading ? (
<Spinner /> <Spinner />
@ -172,9 +173,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
return ( return (
<> <>
<h5>{i18n.t("banned_users")}</h5> <h5>{i18n.t("banned_users")}</h5>
<ul class="list-unstyled"> <ul className="list-unstyled">
{this.state.banned.map(banned => ( {this.state.banned.map(banned => (
<li class="list-inline-item"> <li key={banned.person.id} className="list-inline-item">
<PersonListing person={banned.person} /> <PersonListing person={banned.person} />
</li> </li>
))} ))}
@ -184,11 +185,10 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
} }
handleLeaveAdminTeam(i: AdminSettings) { handleLeaveAdminTeam(i: AdminSettings) {
i.state.leaveAdminTeamLoading = true; i.setState({ leaveAdminTeamLoading: true });
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.leaveAdmin({ auth: auth().unwrap() }) wsClient.leaveAdmin({ auth: auth().unwrap() })
); );
i.setState(i.state);
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -197,26 +197,21 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
if (msg.error) { if (msg.error) {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
this.context.router.history.push("/"); this.context.router.history.push("/");
this.state.loading = false; this.setState({ loading: false });
this.setState(this.state);
return; return;
} else if (op == UserOperation.EditSite) { } else if (op == UserOperation.EditSite) {
let data = wsJsonToRes<SiteResponse>(msg, SiteResponse); let data = wsJsonToRes<SiteResponse>(msg, SiteResponse);
this.state.siteRes.site_view = Some(data.site_view); this.setState(s => ((s.siteRes.site_view = Some(data.site_view)), s));
this.setState(this.state);
toast(i18n.t("site_saved")); toast(i18n.t("site_saved"));
} else if (op == UserOperation.GetBannedPersons) { } else if (op == UserOperation.GetBannedPersons) {
let data = wsJsonToRes<BannedPersonsResponse>(msg, BannedPersonsResponse); let data = wsJsonToRes<BannedPersonsResponse>(msg, BannedPersonsResponse);
this.state.banned = data.banned; this.setState({ banned: data.banned, loading: false });
this.state.loading = false;
this.setState(this.state);
} else if (op == UserOperation.LeaveAdmin) { } else if (op == UserOperation.LeaveAdmin) {
let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse); let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
this.state.siteRes.site_view = data.site_view; this.setState(s => ((s.siteRes.site_view = data.site_view), s));
this.setState(this.state); this.setState({ leaveAdminTeamLoading: false });
this.state.leaveAdminTeamLoading = false;
toast(i18n.t("left_admin_team")); toast(i18n.t("left_admin_team"));
this.setState(this.state);
this.context.router.history.push("/"); this.context.router.history.push("/");
} }
} }

View file

@ -38,6 +38,7 @@ import {
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
auth, auth,
canCreateCommunity,
commentsToFlatNodes, commentsToFlatNodes,
createCommentLikeRes, createCommentLikeRes,
createPostLikeFindRes, createPostLikeFindRes,
@ -51,7 +52,9 @@ import {
getPageFromProps, getPageFromProps,
getSortTypeFromProps, getSortTypeFromProps,
isBrowser, isBrowser,
isPostBlocked,
notifyPost, notifyPost,
nsfwCheck,
postToCommentSortType, postToCommentSortType,
relTags, relTags,
restoreScrollPosition, restoreScrollPosition,
@ -157,22 +160,24 @@ export class Home extends Component<any, HomeState> {
let commentsRes = Some(this.isoData.routeData[1] as GetCommentsResponse); let commentsRes = Some(this.isoData.routeData[1] as GetCommentsResponse);
let trendingRes = this.isoData.routeData[2] as ListCommunitiesResponse; let trendingRes = this.isoData.routeData[2] as ListCommunitiesResponse;
postsRes.match({ if (postsRes.isSome()) {
some: pvs => (this.state.posts = pvs.posts), this.state = { ...this.state, posts: postsRes.unwrap().posts };
none: void 0, }
});
commentsRes.match({ if (commentsRes.isSome()) {
some: cvs => (this.state.comments = cvs.comments), this.state = { ...this.state, comments: commentsRes.unwrap().comments };
none: void 0, }
});
this.state.trendingCommunities = trendingRes.communities;
if (isBrowser()) { if (isBrowser()) {
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.communityJoin({ community_id: 0 }) wsClient.communityJoin({ community_id: 0 })
); );
} }
this.state.loading = false; this.state = {
...this.state,
trendingCommunities: trendingRes.communities,
loading: false,
};
} else { } else {
this.fetchTrendingCommunities(); this.fetchTrendingCommunities();
this.fetchData(); this.fetchData();
@ -317,7 +322,7 @@ export class Home extends Component<any, HomeState> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
@ -325,12 +330,14 @@ export class Home extends Component<any, HomeState> {
image={None} image={None}
/> />
{this.state.siteRes.site_view.isSome() && ( {this.state.siteRes.site_view.isSome() && (
<div class="row"> <div className="row">
<main role="main" class="col-12 col-md-8"> <main role="main" className="col-12 col-md-8">
<div class="d-block d-md-none">{this.mobileView()}</div> <div className="d-block d-md-none">{this.mobileView()}</div>
{this.posts()} {this.posts()}
</main> </main>
<aside class="d-none d-md-block col-md-4">{this.mySidebar()}</aside> <aside className="d-none d-md-block col-md-4">
{this.mySidebar()}
</aside>
</div> </div>
)} )}
</div> </div>
@ -347,11 +354,11 @@ export class Home extends Component<any, HomeState> {
mobileView() { mobileView() {
let siteRes = this.state.siteRes; let siteRes = this.state.siteRes;
return ( return (
<div class="row"> <div className="row">
<div class="col-12"> <div className="col-12">
{this.hasFollows && ( {this.hasFollows && (
<button <button
class="btn btn-secondary d-inline-block mb-2 mr-3" className="btn btn-secondary d-inline-block mb-2 mr-3"
onClick={linkEvent(this, this.handleShowSubscribedMobile)} onClick={linkEvent(this, this.handleShowSubscribedMobile)}
> >
{i18n.t("subscribed")}{" "} {i18n.t("subscribed")}{" "}
@ -366,7 +373,7 @@ export class Home extends Component<any, HomeState> {
</button> </button>
)} )}
<button <button
class="btn btn-secondary d-inline-block mb-2 mr-3" className="btn btn-secondary d-inline-block mb-2 mr-3"
onClick={linkEvent(this, this.handleShowTrendingMobile)} onClick={linkEvent(this, this.handleShowTrendingMobile)}
> >
{i18n.t("trending")}{" "} {i18n.t("trending")}{" "}
@ -378,7 +385,7 @@ export class Home extends Component<any, HomeState> {
/> />
</button> </button>
<button <button
class="btn btn-secondary d-inline-block mb-2 mr-3" className="btn btn-secondary d-inline-block mb-2 mr-3"
onClick={linkEvent(this, this.handleShowSidebarMobile)} onClick={linkEvent(this, this.handleShowSidebarMobile)}
> >
{i18n.t("sidebar")}{" "} {i18n.t("sidebar")}{" "}
@ -403,13 +410,13 @@ export class Home extends Component<any, HomeState> {
none: <></>, none: <></>,
})} })}
{this.state.showTrendingMobile && ( {this.state.showTrendingMobile && (
<div class="col-12 card border-secondary mb-3"> <div className="col-12 card border-secondary mb-3">
<div class="card-body">{this.trendingCommunities()}</div> <div className="card-body">{this.trendingCommunities()}</div>
</div> </div>
)} )}
{this.state.showSubscribedMobile && ( {this.state.showSubscribedMobile && (
<div class="col-12 card border-secondary mb-3"> <div className="col-12 card border-secondary mb-3">
<div class="card-body">{this.subscribedCommunities()}</div> <div className="card-body">{this.subscribedCommunities()}</div>
</div> </div>
)} )}
</div> </div>
@ -423,10 +430,11 @@ export class Home extends Component<any, HomeState> {
<div> <div>
{!this.state.loading && ( {!this.state.loading && (
<div> <div>
<div class="card border-secondary mb-3"> <div className="card border-secondary mb-3">
<div class="card-body"> <div className="card-body">
{this.trendingCommunities()} {this.trendingCommunities()}
{this.createCommunityButton()} {canCreateCommunity(this.state.siteRes) &&
this.createCommunityButton()}
{this.exploreCommunitiesButton()} {this.exploreCommunitiesButton()}
</div> </div>
</div> </div>
@ -443,8 +451,8 @@ export class Home extends Component<any, HomeState> {
none: <></>, none: <></>,
})} })}
{this.hasFollows && ( {this.hasFollows && (
<div class="card border-secondary mb-3"> <div className="card border-secondary mb-3">
<div class="card-body">{this.subscribedCommunities()}</div> <div className="card-body">{this.subscribedCommunities()}</div>
</div> </div>
)} )}
</div> </div>
@ -480,9 +488,12 @@ export class Home extends Component<any, HomeState> {
</Link> </Link>
</T> </T>
</h5> </h5>
<ul class="list-inline mb-0"> <ul className="list-inline mb-0">
{this.state.trendingCommunities.map(cv => ( {this.state.trendingCommunities.map(cv => (
<li class="list-inline-item d-inline-block"> <li
key={cv.community.id}
className="list-inline-item d-inline-block"
>
<CommunityLink community={cv.community} /> <CommunityLink community={cv.community} />
</li> </li>
))} ))}
@ -502,7 +513,7 @@ export class Home extends Component<any, HomeState> {
</Link> </Link>
</T> </T>
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
onClick={linkEvent(this, this.handleCollapseSubscribe)} onClick={linkEvent(this, this.handleCollapseSubscribe)}
aria-label={i18n.t("collapse")} aria-label={i18n.t("collapse")}
data-tippy-content={i18n.t("collapse")} data-tippy-content={i18n.t("collapse")}
@ -515,12 +526,15 @@ export class Home extends Component<any, HomeState> {
</button> </button>
</h5> </h5>
{!this.state.subscribedCollapsed && ( {!this.state.subscribedCollapsed && (
<ul class="list-inline mb-0"> <ul className="list-inline mb-0">
{UserService.Instance.myUserInfo {UserService.Instance.myUserInfo
.map(m => m.follows) .map(m => m.follows)
.unwrapOr([]) .unwrapOr([])
.map(cfv => ( .map(cfv => (
<li class="list-inline-item d-inline-block"> <li
key={cfv.community.id}
className="list-inline-item d-inline-block"
>
<CommunityLink community={cfv.community} /> <CommunityLink community={cfv.community} />
</li> </li>
))} ))}
@ -542,7 +556,7 @@ export class Home extends Component<any, HomeState> {
posts() { posts() {
return ( return (
<div class="main-content-wrapper"> <div className="main-content-wrapper">
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
<Spinner large /> <Spinner large />
@ -569,6 +583,7 @@ export class Home extends Component<any, HomeState> {
removeDuplicates removeDuplicates
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)} enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
/> />
) : ( ) : (
<CommentNodes <CommentNodes
@ -581,6 +596,7 @@ export class Home extends Component<any, HomeState> {
showCommunity showCommunity
showContext showContext
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
/> />
); );
} }
@ -594,13 +610,13 @@ export class Home extends Component<any, HomeState> {
return ( return (
<div className="mb-3"> <div className="mb-3">
<span class="mr-3"> <span className="mr-3">
<DataTypeSelect <DataTypeSelect
type_={this.state.dataType} type_={this.state.dataType}
onChange={this.handleDataTypeChange} onChange={this.handleDataTypeChange}
/> />
</span> </span>
<span class="mr-3"> <span className="mr-3">
<ListingTypeSelect <ListingTypeSelect
type_={this.state.listingType} type_={this.state.listingType}
showLocal={showLocal(this.isoData)} showLocal={showLocal(this.isoData)}
@ -608,7 +624,7 @@ export class Home extends Component<any, HomeState> {
onChange={this.handleListingTypeChange} onChange={this.handleListingTypeChange}
/> />
</span> </span>
<span class="mr-2"> <span className="mr-2">
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} /> <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
</span> </span>
{this.state.listingType == ListingType.All && ( {this.state.listingType == ListingType.All && (
@ -644,23 +660,19 @@ export class Home extends Component<any, HomeState> {
} }
handleShowSubscribedMobile(i: Home) { handleShowSubscribedMobile(i: Home) {
i.state.showSubscribedMobile = !i.state.showSubscribedMobile; i.setState({ showSubscribedMobile: !i.state.showSubscribedMobile });
i.setState(i.state);
} }
handleShowTrendingMobile(i: Home) { handleShowTrendingMobile(i: Home) {
i.state.showTrendingMobile = !i.state.showTrendingMobile; i.setState({ showTrendingMobile: !i.state.showTrendingMobile });
i.setState(i.state);
} }
handleShowSidebarMobile(i: Home) { handleShowSidebarMobile(i: Home) {
i.state.showSidebarMobile = !i.state.showSidebarMobile; i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
i.setState(i.state);
} }
handleCollapseSubscribe(i: Home) { handleCollapseSubscribe(i: Home) {
i.state.subscribedCollapsed = !i.state.subscribedCollapsed; i.setState({ subscribedCollapsed: !i.state.subscribedCollapsed });
i.setState(i.state);
} }
handlePageChange(page: number) { handlePageChange(page: number) {
@ -731,18 +743,14 @@ export class Home extends Component<any, HomeState> {
msg, msg,
ListCommunitiesResponse ListCommunitiesResponse
); );
this.state.trendingCommunities = data.communities; this.setState({ trendingCommunities: data.communities });
this.setState(this.state);
} else if (op == UserOperation.EditSite) { } else if (op == UserOperation.EditSite) {
let data = wsJsonToRes<SiteResponse>(msg, SiteResponse); let data = wsJsonToRes<SiteResponse>(msg, SiteResponse);
this.state.siteRes.site_view = Some(data.site_view); this.setState(s => ((s.siteRes.site_view = Some(data.site_view)), s));
this.setState(this.state);
toast(i18n.t("site_saved")); toast(i18n.t("site_saved"));
} else if (op == UserOperation.GetPosts) { } else if (op == UserOperation.GetPosts) {
let data = wsJsonToRes<GetPostsResponse>(msg, GetPostsResponse); let data = wsJsonToRes<GetPostsResponse>(msg, GetPostsResponse);
this.state.posts = data.posts; this.setState({ posts: data.posts, loading: false });
this.state.loading = false;
this.setState(this.state);
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.communityJoin({ community_id: 0 }) wsClient.communityJoin({ community_id: 0 })
); );
@ -750,21 +758,17 @@ export class Home extends Component<any, HomeState> {
setupTippy(); setupTippy();
} else if (op == UserOperation.CreatePost) { } else if (op == UserOperation.CreatePost) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg, PostResponse);
// NSFW check
let nsfw = data.post_view.post.nsfw || data.post_view.community.nsfw;
let nsfwCheck =
!nsfw ||
(nsfw &&
UserService.Instance.myUserInfo
.map(m => m.local_user_view.local_user.show_nsfw)
.unwrapOr(false));
let showPostNotifs = UserService.Instance.myUserInfo let showPostNotifs = UserService.Instance.myUserInfo
.map(m => m.local_user_view.local_user.show_new_post_notifs) .map(m => m.local_user_view.local_user.show_new_post_notifs)
.unwrapOr(false); .unwrapOr(false);
// Only push these if you're on the first page, and you pass the nsfw check // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
if (this.state.page == 1 && nsfwCheck) { if (
this.state.page == 1 &&
nsfwCheck(data.post_view) &&
!isPostBlocked(data.post_view)
) {
// If you're on subscribed, only push it if you're subscribed. // If you're on subscribed, only push it if you're subscribed.
if (this.state.listingType == ListingType.Subscribed) { if (this.state.listingType == ListingType.Subscribed) {
if ( if (
@ -812,8 +816,7 @@ export class Home extends Component<any, HomeState> {
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.AddAdmin) { } else if (op == UserOperation.AddAdmin) {
let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse); let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
this.state.siteRes.admins = data.admins; this.setState(s => ((s.siteRes.admins = data.admins), s));
this.setState(this.state);
} else if (op == UserOperation.BanPerson) { } else if (op == UserOperation.BanPerson) {
let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse); let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
this.state.posts this.state.posts
@ -823,9 +826,7 @@ export class Home extends Component<any, HomeState> {
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetComments) { } else if (op == UserOperation.GetComments) {
let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse); let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse);
this.state.comments = data.comments; this.setState({ comments: data.comments, loading: false });
this.state.loading = false;
this.setState(this.state);
} else if ( } else if (
op == UserOperation.EditComment || op == UserOperation.EditComment ||
op == UserOperation.DeleteComment || op == UserOperation.DeleteComment ||

View file

@ -30,22 +30,22 @@ export class Instances extends Component<any, InstancesState> {
render() { render() {
return this.state.siteRes.federated_instances.match({ return this.state.siteRes.federated_instances.match({
some: federated_instances => ( some: federated_instances => (
<div class="container"> <div className="container">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None} description={None}
image={None} image={None}
/> />
<div class="row"> <div className="row">
<div class="col-md-6"> <div className="col-md-6">
<h5>{i18n.t("linked_instances")}</h5> <h5>{i18n.t("linked_instances")}</h5>
{this.itemList(federated_instances.linked)} {this.itemList(federated_instances.linked)}
</div> </div>
{federated_instances.allowed.match({ {federated_instances.allowed.match({
some: allowed => some: allowed =>
allowed.length > 0 && ( allowed.length > 0 && (
<div class="col-md-6"> <div className="col-md-6">
<h5>{i18n.t("allowed_instances")}</h5> <h5>{i18n.t("allowed_instances")}</h5>
{this.itemList(allowed)} {this.itemList(allowed)}
</div> </div>
@ -55,7 +55,7 @@ export class Instances extends Component<any, InstancesState> {
{federated_instances.blocked.match({ {federated_instances.blocked.match({
some: blocked => some: blocked =>
blocked.length > 0 && ( blocked.length > 0 && (
<div class="col-md-6"> <div className="col-md-6">
<h5>{i18n.t("blocked_instances")}</h5> <h5>{i18n.t("blocked_instances")}</h5>
{this.itemList(blocked)} {this.itemList(blocked)}
</div> </div>
@ -74,7 +74,7 @@ export class Instances extends Component<any, InstancesState> {
return items.length > 0 ? ( return items.length > 0 ? (
<ul> <ul>
{items.map(i => ( {items.map(i => (
<li> <li key={i}>
<a href={`https://${i}`} rel={relTags}> <a href={`https://${i}`} rel={relTags}>
{i} {i}
</a> </a>

View file

@ -26,7 +26,7 @@ export class Legal extends Component<any, LegalState> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}

View file

@ -81,15 +81,15 @@ export class Login extends Component<any, State> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None} description={None}
image={None} image={None}
/> />
<div class="row"> <div className="row">
<div class="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</div> <div className="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</div>
</div> </div>
</div> </div>
); );
@ -100,17 +100,17 @@ export class Login extends Component<any, State> {
<div> <div>
<form onSubmit={linkEvent(this, this.handleLoginSubmit)}> <form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
<h5>{i18n.t("login")}</h5> <h5>{i18n.t("login")}</h5>
<div class="form-group row"> <div className="form-group row">
<label <label
class="col-sm-2 col-form-label" className="col-sm-2 col-form-label"
htmlFor="login-email-or-username" htmlFor="login-email-or-username"
> >
{i18n.t("email_or_username")} {i18n.t("email_or_username")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<input <input
type="text" type="text"
class="form-control" className="form-control"
id="login-email-or-username" id="login-email-or-username"
value={this.state.loginForm.username_or_email} value={this.state.loginForm.username_or_email}
onInput={linkEvent(this, this.handleLoginUsernameChange)} onInput={linkEvent(this, this.handleLoginUsernameChange)}
@ -120,17 +120,17 @@ export class Login extends Component<any, State> {
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="login-password"> <label className="col-sm-2 col-form-label" htmlFor="login-password">
{i18n.t("password")} {i18n.t("password")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<input <input
type="password" type="password"
id="login-password" id="login-password"
value={this.state.loginForm.password} value={this.state.loginForm.password}
onInput={linkEvent(this, this.handleLoginPasswordChange)} onInput={linkEvent(this, this.handleLoginPasswordChange)}
class="form-control" className="form-control"
autoComplete="current-password" autoComplete="current-password"
required required
maxLength={60} maxLength={60}
@ -146,9 +146,9 @@ export class Login extends Component<any, State> {
</button> </button>
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<div class="col-sm-10"> <div className="col-sm-10">
<button type="submit" class="btn btn-secondary"> <button type="submit" className="btn btn-secondary">
{this.state.loginLoading ? <Spinner /> : i18n.t("login")} {this.state.loginLoading ? <Spinner /> : i18n.t("login")}
</button> </button>
</div> </div>
@ -160,8 +160,7 @@ export class Login extends Component<any, State> {
handleLoginSubmit(i: Login, event: any) { handleLoginSubmit(i: Login, event: any) {
event.preventDefault(); event.preventDefault();
i.state.loginLoading = true; i.setState({ loginLoading: true });
i.setState(i.state);
WebSocketService.Instance.send(wsClient.login(i.state.loginForm)); WebSocketService.Instance.send(wsClient.login(i.state.loginForm));
} }
@ -188,21 +187,18 @@ export class Login extends Component<any, State> {
console.log(msg); console.log(msg);
if (msg.error) { if (msg.error) {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
this.state = this.emptyState; this.setState(this.emptyState);
this.setState(this.state);
return; return;
} else { } else {
if (op == UserOperation.Login) { if (op == UserOperation.Login) {
let data = wsJsonToRes<LoginResponse>(msg, LoginResponse); let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
this.state = this.emptyState; this.setState(this.emptyState);
this.setState(this.state);
UserService.Instance.login(data); UserService.Instance.login(data);
} else if (op == UserOperation.PasswordReset) { } else if (op == UserOperation.PasswordReset) {
toast(i18n.t("reset_password_mail_sent")); toast(i18n.t("reset_password_mail_sent"));
} else if (op == UserOperation.GetSite) { } else if (op == UserOperation.GetSite) {
let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse); let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
this.state.siteRes = data; this.setState({ siteRes: data });
this.setState(this.state);
} }
} }
} }

View file

@ -67,10 +67,10 @@ export class Setup extends Component<any, State> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
<Helmet title={this.documentTitle} /> <Helmet title={this.documentTitle} />
<div class="row"> <div className="row">
<div class="col-12 offset-lg-3 col-lg-6"> <div className="col-12 offset-lg-3 col-lg-6">
<h3>{i18n.t("lemmy_instance_setup")}</h3> <h3>{i18n.t("lemmy_instance_setup")}</h3>
{!this.state.doneRegisteringUser ? ( {!this.state.doneRegisteringUser ? (
this.registerUser() this.registerUser()
@ -87,14 +87,14 @@ export class Setup extends Component<any, State> {
return ( return (
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}> <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h5>{i18n.t("setup_admin")}</h5> <h5>{i18n.t("setup_admin")}</h5>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="username"> <label className="col-sm-2 col-form-label" htmlFor="username">
{i18n.t("username")} {i18n.t("username")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<input <input
type="text" type="text"
class="form-control" className="form-control"
id="username" id="username"
value={this.state.userForm.username} value={this.state.userForm.username}
onInput={linkEvent(this, this.handleRegisterUsernameChange)} onInput={linkEvent(this, this.handleRegisterUsernameChange)}
@ -104,16 +104,16 @@ export class Setup extends Component<any, State> {
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="email"> <label className="col-sm-2 col-form-label" htmlFor="email">
{i18n.t("email")} {i18n.t("email")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<input <input
type="email" type="email"
id="email" id="email"
class="form-control" className="form-control"
placeholder={i18n.t("optional")} placeholder={i18n.t("optional")}
value={toUndefined(this.state.userForm.email)} value={toUndefined(this.state.userForm.email)}
onInput={linkEvent(this, this.handleRegisterEmailChange)} onInput={linkEvent(this, this.handleRegisterEmailChange)}
@ -121,17 +121,17 @@ export class Setup extends Component<any, State> {
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="password"> <label className="col-sm-2 col-form-label" htmlFor="password">
{i18n.t("password")} {i18n.t("password")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<input <input
type="password" type="password"
id="password" id="password"
value={this.state.userForm.password} value={this.state.userForm.password}
onInput={linkEvent(this, this.handleRegisterPasswordChange)} onInput={linkEvent(this, this.handleRegisterPasswordChange)}
class="form-control" className="form-control"
required required
autoComplete="new-password" autoComplete="new-password"
minLength={10} minLength={10}
@ -139,17 +139,17 @@ export class Setup extends Component<any, State> {
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="verify-password"> <label className="col-sm-2 col-form-label" htmlFor="verify-password">
{i18n.t("verify_password")} {i18n.t("verify_password")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<input <input
type="password" type="password"
id="verify-password" id="verify-password"
value={this.state.userForm.password_verify} value={this.state.userForm.password_verify}
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
class="form-control" className="form-control"
required required
autoComplete="new-password" autoComplete="new-password"
minLength={10} minLength={10}
@ -157,9 +157,9 @@ export class Setup extends Component<any, State> {
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<div class="col-sm-10"> <div className="col-sm-10">
<button type="submit" class="btn btn-secondary"> <button type="submit" className="btn btn-secondary">
{this.state.userLoading ? <Spinner /> : i18n.t("sign_up")} {this.state.userLoading ? <Spinner /> : i18n.t("sign_up")}
</button> </button>
</div> </div>
@ -170,8 +170,7 @@ export class Setup extends Component<any, State> {
handleRegisterSubmit(i: Setup, event: any) { handleRegisterSubmit(i: Setup, event: any) {
event.preventDefault(); event.preventDefault();
i.state.userLoading = true; i.setState({ userLoading: true });
i.setState(i.state);
event.preventDefault(); event.preventDefault();
WebSocketService.Instance.send(wsClient.register(i.state.userForm)); WebSocketService.Instance.send(wsClient.register(i.state.userForm));
} }
@ -200,14 +199,12 @@ export class Setup extends Component<any, State> {
let op = wsUserOp(msg); let op = wsUserOp(msg);
if (msg.error) { if (msg.error) {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
this.state.userLoading = false; this.setState({ userLoading: false });
this.setState(this.state);
return; return;
} else if (op == UserOperation.Register) { } else if (op == UserOperation.Register) {
let data = wsJsonToRes<LoginResponse>(msg, LoginResponse); let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
this.state.userLoading = false; this.setState({ userLoading: false });
UserService.Instance.login(data); UserService.Instance.login(data);
this.setState(this.state);
} else if (op == UserOperation.CreateSite) { } else if (op == UserOperation.CreateSite) {
window.location.href = "/"; window.location.href = "/";
} }

View file

@ -127,15 +127,17 @@ export class Signup extends Component<any, State> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None} description={None}
image={None} image={None}
/> />
<div class="row"> <div className="row">
<div class="col-12 col-lg-6 offset-lg-3">{this.registerForm()}</div> <div className="col-12 col-lg-6 offset-lg-3">
{this.registerForm()}
</div>
</div> </div>
</div> </div>
); );
@ -148,8 +150,8 @@ export class Signup extends Component<any, State> {
<h5>{this.titleName(siteView)}</h5> <h5>{this.titleName(siteView)}</h5>
{this.isLemmyMl && ( {this.isLemmyMl && (
<div class="form-group row"> <div className="form-group row">
<div class="mt-2 mb-0 alert alert-warning" role="alert"> <div className="mt-2 mb-0 alert alert-warning" role="alert">
<T i18nKey="lemmy_ml_registration_message"> <T i18nKey="lemmy_ml_registration_message">
#<a href={joinLemmyUrl}>#</a> #<a href={joinLemmyUrl}>#</a>
</T> </T>
@ -157,16 +159,19 @@ export class Signup extends Component<any, State> {
</div> </div>
)} )}
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-username"> <label
className="col-sm-2 col-form-label"
htmlFor="register-username"
>
{i18n.t("username")} {i18n.t("username")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<input <input
type="text" type="text"
id="register-username" id="register-username"
class="form-control" className="form-control"
value={this.state.registerForm.username} value={this.state.registerForm.username}
onInput={linkEvent(this, this.handleRegisterUsernameChange)} onInput={linkEvent(this, this.handleRegisterUsernameChange)}
required required
@ -177,15 +182,15 @@ export class Signup extends Component<any, State> {
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-email"> <label className="col-sm-2 col-form-label" htmlFor="register-email">
{i18n.t("email")} {i18n.t("email")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<input <input
type="email" type="email"
id="register-email" id="register-email"
class="form-control" className="form-control"
placeholder={ placeholder={
siteView.site.require_email_verification siteView.site.require_email_verification
? i18n.t("required") ? i18n.t("required")
@ -201,7 +206,7 @@ export class Signup extends Component<any, State> {
!this.state.registerForm.email !this.state.registerForm.email
.map(validEmail) .map(validEmail)
.unwrapOr(true) && ( .unwrapOr(true) && (
<div class="mt-2 mb-0 alert alert-warning" role="alert"> <div className="mt-2 mb-0 alert alert-warning" role="alert">
<Icon icon="alert-triangle" classes="icon-inline mr-2" /> <Icon icon="alert-triangle" classes="icon-inline mr-2" />
{i18n.t("no_password_reset")} {i18n.t("no_password_reset")}
</div> </div>
@ -209,11 +214,14 @@ export class Signup extends Component<any, State> {
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-password"> <label
className="col-sm-2 col-form-label"
htmlFor="register-password"
>
{i18n.t("password")} {i18n.t("password")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<input <input
type="password" type="password"
id="register-password" id="register-password"
@ -222,25 +230,25 @@ export class Signup extends Component<any, State> {
onInput={linkEvent(this, this.handleRegisterPasswordChange)} onInput={linkEvent(this, this.handleRegisterPasswordChange)}
minLength={10} minLength={10}
maxLength={60} maxLength={60}
class="form-control" className="form-control"
required required
/> />
{this.state.registerForm.password && ( {this.state.registerForm.password && (
<div class={this.passwordColorClass}> <div className={this.passwordColorClass}>
{i18n.t(this.passwordStrength as I18nKeys)} {i18n.t(this.passwordStrength as I18nKeys)}
</div> </div>
)} )}
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label <label
class="col-sm-2 col-form-label" className="col-sm-2 col-form-label"
htmlFor="register-verify-password" htmlFor="register-verify-password"
> >
{i18n.t("verify_password")} {i18n.t("verify_password")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<input <input
type="password" type="password"
id="register-verify-password" id="register-verify-password"
@ -251,7 +259,7 @@ export class Signup extends Component<any, State> {
this.handleRegisterPasswordVerifyChange this.handleRegisterPasswordVerifyChange
)} )}
maxLength={60} maxLength={60}
class="form-control" className="form-control"
required required
/> />
</div> </div>
@ -259,9 +267,9 @@ export class Signup extends Component<any, State> {
{siteView.site.require_application && ( {siteView.site.require_application && (
<> <>
<div class="form-group row"> <div className="form-group row">
<div class="offset-sm-2 col-sm-10"> <div className="offset-sm-2 col-sm-10">
<div class="mt-2 alert alert-warning" role="alert"> <div className="mt-2 alert alert-warning" role="alert">
<Icon icon="alert-triangle" classes="icon-inline mr-2" /> <Icon icon="alert-triangle" classes="icon-inline mr-2" />
{i18n.t("fill_out_application")} {i18n.t("fill_out_application")}
</div> </div>
@ -277,21 +285,23 @@ export class Signup extends Component<any, State> {
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label <label
class="col-sm-2 col-form-label" className="col-sm-2 col-form-label"
htmlFor="application_answer" htmlFor="application_answer"
> >
{i18n.t("answer")} {i18n.t("answer")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<MarkdownTextArea <MarkdownTextArea
initialContent={None} initialContent={None}
initialLanguageId={None}
placeholder={None} placeholder={None}
buttonTitle={None} buttonTitle={None}
maxLength={None} maxLength={None}
onContentChange={this.handleAnswerChange} onContentChange={this.handleAnswerChange}
hideNavigationWarnings hideNavigationWarnings
allLanguages={[]}
/> />
</div> </div>
</div> </div>
@ -299,12 +309,12 @@ export class Signup extends Component<any, State> {
)} )}
{this.state.captcha.isSome() && ( {this.state.captcha.isSome() && (
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2" htmlFor="register-captcha"> <label className="col-sm-2" htmlFor="register-captcha">
<span class="mr-2">{i18n.t("enter_code")}</span> <span className="mr-2">{i18n.t("enter_code")}</span>
<button <button
type="button" type="button"
class="btn btn-secondary" className="btn btn-secondary"
onClick={linkEvent(this, this.handleRegenCaptcha)} onClick={linkEvent(this, this.handleRegenCaptcha)}
aria-label={i18n.t("captcha")} aria-label={i18n.t("captcha")}
> >
@ -312,10 +322,10 @@ export class Signup extends Component<any, State> {
</button> </button>
</label> </label>
{this.showCaptcha()} {this.showCaptcha()}
<div class="col-sm-6"> <div className="col-sm-6">
<input <input
type="text" type="text"
class="form-control" className="form-control"
id="register-captcha" id="register-captcha"
value={toUndefined(this.state.registerForm.captcha_answer)} value={toUndefined(this.state.registerForm.captcha_answer)}
onInput={linkEvent( onInput={linkEvent(
@ -328,11 +338,11 @@ export class Signup extends Component<any, State> {
</div> </div>
)} )}
{siteView.site.enable_nsfw && ( {siteView.site.enable_nsfw && (
<div class="form-group row"> <div className="form-group row">
<div class="col-sm-10"> <div className="col-sm-10">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="register-show-nsfw" id="register-show-nsfw"
type="checkbox" type="checkbox"
checked={this.state.registerForm.show_nsfw} checked={this.state.registerForm.show_nsfw}
@ -341,7 +351,10 @@ export class Signup extends Component<any, State> {
this.handleRegisterShowNsfwChange this.handleRegisterShowNsfwChange
)} )}
/> />
<label class="form-check-label" htmlFor="register-show-nsfw"> <label
className="form-check-label"
htmlFor="register-show-nsfw"
>
{i18n.t("show_nsfw")} {i18n.t("show_nsfw")}
</label> </label>
</div> </div>
@ -353,14 +366,14 @@ export class Signup extends Component<any, State> {
autoComplete="false" autoComplete="false"
name="a_password" name="a_password"
type="text" type="text"
class="form-control honeypot" className="form-control honeypot"
id="register-honey" id="register-honey"
value={toUndefined(this.state.registerForm.honeypot)} value={toUndefined(this.state.registerForm.honeypot)}
onInput={linkEvent(this, this.handleHoneyPotChange)} onInput={linkEvent(this, this.handleHoneyPotChange)}
/> />
<div class="form-group row"> <div className="form-group row">
<div class="col-sm-10"> <div className="col-sm-10">
<button type="submit" class="btn btn-secondary"> <button type="submit" className="btn btn-secondary">
{this.state.registerLoading ? ( {this.state.registerLoading ? (
<Spinner /> <Spinner />
) : ( ) : (
@ -378,19 +391,19 @@ export class Signup extends Component<any, State> {
showCaptcha() { showCaptcha() {
return this.state.captcha.match({ return this.state.captcha.match({
some: captcha => ( some: captcha => (
<div class="col-sm-4"> <div className="col-sm-4">
{captcha.ok.match({ {captcha.ok.match({
some: res => ( some: res => (
<> <>
<img <img
class="rounded-top img-fluid" className="rounded-top img-fluid"
src={this.captchaPngSrc(res)} src={this.captchaPngSrc(res)}
style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;" style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
alt={i18n.t("captcha")} alt={i18n.t("captcha")}
/> />
{res.wav.isSome() && ( {res.wav.isSome() && (
<button <button
class="rounded-bottom btn btn-sm btn-secondary btn-block" className="rounded-bottom btn btn-sm btn-secondary btn-block"
style="border-top-right-radius: 0; border-top-left-radius: 0;" style="border-top-right-radius: 0; border-top-left-radius: 0;"
title={i18n.t("play_captcha_audio")} title={i18n.t("play_captcha_audio")}
onClick={linkEvent(this, this.handleCaptchaPlay)} onClick={linkEvent(this, this.handleCaptchaPlay)}
@ -431,8 +444,7 @@ export class Signup extends Component<any, State> {
handleRegisterSubmit(i: Signup, event: any) { handleRegisterSubmit(i: Signup, event: any) {
event.preventDefault(); event.preventDefault();
i.state.registerLoading = true; i.setState({ registerLoading: true });
i.setState(i.state);
WebSocketService.Instance.send(wsClient.register(i.state.registerForm)); WebSocketService.Instance.send(wsClient.register(i.state.registerForm));
} }
@ -470,8 +482,7 @@ export class Signup extends Component<any, State> {
} }
handleAnswerChange(val: string) { handleAnswerChange(val: string) {
this.state.registerForm.answer = Some(val); this.setState(s => ((s.registerForm.answer = Some(val)), s));
this.setState(this.state);
} }
handleHoneyPotChange(i: Signup, event: any) { handleHoneyPotChange(i: Signup, event: any) {
@ -481,8 +492,7 @@ export class Signup extends Component<any, State> {
handleRegenCaptcha(i: Signup) { handleRegenCaptcha(i: Signup) {
i.audio = null; i.audio = null;
i.state.captchaPlaying = false; i.setState({ captchaPlaying: false });
i.setState(i.state);
WebSocketService.Instance.send(wsClient.getCaptcha()); WebSocketService.Instance.send(wsClient.getCaptcha());
} }
@ -500,13 +510,11 @@ export class Signup extends Component<any, State> {
i.audio.play(); i.audio.play();
i.state.captchaPlaying = true; i.setState({ captchaPlaying: true });
i.setState(i.state);
i.audio.addEventListener("ended", () => { i.audio.addEventListener("ended", () => {
i.audio.currentTime = 0; i.audio.currentTime = 0;
i.state.captchaPlaying = false; i.setState({ captchaPlaying: false });
i.setState(i.state);
}); });
}, },
none: void 0, none: void 0,
@ -524,17 +532,15 @@ export class Signup extends Component<any, State> {
console.log(msg); console.log(msg);
if (msg.error) { if (msg.error) {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
this.state = this.emptyState; this.setState(this.emptyState);
this.state.registerForm.captcha_answer = undefined; this.setState(s => ((s.registerForm.captcha_answer = undefined), s));
// Refetch another captcha // Refetch another captcha
// WebSocketService.Instance.send(wsClient.getCaptcha()); // WebSocketService.Instance.send(wsClient.getCaptcha());
this.setState(this.state);
return; return;
} else { } else {
if (op == UserOperation.Register) { if (op == UserOperation.Register) {
let data = wsJsonToRes<LoginResponse>(msg, LoginResponse); let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
this.state = this.emptyState; this.setState(this.emptyState);
this.setState(this.state);
// Only log them in if a jwt was set // Only log them in if a jwt was set
if (data.jwt.isSome()) { if (data.jwt.isSome()) {
UserService.Instance.login(data); UserService.Instance.login(data);
@ -552,9 +558,10 @@ export class Signup extends Component<any, State> {
let data = wsJsonToRes<GetCaptchaResponse>(msg, GetCaptchaResponse); let data = wsJsonToRes<GetCaptchaResponse>(msg, GetCaptchaResponse);
data.ok.match({ data.ok.match({
some: res => { some: res => {
this.state.captcha = Some(data); this.setState({ captcha: Some(data) });
this.state.registerForm.captcha_uuid = Some(res.uuid); this.setState(
this.setState(this.state); s => ((s.registerForm.captcha_uuid = Some(res.uuid)), s)
);
}, },
none: void 0, none: void 0,
}); });
@ -562,8 +569,7 @@ export class Signup extends Component<any, State> {
toast(i18n.t("reset_password_mail_sent")); toast(i18n.t("reset_password_mail_sent"));
} else if (op == UserOperation.GetSite) { } else if (op == UserOperation.GetSite) {
let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse); let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
this.state.siteRes = data; this.setState({ siteRes: data });
this.setState(this.state);
} }
} }
} }

View file

@ -53,6 +53,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
legal_information: None, legal_information: None,
description: None, description: None,
community_creation_admin_only: None, community_creation_admin_only: None,
application_email_admins: None,
auth: undefined, auth: undefined,
hide_modlog_mod_names: Some(true), hide_modlog_mod_names: Some(true),
}), }),
@ -78,9 +79,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
this.handleDefaultPostListingTypeChange = this.handleDefaultPostListingTypeChange =
this.handleDefaultPostListingTypeChange.bind(this); this.handleDefaultPostListingTypeChange.bind(this);
this.props.site.match({ if (this.props.site.isSome()) {
some: site => { let site = this.props.site.unwrap();
this.state.siteForm = new EditSite({ this.state = {
...this.state,
siteForm: new EditSite({
name: Some(site.name), name: Some(site.name),
sidebar: site.sidebar, sidebar: site.sidebar,
description: site.description, description: site.description,
@ -99,23 +102,21 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
default_theme: Some(site.default_theme), default_theme: Some(site.default_theme),
default_post_listing_type: Some(site.default_post_listing_type), default_post_listing_type: Some(site.default_post_listing_type),
legal_information: site.legal_information, legal_information: site.legal_information,
application_email_admins: Some(site.application_email_admins),
hide_modlog_mod_names: site.hide_modlog_mod_names, hide_modlog_mod_names: site.hide_modlog_mod_names,
auth: undefined, auth: undefined,
}); }),
}, };
none: void 0, }
});
} }
async componentDidMount() { async componentDidMount() {
this.state.themeList = Some(await fetchThemeList()); this.setState({ themeList: Some(await fetchThemeList()) });
this.setState(this.state);
} }
// Necessary to stop the loading // Necessary to stop the loading
componentWillReceiveProps() { componentWillReceiveProps() {
this.state.loading = false; this.setState({ loading: false });
this.setState(this.state);
} }
componentDidUpdate() { componentDidUpdate() {
@ -157,15 +158,15 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
? capitalizeFirstLetter(i18n.t("save")) ? capitalizeFirstLetter(i18n.t("save"))
: capitalizeFirstLetter(i18n.t("name")) : capitalizeFirstLetter(i18n.t("name"))
} ${i18n.t("your_site")}`}</h5> } ${i18n.t("your_site")}`}</h5>
<div class="form-group row"> <div className="form-group row">
<label class="col-12 col-form-label" htmlFor="create-site-name"> <label className="col-12 col-form-label" htmlFor="create-site-name">
{i18n.t("name")} {i18n.t("name")}
</label> </label>
<div class="col-12"> <div className="col-12">
<input <input
type="text" type="text"
id="create-site-name" id="create-site-name"
class="form-control" className="form-control"
value={toUndefined(this.state.siteForm.name)} value={toUndefined(this.state.siteForm.name)}
onInput={linkEvent(this, this.handleSiteNameChange)} onInput={linkEvent(this, this.handleSiteNameChange)}
required required
@ -174,7 +175,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
/> />
</div> </div>
</div> </div>
<div class="form-group"> <div className="form-group">
<label>{i18n.t("icon")}</label> <label>{i18n.t("icon")}</label>
<ImageUploadForm <ImageUploadForm
uploadTitle={i18n.t("upload_icon")} uploadTitle={i18n.t("upload_icon")}
@ -184,7 +185,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
rounded rounded
/> />
</div> </div>
<div class="form-group"> <div className="form-group">
<label>{i18n.t("banner")}</label> <label>{i18n.t("banner")}</label>
<ImageUploadForm <ImageUploadForm
uploadTitle={i18n.t("upload_banner")} uploadTitle={i18n.t("upload_banner")}
@ -193,14 +194,14 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
onRemove={this.handleBannerRemove} onRemove={this.handleBannerRemove}
/> />
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-12 col-form-label" htmlFor="site-desc"> <label className="col-12 col-form-label" htmlFor="site-desc">
{i18n.t("description")} {i18n.t("description")}
</label> </label>
<div class="col-12"> <div className="col-12">
<input <input
type="text" type="text"
class="form-control" className="form-control"
id="site-desc" id="site-desc"
value={toUndefined(this.state.siteForm.description)} value={toUndefined(this.state.siteForm.description)}
onInput={linkEvent(this, this.handleSiteDescChange)} onInput={linkEvent(this, this.handleSiteDescChange)}
@ -208,56 +209,62 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-12 col-form-label">{i18n.t("sidebar")}</label> <label className="col-12 col-form-label">{i18n.t("sidebar")}</label>
<div class="col-12"> <div className="col-12">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.siteForm.sidebar} initialContent={this.state.siteForm.sidebar}
initialLanguageId={None}
placeholder={None} placeholder={None}
buttonTitle={None} buttonTitle={None}
maxLength={None} maxLength={None}
onContentChange={this.handleSiteSidebarChange} onContentChange={this.handleSiteSidebarChange}
hideNavigationWarnings hideNavigationWarnings
allLanguages={[]}
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-12 col-form-label"> <label className="col-12 col-form-label">
{i18n.t("legal_information")} {i18n.t("legal_information")}
</label> </label>
<div class="col-12"> <div className="col-12">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.siteForm.legal_information} initialContent={this.state.siteForm.legal_information}
initialLanguageId={None}
placeholder={None} placeholder={None}
buttonTitle={None} buttonTitle={None}
maxLength={None} maxLength={None}
onContentChange={this.handleSiteLegalInfoChange} onContentChange={this.handleSiteLegalInfoChange}
hideNavigationWarnings hideNavigationWarnings
allLanguages={[]}
/> />
</div> </div>
</div> </div>
{this.state.siteForm.require_application.unwrapOr(false) && ( {this.state.siteForm.require_application.unwrapOr(false) && (
<div class="form-group row"> <div className="form-group row">
<label class="col-12 col-form-label"> <label className="col-12 col-form-label">
{i18n.t("application_questionnaire")} {i18n.t("application_questionnaire")}
</label> </label>
<div class="col-12"> <div className="col-12">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.siteForm.application_question} initialContent={this.state.siteForm.application_question}
initialLanguageId={None}
placeholder={None} placeholder={None}
buttonTitle={None} buttonTitle={None}
maxLength={None} maxLength={None}
onContentChange={this.handleSiteApplicationQuestionChange} onContentChange={this.handleSiteApplicationQuestionChange}
hideNavigationWarnings hideNavigationWarnings
allLanguages={[]}
/> />
</div> </div>
</div> </div>
)} )}
<div class="form-group row"> <div className="form-group row">
<div class="col-12"> <div className="col-12">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="create-site-downvotes" id="create-site-downvotes"
type="checkbox" type="checkbox"
checked={toUndefined(this.state.siteForm.enable_downvotes)} checked={toUndefined(this.state.siteForm.enable_downvotes)}
@ -266,24 +273,27 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
this.handleSiteEnableDownvotesChange this.handleSiteEnableDownvotesChange
)} )}
/> />
<label class="form-check-label" htmlFor="create-site-downvotes"> <label
className="form-check-label"
htmlFor="create-site-downvotes"
>
{i18n.t("enable_downvotes")} {i18n.t("enable_downvotes")}
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<div class="col-12"> <div className="col-12">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="create-site-enable-nsfw" id="create-site-enable-nsfw"
type="checkbox" type="checkbox"
checked={toUndefined(this.state.siteForm.enable_nsfw)} checked={toUndefined(this.state.siteForm.enable_nsfw)}
onChange={linkEvent(this, this.handleSiteEnableNsfwChange)} onChange={linkEvent(this, this.handleSiteEnableNsfwChange)}
/> />
<label <label
class="form-check-label" className="form-check-label"
htmlFor="create-site-enable-nsfw" htmlFor="create-site-enable-nsfw"
> >
{i18n.t("enable_nsfw")} {i18n.t("enable_nsfw")}
@ -291,11 +301,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<div class="col-12"> <div className="col-12">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="create-site-open-registration" id="create-site-open-registration"
type="checkbox" type="checkbox"
checked={toUndefined(this.state.siteForm.open_registration)} checked={toUndefined(this.state.siteForm.open_registration)}
@ -305,7 +315,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
)} )}
/> />
<label <label
class="form-check-label" className="form-check-label"
htmlFor="create-site-open-registration" htmlFor="create-site-open-registration"
> >
{i18n.t("open_registration")} {i18n.t("open_registration")}
@ -313,11 +323,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<div class="col-12"> <div className="col-12">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="create-site-community-creation-admin-only" id="create-site-community-creation-admin-only"
type="checkbox" type="checkbox"
checked={toUndefined( checked={toUndefined(
@ -329,7 +339,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
)} )}
/> />
<label <label
class="form-check-label" className="form-check-label"
htmlFor="create-site-community-creation-admin-only" htmlFor="create-site-community-creation-admin-only"
> >
{i18n.t("community_creation_admin_only")} {i18n.t("community_creation_admin_only")}
@ -337,11 +347,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<div class="col-12"> <div className="col-12">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="create-site-require-email-verification" id="create-site-require-email-verification"
type="checkbox" type="checkbox"
checked={toUndefined( checked={toUndefined(
@ -353,7 +363,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
)} )}
/> />
<label <label
class="form-check-label" className="form-check-label"
htmlFor="create-site-require-email-verification" htmlFor="create-site-require-email-verification"
> >
{i18n.t("require_email_verification")} {i18n.t("require_email_verification")}
@ -361,18 +371,18 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<div class="col-12"> <div className="col-12">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="create-site-require-application" id="create-site-require-application"
type="checkbox" type="checkbox"
checked={toUndefined(this.state.siteForm.require_application)} checked={toUndefined(this.state.siteForm.require_application)}
onChange={linkEvent(this, this.handleSiteRequireApplication)} onChange={linkEvent(this, this.handleSiteRequireApplication)}
/> />
<label <label
class="form-check-label" className="form-check-label"
htmlFor="create-site-require-application" htmlFor="create-site-require-application"
> >
{i18n.t("require_registration_application")} {i18n.t("require_registration_application")}
@ -380,10 +390,34 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<div class="col-12"> <div className="col-12">
<div className="form-check">
<input
className="form-check-input"
id="create-site-application-email-admins"
type="checkbox"
checked={toUndefined(
this.state.siteForm.application_email_admins
)}
onChange={linkEvent(
this,
this.handleSiteApplicationEmailAdmins
)}
/>
<label
className="form-check-label"
htmlFor="create-site-email-admins"
>
{i18n.t("application_email_admins")}
</label>
</div>
</div>
</div>
<div className="form-group row">
<div className="col-12">
<label <label
class="form-check-label mr-2" className="form-check-label mr-2"
htmlFor="create-site-default-theme" htmlFor="create-site-default-theme"
> >
{i18n.t("theme")} {i18n.t("theme")}
@ -392,19 +426,21 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
id="create-site-default-theme" id="create-site-default-theme"
value={toUndefined(this.state.siteForm.default_theme)} value={toUndefined(this.state.siteForm.default_theme)}
onChange={linkEvent(this, this.handleSiteDefaultTheme)} onChange={linkEvent(this, this.handleSiteDefaultTheme)}
class="custom-select w-auto" className="custom-select w-auto"
> >
<option value="browser">{i18n.t("browser_default")}</option> <option value="browser">{i18n.t("browser_default")}</option>
{this.state.themeList.unwrapOr([]).map(theme => ( {this.state.themeList.unwrapOr([]).map(theme => (
<option value={theme}>{theme}</option> <option key={theme} value={theme}>
{theme}
</option>
))} ))}
</select> </select>
</div> </div>
</div> </div>
{this.props.showLocal && ( {this.props.showLocal && (
<form className="form-group row"> <form className="form-group row">
<label class="col-sm-3">{i18n.t("listing_type")}</label> <label className="col-sm-3">{i18n.t("listing_type")}</label>
<div class="col-sm-9"> <div className="col-sm-9">
<ListingTypeSelect <ListingTypeSelect
type_={ type_={
ListingType[ ListingType[
@ -420,18 +456,18 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</div> </div>
</form> </form>
)} )}
<div class="form-group row"> <div className="form-group row">
<div class="col-12"> <div className="col-12">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="create-site-private-instance" id="create-site-private-instance"
type="checkbox" type="checkbox"
value={toUndefined(this.state.siteForm.default_theme)} checked={toUndefined(this.state.siteForm.private_instance)}
onChange={linkEvent(this, this.handleSitePrivateInstance)} onChange={linkEvent(this, this.handleSitePrivateInstance)}
/> />
<label <label
class="form-check-label" className="form-check-label"
htmlFor="create-site-private-instance" htmlFor="create-site-private-instance"
> >
{i18n.t("private_instance")} {i18n.t("private_instance")}
@ -439,11 +475,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<div class="col-12"> <div className="col-12">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="create-site-hide-modlog-mod-names" id="create-site-hide-modlog-mod-names"
type="checkbox" type="checkbox"
checked={toUndefined( checked={toUndefined(
@ -452,7 +488,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
onChange={linkEvent(this, this.handleSiteHideModlogModNames)} onChange={linkEvent(this, this.handleSiteHideModlogModNames)}
/> />
<label <label
class="form-check-label" className="form-check-label"
htmlFor="create-site-hide-modlog-mod-names" htmlFor="create-site-hide-modlog-mod-names"
> >
{i18n.t("hide_modlog_mod_names")} {i18n.t("hide_modlog_mod_names")}
@ -460,11 +496,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<div class="col-12"> <div className="col-12">
<button <button
type="submit" type="submit"
class="btn btn-secondary mr-2" className="btn btn-secondary mr-2"
disabled={this.state.loading} disabled={this.state.loading}
> >
{this.state.loading ? ( {this.state.loading ? (
@ -478,7 +514,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
{this.props.site.isSome() && ( {this.props.site.isSome() && (
<button <button
type="button" type="button"
class="btn btn-secondary" className="btn btn-secondary"
onClick={linkEvent(this, this.handleCancel)} onClick={linkEvent(this, this.handleCancel)}
> >
{i18n.t("cancel")} {i18n.t("cancel")}
@ -493,8 +529,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
handleCreateSiteSubmit(i: SiteForm, event: any) { handleCreateSiteSubmit(i: SiteForm, event: any) {
event.preventDefault(); event.preventDefault();
i.state.loading = true; i.setState({ loading: true });
i.state.siteForm.auth = auth().unwrap(); i.setState(s => ((s.siteForm.auth = auth().unwrap()), s));
if (i.props.site.isSome()) { if (i.props.site.isSome()) {
WebSocketService.Instance.send(wsClient.editSite(i.state.siteForm)); WebSocketService.Instance.send(wsClient.editSite(i.state.siteForm));
@ -517,6 +553,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
private_instance: sForm.private_instance, private_instance: sForm.private_instance,
default_theme: sForm.default_theme, default_theme: sForm.default_theme,
default_post_listing_type: sForm.default_post_listing_type, default_post_listing_type: sForm.default_post_listing_type,
application_email_admins: sForm.application_email_admins,
auth: auth().unwrap(), auth: auth().unwrap(),
hide_modlog_mod_names: sForm.hide_modlog_mod_names, hide_modlog_mod_names: sForm.hide_modlog_mod_names,
}); });
@ -531,18 +568,15 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
} }
handleSiteSidebarChange(val: string) { handleSiteSidebarChange(val: string) {
this.state.siteForm.sidebar = Some(val); this.setState(s => ((s.siteForm.sidebar = Some(val)), s));
this.setState(this.state);
} }
handleSiteLegalInfoChange(val: string) { handleSiteLegalInfoChange(val: string) {
this.state.siteForm.legal_information = Some(val); this.setState(s => ((s.siteForm.legal_information = Some(val)), s));
this.setState(this.state);
} }
handleSiteApplicationQuestionChange(val: string) { handleSiteApplicationQuestionChange(val: string) {
this.state.siteForm.application_question = Some(val); this.setState(s => ((s.siteForm.application_question = Some(val)), s));
this.setState(this.state);
} }
handleSiteDescChange(i: SiteForm, event: any) { handleSiteDescChange(i: SiteForm, event: any) {
@ -580,6 +614,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
i.setState(i.state); i.setState(i.state);
} }
handleSiteApplicationEmailAdmins(i: SiteForm, event: any) {
i.state.siteForm.application_email_admins = Some(event.target.checked);
i.setState(i.state);
}
handleSitePrivateInstance(i: SiteForm, event: any) { handleSitePrivateInstance(i: SiteForm, event: any) {
i.state.siteForm.private_instance = Some(event.target.checked); i.state.siteForm.private_instance = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
@ -600,29 +639,29 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
} }
handleIconUpload(url: string) { handleIconUpload(url: string) {
this.state.siteForm.icon = Some(url); this.setState(s => ((s.siteForm.icon = Some(url)), s));
this.setState(this.state);
} }
handleIconRemove() { handleIconRemove() {
this.state.siteForm.icon = Some(""); this.setState(s => ((s.siteForm.icon = Some("")), s));
this.setState(this.state);
} }
handleBannerUpload(url: string) { handleBannerUpload(url: string) {
this.state.siteForm.banner = Some(url); this.setState(s => ((s.siteForm.banner = Some(url)), s));
this.setState(this.state);
} }
handleBannerRemove() { handleBannerRemove() {
this.state.siteForm.banner = Some(""); this.setState(s => ((s.siteForm.banner = Some("")), s));
this.setState(this.state);
} }
handleDefaultPostListingTypeChange(val: ListingType) { handleDefaultPostListingTypeChange(val: ListingType) {
this.state.siteForm.default_post_listing_type = Some( this.setState(
ListingType[ListingType[val]] s => (
(s.siteForm.default_post_listing_type = Some(
ListingType[ListingType[val]]
)),
s
)
); );
this.setState(this.state);
} }
} }

View file

@ -38,11 +38,11 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
render() { render() {
let site = this.props.site; let site = this.props.site;
return ( return (
<div class="card border-secondary mb-3"> <div className="card border-secondary mb-3">
<div class="card-body"> <div className="card-body">
{!this.state.showEdit ? ( {!this.state.showEdit ? (
<div> <div>
<div class="mb-2"> <div className="mb-2">
{this.siteName()} {this.siteName()}
{this.props.admins.isSome() && this.adminButtons()} {this.props.admins.isSome() && this.adminButtons()}
</div> </div>
@ -69,10 +69,10 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
siteName() { siteName() {
let site = this.props.site; let site = this.props.site;
return ( return (
<h5 class="mb-0 d-inline"> <h5 className="mb-0 d-inline">
{site.name} {site.name}
<button <button
class="btn btn-sm text-muted" className="btn btn-sm text-muted"
onClick={linkEvent(this, this.handleCollapseSidebar)} onClick={linkEvent(this, this.handleCollapseSidebar)}
aria-label={i18n.t("collapse")} aria-label={i18n.t("collapse")}
data-tippy-content={i18n.t("collapse")} data-tippy-content={i18n.t("collapse")}
@ -113,11 +113,11 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
adminButtons() { adminButtons() {
return ( return (
amAdmin(this.props.admins) && ( amAdmin() && (
<ul class="list-inline mb-1 text-muted font-weight-bold"> <ul className="list-inline mb-1 text-muted font-weight-bold">
<li className="list-inline-item-action"> <li className="list-inline-item-action">
<button <button
class="btn btn-link d-inline-block text-muted" className="btn btn-link d-inline-block text-muted"
onClick={linkEvent(this, this.handleEditClick)} onClick={linkEvent(this, this.handleEditClick)}
aria-label={i18n.t("edit")} aria-label={i18n.t("edit")}
data-tippy-content={i18n.t("edit")} data-tippy-content={i18n.t("edit")}
@ -138,10 +138,10 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
admins(admins: PersonViewSafe[]) { admins(admins: PersonViewSafe[]) {
return ( return (
<ul class="mt-1 list-inline small mb-0"> <ul className="mt-1 list-inline small mb-0">
<li class="list-inline-item">{i18n.t("admins")}:</li> <li className="list-inline-item">{i18n.t("admins")}:</li>
{admins.map(av => ( {admins.map(av => (
<li class="list-inline-item"> <li key={av.person.id} className="list-inline-item">
<PersonListing person={av.person} /> <PersonListing person={av.person} />
</li> </li>
))} ))}
@ -153,7 +153,7 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
let counts = siteAggregates; let counts = siteAggregates;
let online = this.props.online.unwrapOr(1); let online = this.props.online.unwrapOr(1);
return ( return (
<ul class="my-2 list-inline"> <ul className="my-2 list-inline">
<li className="list-inline-item badge badge-secondary"> <li className="list-inline-item badge badge-secondary">
{i18n.t("number_online", { {i18n.t("number_online", {
count: online, count: online,
@ -246,22 +246,18 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
} }
handleCollapseSidebar(i: SiteSidebar) { handleCollapseSidebar(i: SiteSidebar) {
i.state.collapsed = !i.state.collapsed; i.setState({ collapsed: !i.state.collapsed });
i.setState(i.state);
} }
handleEditClick(i: SiteSidebar) { handleEditClick(i: SiteSidebar) {
i.state.showEdit = true; i.setState({ showEdit: true });
i.setState(i.state);
} }
handleEditSite() { handleEditSite() {
this.state.showEdit = false; this.setState({ showEdit: false });
this.setState(this.state);
} }
handleEditCancel() { handleEditCancel() {
this.state.showEdit = false; this.setState({ showEdit: false });
this.setState(this.state);
} }
} }

View file

@ -38,7 +38,7 @@ import {
amAdmin, amAdmin,
amMod, amMod,
auth, auth,
choicesModLogConfig, choicesConfig,
debounce, debounce,
fetchLimit, fetchLimit,
fetchUsers, fetchUsers,
@ -114,31 +114,41 @@ export class Modlog extends Component<any, ModlogState> {
filter_user: None, filter_user: None,
filter_mod: None, filter_mod: None,
}; };
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.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
this.state.communityId = this.props.match.params.community_id
? Some(Number(this.props.match.params.community_id))
: None;
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
this.state = {
...this.state,
communityId: this.props.match.params.community_id
? Some(Number(this.props.match.params.community_id))
: None,
};
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state.res = Some(this.isoData.routeData[0] as GetModlogResponse); this.state = {
...this.state,
res: Some(this.isoData.routeData[0] as GetModlogResponse),
};
if (this.isoData.routeData[1]) { if (this.isoData.routeData[1]) {
// Getting the moderators // Getting the moderators
let communityRes = Some( let communityRes = Some(
this.isoData.routeData[1] as GetCommunityResponse this.isoData.routeData[1] as GetCommunityResponse
); );
this.state.communityMods = communityRes.map(c => c.moderators); this.state = {
...this.state,
communityMods: communityRes.map(c => c.moderators),
};
} }
this.state.loading = false; this.state = { ...this.state, loading: false };
} else { } else {
this.refetch(); this.refetch();
} }
@ -300,219 +310,283 @@ export class Modlog extends Component<any, ModlogState> {
switch (i.type_) { switch (i.type_) {
case ModlogActionType.ModRemovePost: { case ModlogActionType.ModRemovePost: {
let mrpv = i.view as ModRemovePostView; let mrpv = i.view as ModRemovePostView;
return [ return (
mrpv.mod_remove_post.removed.unwrapOr(false) <>
? "Removed " <span>
: "Restored ", {mrpv.mod_remove_post.removed.unwrapOr(false)
<span> ? "Removed "
Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link> : "Restored "}
</span>, </span>
mrpv.mod_remove_post.reason.match({ <span>
some: reason => <div>reason: {reason}</div>, Post <Link to={`/post/${mrpv.post.id}`}>{mrpv.post.name}</Link>
none: <></>, </span>
}), <span>
]; {mrpv.mod_remove_post.reason.match({
some: reason => <div>reason: {reason}</div>,
none: <></>,
})}
</span>
</>
);
} }
case ModlogActionType.ModLockPost: { case ModlogActionType.ModLockPost: {
let mlpv = i.view as ModLockPostView; let mlpv = i.view as ModLockPostView;
return [ return (
mlpv.mod_lock_post.locked.unwrapOr(false) ? "Locked " : "Unlocked ", <>
<span> <span>
Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link> {mlpv.mod_lock_post.locked.unwrapOr(false)
</span>, ? "Locked "
]; : "Unlocked "}
</span>
<span>
Post <Link to={`/post/${mlpv.post.id}`}>{mlpv.post.name}</Link>
</span>
</>
);
} }
case ModlogActionType.ModStickyPost: { case ModlogActionType.ModStickyPost: {
let mspv = i.view as ModStickyPostView; let mspv = i.view as ModStickyPostView;
return [ return (
mspv.mod_sticky_post.stickied.unwrapOr(false) <>
? "Stickied " <span>
: "Unstickied ", {mspv.mod_sticky_post.stickied.unwrapOr(false)
<span> ? "Stickied "
Post <Link to={`/post/${mspv.post.id}`}>{mspv.post.name}</Link> : "Unstickied "}
</span>, </span>
]; <span>
Post <Link to={`/post/${mspv.post.id}`}>{mspv.post.name}</Link>
</span>
</>
);
} }
case ModlogActionType.ModRemoveComment: { case ModlogActionType.ModRemoveComment: {
let mrc = i.view as ModRemoveCommentView; let mrc = i.view as ModRemoveCommentView;
return [ return (
mrc.mod_remove_comment.removed.unwrapOr(false) <>
? "Removed " <span>
: "Restored ", {mrc.mod_remove_comment.removed.unwrapOr(false)
<span> ? "Removed "
Comment{" "} : "Restored "}
<Link to={`/post/${mrc.post.id}/comment/${mrc.comment.id}`}> </span>
{mrc.comment.content} <span>
</Link> Comment{" "}
</span>, <Link to={`/post/${mrc.post.id}/comment/${mrc.comment.id}`}>
<span> {mrc.comment.content}
{" "} </Link>
by <PersonListing person={mrc.commenter} /> </span>
</span>, <span>
mrc.mod_remove_comment.reason.match({ {" "}
some: reason => <div>reason: {reason}</div>, by <PersonListing person={mrc.commenter} />
none: <></>, </span>
}), <span>
]; {mrc.mod_remove_comment.reason.match({
some: reason => <div>reason: {reason}</div>,
none: <></>,
})}
</span>
</>
);
} }
case ModlogActionType.ModRemoveCommunity: { case ModlogActionType.ModRemoveCommunity: {
let mrco = i.view as ModRemoveCommunityView; let mrco = i.view as ModRemoveCommunityView;
return [ return (
mrco.mod_remove_community.removed.unwrapOr(false) <>
? "Removed " <span>
: "Restored ", {mrco.mod_remove_community.removed.unwrapOr(false)
<span> ? "Removed "
Community <CommunityLink community={mrco.community} /> : "Restored "}
</span>, </span>
mrco.mod_remove_community.reason.match({ <span>
some: reason => <div>reason: {reason}</div>, Community <CommunityLink community={mrco.community} />
none: <></>, </span>
}), <span>
mrco.mod_remove_community.expires.match({ {mrco.mod_remove_community.reason.match({
some: expires => ( some: reason => <div>reason: {reason}</div>,
<div>expires: {moment.utc(expires).fromNow()}</div> none: <></>,
), })}
none: <></>, </span>
}), <span>
]; {mrco.mod_remove_community.expires.match({
some: expires => (
<div>expires: {moment.utc(expires).fromNow()}</div>
),
none: <></>,
})}
</span>
</>
);
} }
case ModlogActionType.ModBanFromCommunity: { case ModlogActionType.ModBanFromCommunity: {
let mbfc = i.view as ModBanFromCommunityView; let mbfc = i.view as ModBanFromCommunityView;
return [ return (
<span> <>
{mbfc.mod_ban_from_community.banned.unwrapOr(false) <span>
? "Banned " {mbfc.mod_ban_from_community.banned.unwrapOr(false)
: "Unbanned "}{" "} ? "Banned "
</span>, : "Unbanned "}{" "}
<span> </span>
<PersonListing person={mbfc.banned_person} /> <span>
</span>, <PersonListing person={mbfc.banned_person} />
<span> from the community </span>, </span>
<span> <span> from the community </span>
<CommunityLink community={mbfc.community} /> <span>
</span>, <CommunityLink community={mbfc.community} />
mbfc.mod_ban_from_community.reason.match({ </span>
some: reason => <div>reason: {reason}</div>, <span>
none: <></>, {mbfc.mod_ban_from_community.reason.match({
}), some: reason => <div>reason: {reason}</div>,
mbfc.mod_ban_from_community.expires.match({ none: <></>,
some: expires => ( })}
<div>expires: {moment.utc(expires).fromNow()}</div> </span>
), <span>
none: <></>, {mbfc.mod_ban_from_community.expires.match({
}), some: expires => (
]; <div>expires: {moment.utc(expires).fromNow()}</div>
),
none: <></>,
})}
</span>
</>
);
} }
case ModlogActionType.ModAddCommunity: { case ModlogActionType.ModAddCommunity: {
let mac = i.view as ModAddCommunityView; let mac = i.view as ModAddCommunityView;
return [ return (
<span> <>
{mac.mod_add_community.removed.unwrapOr(false) <span>
? "Removed " {mac.mod_add_community.removed.unwrapOr(false)
: "Appointed "}{" "} ? "Removed "
</span>, : "Appointed "}{" "}
<span> </span>
<PersonListing person={mac.modded_person} /> <span>
</span>, <PersonListing person={mac.modded_person} />
<span> as a mod to the community </span>, </span>
<span> <span> as a mod to the community </span>
<CommunityLink community={mac.community} /> <span>
</span>, <CommunityLink community={mac.community} />
]; </span>
</>
);
} }
case ModlogActionType.ModTransferCommunity: { case ModlogActionType.ModTransferCommunity: {
let mtc = i.view as ModTransferCommunityView; let mtc = i.view as ModTransferCommunityView;
return [ return (
<span> <>
{mtc.mod_transfer_community.removed.unwrapOr(false) <span>
? "Removed " {mtc.mod_transfer_community.removed.unwrapOr(false)
: "Transferred "}{" "} ? "Removed "
</span>, : "Transferred "}{" "}
<span> </span>
<CommunityLink community={mtc.community} /> <span>
</span>, <CommunityLink community={mtc.community} />
<span> to </span>, </span>
<span> <span> to </span>
<PersonListing person={mtc.modded_person} /> <span>
</span>, <PersonListing person={mtc.modded_person} />
]; </span>
</>
);
} }
case ModlogActionType.ModBan: { case ModlogActionType.ModBan: {
let mb = i.view as ModBanView; let mb = i.view as ModBanView;
return [ return (
<span> <>
{mb.mod_ban.banned.unwrapOr(false) ? "Banned " : "Unbanned "}{" "} <span>
</span>, {mb.mod_ban.banned.unwrapOr(false) ? "Banned " : "Unbanned "}{" "}
<span> </span>
<PersonListing person={mb.banned_person} /> <span>
</span>, <PersonListing person={mb.banned_person} />
mb.mod_ban.reason.match({ </span>
some: reason => <div>reason: {reason}</div>, <span>
none: <></>, {mb.mod_ban.reason.match({
}), some: reason => <div>reason: {reason}</div>,
mb.mod_ban.expires.match({ none: <></>,
some: expires => ( })}
<div>expires: {moment.utc(expires).fromNow()}</div> </span>
), <span>
none: <></>, {mb.mod_ban.expires.match({
}), some: expires => (
]; <div>expires: {moment.utc(expires).fromNow()}</div>
),
none: <></>,
})}
</span>
</>
);
} }
case ModlogActionType.ModAdd: { case ModlogActionType.ModAdd: {
let ma = i.view as ModAddView; let ma = i.view as ModAddView;
return [ return (
<span> <>
{ma.mod_add.removed.unwrapOr(false) ? "Removed " : "Appointed "}{" "} <span>
</span>, {ma.mod_add.removed.unwrapOr(false) ? "Removed " : "Appointed "}{" "}
<span> </span>
<PersonListing person={ma.modded_person} /> <span>
</span>, <PersonListing person={ma.modded_person} />
<span> as an admin </span>, </span>
]; <span> as an admin </span>
</>
);
} }
case ModlogActionType.AdminPurgePerson: { case ModlogActionType.AdminPurgePerson: {
let ap = i.view as AdminPurgePersonView; let ap = i.view as AdminPurgePersonView;
return [ return (
<span>Purged a Person</span>, <>
ap.admin_purge_person.reason.match({ <span>Purged a Person</span>
some: reason => <div>reason: {reason}</div>, <span>
none: <></>, {ap.admin_purge_person.reason.match({
}), some: reason => <div>reason: {reason}</div>,
]; none: <></>,
})}
</span>
</>
);
} }
case ModlogActionType.AdminPurgeCommunity: { case ModlogActionType.AdminPurgeCommunity: {
let ap = i.view as AdminPurgeCommunityView; let ap = i.view as AdminPurgeCommunityView;
return [ return (
<span>Purged a Community</span>, <>
ap.admin_purge_community.reason.match({ <span>Purged a Community</span>
some: reason => <div>reason: {reason}</div>, <span>
none: <></>, {ap.admin_purge_community.reason.match({
}), some: reason => <div>reason: {reason}</div>,
]; none: <></>,
})}
</span>
</>
);
} }
case ModlogActionType.AdminPurgePost: { case ModlogActionType.AdminPurgePost: {
let ap = i.view as AdminPurgePostView; let ap = i.view as AdminPurgePostView;
return [ return (
<span>Purged a Post from from </span>, <>
<CommunityLink community={ap.community} />, <span>Purged a Post from from </span>
ap.admin_purge_post.reason.match({ <CommunityLink community={ap.community} />
some: reason => <div>reason: {reason}</div>, <span>
none: <></>, {ap.admin_purge_post.reason.match({
}), some: reason => <div>reason: {reason}</div>,
]; none: <></>,
})}
</span>
</>
);
} }
case ModlogActionType.AdminPurgeComment: { case ModlogActionType.AdminPurgeComment: {
let ap = i.view as AdminPurgeCommentView; let ap = i.view as AdminPurgeCommentView;
return [ return (
<span> <>
Purged a Comment from{" "} <span>
<Link to={`/post/${ap.post.id}`}>{ap.post.name}</Link> Purged a Comment from{" "}
</span>, <Link to={`/post/${ap.post.id}`}>{ap.post.name}</Link>
ap.admin_purge_comment.reason.match({ </span>
some: reason => <div>reason: {reason}</div>, <span>
none: <></>, {ap.admin_purge_comment.reason.match({
}), some: reason => <div>reason: {reason}</div>,
]; none: <></>,
})}
</span>
</>
);
} }
default: default:
return <div />; return <div />;
@ -525,7 +599,7 @@ export class Modlog extends Component<any, ModlogState> {
return ( return (
<tbody> <tbody>
{combined.map(i => ( {combined.map(i => (
<tr> <tr key={i.id}>
<td> <td>
<MomentTime published={i.when_} updated={None} /> <MomentTime published={i.when_} updated={None} />
</td> </td>
@ -544,10 +618,7 @@ export class Modlog extends Component<any, ModlogState> {
} }
get amAdminOrMod(): boolean { get amAdminOrMod(): boolean {
return ( return amAdmin() || amMod(this.state.communityMods);
amAdmin(Some(this.state.siteRes.admins)) ||
amMod(this.state.communityMods)
);
} }
modOrAdminText(person: Option<PersonSafe>): string { modOrAdminText(person: Option<PersonSafe>): string {
@ -593,67 +664,75 @@ export class Modlog extends Component<any, ModlogState> {
})} })}
<span>{i18n.t("modlog")}</span> <span>{i18n.t("modlog")}</span>
</h5> </h5>
<form className="form-inline mr-2"> <div className="form-row">
<select <div className="form-group col-sm-6">
value={this.state.filter_action} <select
onChange={linkEvent(this, this.handleFilterActionChange)} value={this.state.filter_action}
className="custom-select col-4 mb-2" onChange={linkEvent(this, this.handleFilterActionChange)}
aria-label="action" className="custom-select mb-2"
> aria-label="action"
<option disabled aria-hidden="true"> >
{i18n.t("filter_by_action")} <option disabled aria-hidden="true">
</option> {i18n.t("filter_by_action")}
<option value={ModlogActionType.All}>{i18n.t("all")}</option> </option>
<option value={ModlogActionType.ModRemovePost}> <option value={ModlogActionType.All}>{i18n.t("all")}</option>
Removing Posts <option value={ModlogActionType.ModRemovePost}>
</option> Removing Posts
<option value={ModlogActionType.ModLockPost}> </option>
Locking Posts <option value={ModlogActionType.ModLockPost}>
</option> Locking Posts
<option value={ModlogActionType.ModStickyPost}> </option>
Stickying Posts <option value={ModlogActionType.ModStickyPost}>
</option> Stickying Posts
<option value={ModlogActionType.ModRemoveComment}> </option>
Removing Comments <option value={ModlogActionType.ModRemoveComment}>
</option> Removing Comments
<option value={ModlogActionType.ModRemoveCommunity}> </option>
Removing Communities <option value={ModlogActionType.ModRemoveCommunity}>
</option> Removing Communities
<option value={ModlogActionType.ModBanFromCommunity}> </option>
Banning From Communities <option value={ModlogActionType.ModBanFromCommunity}>
</option> Banning From Communities
<option value={ModlogActionType.ModAddCommunity}> </option>
Adding Mod to Community <option value={ModlogActionType.ModAddCommunity}>
</option> Adding Mod to Community
<option value={ModlogActionType.ModTransferCommunity}> </option>
Transfering Communities <option value={ModlogActionType.ModTransferCommunity}>
</option> Transfering Communities
<option value={ModlogActionType.ModAdd}> </option>
Adding Mod to Site <option value={ModlogActionType.ModAdd}>
</option> Adding Mod to Site
<option value={ModlogActionType.ModBan}> </option>
Banning From Site <option value={ModlogActionType.ModBan}>
</option> Banning From Site
</select> </option>
</select>
</div>
{this.state.siteRes.site_view.match({ {this.state.siteRes.site_view.match({
some: site_view => some: site_view =>
!site_view.site.hide_modlog_mod_names.unwrapOr(false) && ( !site_view.site.hide_modlog_mod_names.unwrapOr(false) && (
<select <div className="form-group col-sm-6">
id="filter-mod" <select
value={toUndefined(this.state.filter_mod)} id="filter-mod"
> className="form-control"
<option>{i18n.t("filter_by_mod")}</option> value={toUndefined(this.state.filter_mod)}
</select> >
<option>{i18n.t("filter_by_mod")}</option>
</select>
</div>
), ),
none: <></>, none: <></>,
})} })}
<select <div className="form-group col-sm-6">
id="filter-user" <select
value={toUndefined(this.state.filter_user)} id="filter-user"
> className="form-control"
<option>{i18n.t("filter_by_user")}</option> value={toUndefined(this.state.filter_user)}
</select> >
</form> <option>{i18n.t("filter_by_user")}</option>
</select>
</div>
</div>
<div className="table-responsive"> <div className="table-responsive">
<table id="modlog_table" className="table table-sm table-hover"> <table id="modlog_table" className="table table-sm table-hover">
<thead className="pointer"> <thead className="pointer">
@ -715,12 +794,11 @@ export class Modlog extends Component<any, ModlogState> {
if (isBrowser()) { if (isBrowser()) {
let selectId: any = document.getElementById("filter-user"); let selectId: any = document.getElementById("filter-user");
if (selectId) { if (selectId) {
this.userChoices = new Choices(selectId, choicesModLogConfig); this.userChoices = new Choices(selectId, choicesConfig);
this.userChoices.passedElement.element.addEventListener( this.userChoices.passedElement.element.addEventListener(
"choice", "choice",
(e: any) => { (e: any) => {
this.state.filter_user = Some(Number(e.detail.choice.value)); this.setState({ filter_user: Some(Number(e.detail.choice.value)) });
this.setState(this.state);
this.refetch(); this.refetch();
}, },
false false
@ -755,12 +833,11 @@ export class Modlog extends Component<any, ModlogState> {
if (isBrowser()) { if (isBrowser()) {
let selectId: any = document.getElementById("filter-mod"); let selectId: any = document.getElementById("filter-mod");
if (selectId) { if (selectId) {
this.modChoices = new Choices(selectId, choicesModLogConfig); this.modChoices = new Choices(selectId, choicesConfig);
this.modChoices.passedElement.element.addEventListener( this.modChoices.passedElement.element.addEventListener(
"choice", "choice",
(e: any) => { (e: any) => {
this.state.filter_mod = Some(Number(e.detail.choice.value)); this.setState({ filter_mod: Some(Number(e.detail.choice.value)) });
this.setState(this.state);
this.refetch(); this.refetch();
}, },
false false
@ -829,14 +906,16 @@ export class Modlog extends Component<any, ModlogState> {
return; return;
} else if (op == UserOperation.GetModlog) { } else if (op == UserOperation.GetModlog) {
let data = wsJsonToRes<GetModlogResponse>(msg, GetModlogResponse); let data = wsJsonToRes<GetModlogResponse>(msg, GetModlogResponse);
this.state.loading = false;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.state.res = Some(data); this.setState({ res: Some(data), loading: false });
this.setState(this.state); this.setupUserFilter();
this.setupModFilter();
} else if (op == UserOperation.GetCommunity) { } else if (op == UserOperation.GetCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse); let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
this.state.communityMods = Some(data.moderators); this.setState({
this.state.communityName = Some(data.community_view.community.name); communityMods: Some(data.moderators),
communityName: Some(data.community_view.community.name),
});
} }
} }
} }

View file

@ -17,6 +17,7 @@ import {
PersonMentionResponse, PersonMentionResponse,
PersonMentionView, PersonMentionView,
PostReportResponse, PostReportResponse,
PrivateMessageReportResponse,
PrivateMessageResponse, PrivateMessageResponse,
PrivateMessagesResponse, PrivateMessagesResponse,
PrivateMessageView, PrivateMessageView,
@ -127,15 +128,19 @@ export class Inbox extends Component<any, InboxState> {
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state.replies = this.state = {
(this.isoData.routeData[0] as GetRepliesResponse).replies || []; ...this.state,
this.state.mentions = replies:
(this.isoData.routeData[1] as GetPersonMentionsResponse).mentions || []; (this.isoData.routeData[0] as GetRepliesResponse).replies || [],
this.state.messages = mentions:
(this.isoData.routeData[2] as PrivateMessagesResponse) (this.isoData.routeData[1] as GetPersonMentionsResponse).mentions ||
.private_messages || []; [],
this.state.combined = this.buildCombined(); messages:
this.state.loading = false; (this.isoData.routeData[2] as PrivateMessagesResponse)
.private_messages || [],
loading: false,
};
this.state = { ...this.state, combined: this.buildCombined() };
} else { } else {
this.refetch(); this.refetch();
} }
@ -166,21 +171,21 @@ export class Inbox extends Component<any, InboxState> {
.ok() .ok()
.map(a => `/feeds/inbox/${a}.xml`); .map(a => `/feeds/inbox/${a}.xml`);
return ( return (
<div class="container"> <div className="container">
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
<div class="row"> <div className="row">
<div class="col-12"> <div className="col-12">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None} description={None}
image={None} image={None}
/> />
<h5 class="mb-2"> <h5 className="mb-2">
{i18n.t("inbox")} {i18n.t("inbox")}
{inboxRss.match({ {inboxRss.match({
some: rss => ( some: rss => (
@ -204,7 +209,7 @@ export class Inbox extends Component<any, InboxState> {
0 && 0 &&
this.state.unreadOrAll == UnreadOrAll.Unread && ( this.state.unreadOrAll == UnreadOrAll.Unread && (
<button <button
class="btn btn-secondary mb-2" className="btn btn-secondary mb-2"
onClick={linkEvent(this, this.markAllAsRead)} onClick={linkEvent(this, this.markAllAsRead)}
> >
{i18n.t("mark_all_as_read")} {i18n.t("mark_all_as_read")}
@ -230,7 +235,7 @@ export class Inbox extends Component<any, InboxState> {
unreadOrAllRadios() { unreadOrAllRadios() {
return ( return (
<div class="btn-group btn-group-toggle flex-wrap mb-2"> <div className="btn-group btn-group-toggle flex-wrap mb-2">
<label <label
className={`btn btn-outline-secondary pointer className={`btn btn-outline-secondary pointer
${this.state.unreadOrAll == UnreadOrAll.Unread && "active"} ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
@ -263,7 +268,7 @@ export class Inbox extends Component<any, InboxState> {
messageTypeRadios() { messageTypeRadios() {
return ( return (
<div class="btn-group btn-group-toggle flex-wrap mb-2"> <div className="btn-group btn-group-toggle flex-wrap mb-2">
<label <label
className={`btn btn-outline-secondary pointer className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.All && "active"} ${this.state.messageType == MessageType.All && "active"}
@ -323,8 +328,8 @@ export class Inbox extends Component<any, InboxState> {
selects() { selects() {
return ( return (
<div className="mb-2"> <div className="mb-2">
<span class="mr-3">{this.unreadOrAllRadios()}</span> <span className="mr-3">{this.unreadOrAllRadios()}</span>
<span class="mr-3">{this.messageTypeRadios()}</span> <span className="mr-3">{this.messageTypeRadios()}</span>
<CommentSortSelect <CommentSortSelect
sort={this.state.sort} sort={this.state.sort}
onChange={this.handleSortChange} onChange={this.handleSortChange}
@ -394,6 +399,7 @@ export class Inbox extends Component<any, InboxState> {
showCommunity showCommunity
showContext showContext
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
/> />
); );
case ReplyEnum.Mention: case ReplyEnum.Mention:
@ -416,6 +422,7 @@ export class Inbox extends Component<any, InboxState> {
showCommunity showCommunity
showContext showContext
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
/> />
); );
case ReplyEnum.Message: case ReplyEnum.Message:
@ -448,6 +455,7 @@ export class Inbox extends Component<any, InboxState> {
showCommunity showCommunity
showContext showContext
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
/> />
</div> </div>
); );
@ -469,6 +477,7 @@ export class Inbox extends Component<any, InboxState> {
showCommunity showCommunity
showContext showContext
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
/> />
))} ))}
</div> </div>
@ -494,16 +503,12 @@ export class Inbox extends Component<any, InboxState> {
} }
handleUnreadOrAllChange(i: Inbox, event: any) { handleUnreadOrAllChange(i: Inbox, event: any) {
i.state.unreadOrAll = Number(event.target.value); i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
i.state.page = 1;
i.setState(i.state);
i.refetch(); i.refetch();
} }
handleMessageTypeChange(i: Inbox, event: any) { handleMessageTypeChange(i: Inbox, event: any) {
i.state.messageType = Number(event.target.value); i.setState({ messageType: Number(event.target.value), page: 1 });
i.state.page = 1;
i.setState(i.state);
i.refetch(); i.refetch();
} }
@ -580,9 +585,7 @@ export class Inbox extends Component<any, InboxState> {
} }
handleSortChange(val: CommentSortType) { handleSortChange(val: CommentSortType) {
this.state.sort = val; this.setState({ sort: val, page: 1 });
this.state.page = 1;
this.setState(this.state);
this.refetch(); this.refetch();
} }
@ -592,10 +595,8 @@ export class Inbox extends Component<any, InboxState> {
auth: auth().unwrap(), auth: auth().unwrap(),
}) })
); );
i.state.replies = []; i.setState({ replies: [], mentions: [], messages: [] });
i.state.mentions = []; i.setState({ combined: i.buildCombined() });
i.state.messages = [];
i.state.combined = i.buildCombined();
UserService.Instance.unreadInboxCountSub.next(0); UserService.Instance.unreadInboxCountSub.next(0);
window.scrollTo(0, 0); window.scrollTo(0, 0);
i.setState(i.state); i.setState(i.state);
@ -620,31 +621,27 @@ export class Inbox extends Component<any, InboxState> {
this.refetch(); this.refetch();
} else if (op == UserOperation.GetReplies) { } else if (op == UserOperation.GetReplies) {
let data = wsJsonToRes<GetRepliesResponse>(msg, GetRepliesResponse); let data = wsJsonToRes<GetRepliesResponse>(msg, GetRepliesResponse);
this.state.replies = data.replies; this.setState({ replies: data.replies });
this.state.combined = this.buildCombined(); this.setState({ combined: this.buildCombined(), loading: false });
this.state.loading = false;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.GetPersonMentions) { } else if (op == UserOperation.GetPersonMentions) {
let data = wsJsonToRes<GetPersonMentionsResponse>( let data = wsJsonToRes<GetPersonMentionsResponse>(
msg, msg,
GetPersonMentionsResponse GetPersonMentionsResponse
); );
this.state.mentions = data.mentions; this.setState({ mentions: data.mentions });
this.state.combined = this.buildCombined(); this.setState({ combined: this.buildCombined() });
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.GetPrivateMessages) { } else if (op == UserOperation.GetPrivateMessages) {
let data = wsJsonToRes<PrivateMessagesResponse>( let data = wsJsonToRes<PrivateMessagesResponse>(
msg, msg,
PrivateMessagesResponse PrivateMessagesResponse
); );
this.state.messages = data.private_messages; this.setState({ messages: data.private_messages });
this.state.combined = this.buildCombined(); this.setState({ combined: this.buildCombined() });
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.EditPrivateMessage) { } else if (op == UserOperation.EditPrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>( let data = wsJsonToRes<PrivateMessageResponse>(
@ -696,7 +693,9 @@ export class Inbox extends Component<any, InboxState> {
if (found) { if (found) {
let combinedView = this.state.combined.find( let combinedView = this.state.combined.find(
i => i.id == data.private_message_view.private_message.id i =>
i.id == data.private_message_view.private_message.id &&
i.type_ == ReplyEnum.Message
).view as PrivateMessageView; ).view as PrivateMessageView;
found.private_message.updated = combinedView.private_message.updated = found.private_message.updated = combinedView.private_message.updated =
data.private_message_view.private_message.updated; data.private_message_view.private_message.updated;
@ -706,14 +705,18 @@ export class Inbox extends Component<any, InboxState> {
this.state.unreadOrAll == UnreadOrAll.Unread && this.state.unreadOrAll == UnreadOrAll.Unread &&
data.private_message_view.private_message.read data.private_message_view.private_message.read
) { ) {
this.state.messages = this.state.messages.filter( this.setState({
r => messages: this.state.messages.filter(
r.private_message.id !== r =>
data.private_message_view.private_message.id r.private_message.id !==
); data.private_message_view.private_message.id
this.state.combined = this.state.combined.filter( ),
r => r.id !== data.private_message_view.private_message.id });
); this.setState({
combined: this.state.combined.filter(
r => r.id !== data.private_message_view.private_message.id
),
});
} else { } else {
found.private_message.read = combinedView.private_message.read = found.private_message.read = combinedView.private_message.read =
data.private_message_view.private_message.read; data.private_message_view.private_message.read;
@ -733,7 +736,6 @@ export class Inbox extends Component<any, InboxState> {
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.MarkCommentReplyAsRead) { } else if (op == UserOperation.MarkCommentReplyAsRead) {
let data = wsJsonToRes<CommentReplyResponse>(msg, CommentReplyResponse); let data = wsJsonToRes<CommentReplyResponse>(msg, CommentReplyResponse);
console.log(data);
let found = this.state.replies.find( let found = this.state.replies.find(
c => c.comment_reply.id == data.comment_reply_view.comment_reply.id c => c.comment_reply.id == data.comment_reply_view.comment_reply.id
@ -741,7 +743,9 @@ export class Inbox extends Component<any, InboxState> {
if (found) { if (found) {
let combinedView = this.state.combined.find( let combinedView = this.state.combined.find(
i => i.id == data.comment_reply_view.comment_reply.id i =>
i.id == data.comment_reply_view.comment_reply.id &&
i.type_ == ReplyEnum.Reply
).view as CommentReplyView; ).view as CommentReplyView;
found.comment.content = combinedView.comment.content = found.comment.content = combinedView.comment.content =
data.comment_reply_view.comment.content; data.comment_reply_view.comment.content;
@ -763,12 +767,17 @@ export class Inbox extends Component<any, InboxState> {
this.state.unreadOrAll == UnreadOrAll.Unread && this.state.unreadOrAll == UnreadOrAll.Unread &&
data.comment_reply_view.comment_reply.read data.comment_reply_view.comment_reply.read
) { ) {
this.state.replies = this.state.replies.filter( this.setState({
r => r.comment_reply.id !== data.comment_reply_view.comment_reply.id replies: this.state.replies.filter(
); r =>
this.state.combined = this.state.combined.filter( r.comment_reply.id !== data.comment_reply_view.comment_reply.id
r => r.id !== data.comment_reply_view.comment_reply.id ),
); });
this.setState({
combined: this.state.combined.filter(
r => r.id !== data.comment_reply_view.comment_reply.id
),
});
} else { } else {
found.comment_reply.read = combinedView.comment_reply.read = found.comment_reply.read = combinedView.comment_reply.read =
data.comment_reply_view.comment_reply.read; data.comment_reply_view.comment_reply.read;
@ -786,7 +795,9 @@ export class Inbox extends Component<any, InboxState> {
if (found) { if (found) {
let combinedView = this.state.combined.find( let combinedView = this.state.combined.find(
i => i.id == data.person_mention_view.person_mention.id i =>
i.id == data.person_mention_view.person_mention.id &&
i.type_ == ReplyEnum.Mention
).view as PersonMentionView; ).view as PersonMentionView;
found.comment.content = combinedView.comment.content = found.comment.content = combinedView.comment.content =
data.person_mention_view.comment.content; data.person_mention_view.comment.content;
@ -808,13 +819,18 @@ export class Inbox extends Component<any, InboxState> {
this.state.unreadOrAll == UnreadOrAll.Unread && this.state.unreadOrAll == UnreadOrAll.Unread &&
data.person_mention_view.person_mention.read data.person_mention_view.person_mention.read
) { ) {
this.state.mentions = this.state.mentions.filter( this.setState({
r => mentions: this.state.mentions.filter(
r.person_mention.id !== data.person_mention_view.person_mention.id r =>
); r.person_mention.id !==
this.state.combined = this.state.combined.filter( data.person_mention_view.person_mention.id
r => r.id !== data.person_mention_view.person_mention.id ),
); });
this.setState({
combined: this.state.combined.filter(
r => r.id !== data.person_mention_view.person_mention.id
),
});
} else { } else {
// TODO test to make sure these mentions are getting marked as read // TODO test to make sure these mentions are getting marked as read
found.person_mention.read = combinedView.person_mention.read = found.person_mention.read = combinedView.person_mention.read =
@ -865,6 +881,14 @@ export class Inbox extends Component<any, InboxState> {
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
} else if (op == UserOperation.CreatePrivateMessageReport) {
let data = wsJsonToRes<PrivateMessageReportResponse>(
msg,
PrivateMessageReportResponse
);
if (data) {
toast(i18n.t("report_created"));
}
} }
} }

View file

@ -66,15 +66,15 @@ export class PasswordChange extends Component<any, State> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None} description={None}
image={None} image={None}
/> />
<div class="row"> <div className="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t("password_change")}</h5> <h5>{i18n.t("password_change")}</h5>
{this.passwordChangeForm()} {this.passwordChangeForm()}
</div> </div>
@ -86,41 +86,41 @@ export class PasswordChange extends Component<any, State> {
passwordChangeForm() { passwordChangeForm() {
return ( return (
<form onSubmit={linkEvent(this, this.handlePasswordChangeSubmit)}> <form onSubmit={linkEvent(this, this.handlePasswordChangeSubmit)}>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="new-password"> <label className="col-sm-2 col-form-label" htmlFor="new-password">
{i18n.t("new_password")} {i18n.t("new_password")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<input <input
id="new-password" id="new-password"
type="password" type="password"
value={this.state.passwordChangeForm.password} value={this.state.passwordChangeForm.password}
onInput={linkEvent(this, this.handlePasswordChange)} onInput={linkEvent(this, this.handlePasswordChange)}
class="form-control" className="form-control"
required required
maxLength={60} maxLength={60}
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="verify-password"> <label className="col-sm-2 col-form-label" htmlFor="verify-password">
{i18n.t("verify_password")} {i18n.t("verify_password")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<input <input
id="verify-password" id="verify-password"
type="password" type="password"
value={this.state.passwordChangeForm.password_verify} value={this.state.passwordChangeForm.password_verify}
onInput={linkEvent(this, this.handleVerifyPasswordChange)} onInput={linkEvent(this, this.handleVerifyPasswordChange)}
class="form-control" className="form-control"
required required
maxLength={60} maxLength={60}
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<div class="col-sm-10"> <div className="col-sm-10">
<button type="submit" class="btn btn-secondary"> <button type="submit" className="btn btn-secondary">
{this.state.loading ? ( {this.state.loading ? (
<Spinner /> <Spinner />
) : ( ) : (
@ -145,8 +145,7 @@ export class PasswordChange extends Component<any, State> {
handlePasswordChangeSubmit(i: PasswordChange, event: any) { handlePasswordChangeSubmit(i: PasswordChange, event: any) {
event.preventDefault(); event.preventDefault();
i.state.loading = true; i.setState({ loading: true });
i.setState(i.state);
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.passwordChange(i.state.passwordChangeForm) wsClient.passwordChange(i.state.passwordChangeForm)
@ -158,13 +157,11 @@ export class PasswordChange extends Component<any, State> {
console.log(msg); console.log(msg);
if (msg.error) { if (msg.error) {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
this.state.loading = false; this.setState({ loading: false });
this.setState(this.state);
return; return;
} else if (op == UserOperation.PasswordChange) { } else if (op == UserOperation.PasswordChange) {
let data = wsJsonToRes<LoginResponse>(msg, LoginResponse); let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
this.state = this.emptyState; this.setState(this.emptyState);
this.setState(this.state);
UserService.Instance.login(data); UserService.Instance.login(data);
this.props.history.push("/"); this.props.history.push("/");
} }

View file

@ -3,6 +3,7 @@ import { Component } from "inferno";
import { import {
CommentView, CommentView,
GetPersonDetailsResponse, GetPersonDetailsResponse,
Language,
PersonViewSafe, PersonViewSafe,
PostView, PostView,
SortType, SortType,
@ -16,6 +17,7 @@ import { PostListing } from "../post/post-listing";
interface PersonDetailsProps { interface PersonDetailsProps {
personRes: GetPersonDetailsResponse; personRes: GetPersonDetailsResponse;
admins: PersonViewSafe[]; admins: PersonViewSafe[];
allLanguages: Language[];
page: number; page: number;
limit: number; limit: number;
sort: SortType; sort: SortType;
@ -99,6 +101,7 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
showCommunity showCommunity
showContext showContext
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
allLanguages={this.props.allLanguages}
/> />
); );
} }
@ -114,6 +117,7 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
showCommunity showCommunity
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
allLanguages={this.props.allLanguages}
/> />
); );
} }
@ -150,7 +154,10 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
return ( return (
<div> <div>
{combined.map(i => [this.renderItemType(i), <hr class="my-3" />])} {combined.map(i => [
this.renderItemType(i),
<hr key={i.type_} className="my-3" />,
])}
</div> </div>
); );
} }
@ -168,6 +175,7 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
showCommunity showCommunity
showContext showContext
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
allLanguages={this.props.allLanguages}
/> />
</div> </div>
); );
@ -186,8 +194,9 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
moderators={None} moderators={None}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
allLanguages={this.props.allLanguages}
/> />
<hr class="my-3" /> <hr className="my-3" />
</> </>
))} ))}
</div> </div>

View file

@ -121,15 +121,14 @@ export class Profile extends Component<any, ProfileState> {
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state.personRes = Some( this.state = {
this.isoData.routeData[0] as GetPersonDetailsResponse ...this.state,
); personRes: Some(this.isoData.routeData[0] as GetPersonDetailsResponse),
this.state.loading = false; loading: false,
};
} else { } else {
this.fetchUserData(); this.fetchUserData();
} }
this.setPersonBlock();
} }
fetchUserData() { fetchUserData() {
@ -162,11 +161,12 @@ export class Profile extends Component<any, ProfileState> {
UserService.Instance.myUserInfo.match({ UserService.Instance.myUserInfo.match({
some: mui => some: mui =>
this.state.personRes.match({ this.state.personRes.match({
some: res => { some: res =>
this.state.personBlocked = mui.person_blocks this.setState({
.map(a => a.target.id) personBlocked: mui.person_blocks
.includes(res.person_view.person.id); .map(a => a.target.id)
}, .includes(res.person_view.person.id),
}),
none: void 0, none: void 0,
}), }),
none: void 0, none: void 0,
@ -207,6 +207,7 @@ export class Profile extends Component<any, ProfileState> {
} }
componentDidMount() { componentDidMount() {
this.setPersonBlock();
setupTippy(); setupTippy();
} }
@ -250,7 +251,7 @@ export class Profile extends Component<any, ProfileState> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
<Spinner large /> <Spinner large />
@ -258,8 +259,8 @@ export class Profile extends Component<any, ProfileState> {
) : ( ) : (
this.state.personRes.match({ this.state.personRes.match({
some: res => ( some: res => (
<div class="row"> <div className="row">
<div class="col-12 col-md-8"> <div className="col-12 col-md-8">
<> <>
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
@ -281,11 +282,12 @@ export class Profile extends Component<any, ProfileState> {
enableNsfw={enableNsfw(this.state.siteRes)} enableNsfw={enableNsfw(this.state.siteRes)}
view={this.state.view} view={this.state.view}
onPageChange={this.handlePageChange} onPageChange={this.handlePageChange}
allLanguages={this.state.siteRes.all_languages}
/> />
</div> </div>
{!this.state.loading && ( {!this.state.loading && (
<div class="col-12 col-md-4"> <div className="col-12 col-md-4">
{this.moderates()} {this.moderates()}
{this.amCurrentUser && this.follows()} {this.amCurrentUser && this.follows()}
</div> </div>
@ -301,7 +303,7 @@ export class Profile extends Component<any, ProfileState> {
viewRadios() { viewRadios() {
return ( return (
<div class="btn-group btn-group-toggle flex-wrap mb-2"> <div className="btn-group btn-group-toggle flex-wrap mb-2">
<label <label
className={`btn btn-outline-secondary pointer className={`btn btn-outline-secondary pointer
${this.state.view == PersonDetailsView.Overview && "active"} ${this.state.view == PersonDetailsView.Overview && "active"}
@ -363,7 +365,7 @@ export class Profile extends Component<any, ProfileState> {
return ( return (
<div className="mb-2"> <div className="mb-2">
<span class="mr-3">{this.viewRadios()}</span> <span className="mr-3">{this.viewRadios()}</span>
<SortSelect <SortSelect
sort={this.state.sort} sort={this.state.sort}
onChange={this.handleSortChange} onChange={this.handleSortChange}
@ -406,14 +408,17 @@ export class Profile extends Component<any, ProfileState> {
banner={pv.person.banner} banner={pv.person.banner}
icon={pv.person.avatar} icon={pv.person.avatar}
/> />
<div class="mb-3"> <div className="mb-3">
<div class=""> <div className="">
<div class="mb-0 d-flex flex-wrap"> <div className="mb-0 d-flex flex-wrap">
<div> <div>
{pv.person.display_name && ( {pv.person.display_name.match({
<h5 class="mb-0">{pv.person.display_name}</h5> some: displayName => (
)} <h5 className="mb-0">{displayName}</h5>
<ul class="list-inline mb-2"> ),
none: <></>,
})}
<ul className="list-inline mb-2">
<li className="list-inline-item"> <li className="list-inline-item">
<PersonListing <PersonListing
person={pv.person} person={pv.person}
@ -531,7 +536,7 @@ export class Profile extends Component<any, ProfileState> {
none: <></>, none: <></>,
})} })}
<div> <div>
<ul class="list-inline mb-2"> <ul className="list-inline mb-2">
<li className="list-inline-item badge badge-light"> <li className="list-inline-item badge badge-light">
{i18n.t("number_of_posts", { {i18n.t("number_of_posts", {
count: pv.counts.post_count, count: pv.counts.post_count,
@ -546,7 +551,7 @@ export class Profile extends Component<any, ProfileState> {
</li> </li>
</ul> </ul>
</div> </div>
<div class="text-muted"> <div className="text-muted">
{i18n.t("joined")}{" "} {i18n.t("joined")}{" "}
<MomentTime <MomentTime
published={pv.person.published} published={pv.person.published}
@ -581,33 +586,36 @@ export class Profile extends Component<any, ProfileState> {
<> <>
{this.state.showBanDialog && ( {this.state.showBanDialog && (
<form onSubmit={linkEvent(this, this.handleModBanSubmit)}> <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
<div class="form-group row col-12"> <div className="form-group row col-12">
<label class="col-form-label" htmlFor="profile-ban-reason"> <label
className="col-form-label"
htmlFor="profile-ban-reason"
>
{i18n.t("reason")} {i18n.t("reason")}
</label> </label>
<input <input
type="text" type="text"
id="profile-ban-reason" id="profile-ban-reason"
class="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={toUndefined(this.state.banReason)} value={toUndefined(this.state.banReason)}
onInput={linkEvent(this, this.handleModBanReasonChange)} onInput={linkEvent(this, this.handleModBanReasonChange)}
/> />
<label class="col-form-label" htmlFor={`mod-ban-expires`}> <label className="col-form-label" htmlFor={`mod-ban-expires`}>
{i18n.t("expires")} {i18n.t("expires")}
</label> </label>
<input <input
type="number" type="number"
id={`mod-ban-expires`} id={`mod-ban-expires`}
class="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("number_of_days")} placeholder={i18n.t("number_of_days")}
value={toUndefined(this.state.banExpireDays)} value={toUndefined(this.state.banExpireDays)}
onInput={linkEvent(this, this.handleModBanExpireDaysChange)} onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
/> />
<div class="form-group"> <div className="form-group">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="mod-ban-remove-data" id="mod-ban-remove-data"
type="checkbox" type="checkbox"
checked={this.state.removeData} checked={this.state.removeData}
@ -617,7 +625,7 @@ export class Profile extends Component<any, ProfileState> {
)} )}
/> />
<label <label
class="form-check-label" className="form-check-label"
htmlFor="mod-ban-remove-data" htmlFor="mod-ban-remove-data"
title={i18n.t("remove_content_more")} title={i18n.t("remove_content_more")}
> >
@ -631,10 +639,10 @@ export class Profile extends Component<any, ProfileState> {
{/* <label class="col-form-label">Expires</label> */} {/* <label class="col-form-label">Expires</label> */}
{/* <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 className="form-group row">
<button <button
type="cancel" type="reset"
class="btn btn-secondary mr-2" className="btn btn-secondary mr-2"
aria-label={i18n.t("cancel")} aria-label={i18n.t("cancel")}
onClick={linkEvent(this, this.handleModBanSubmitCancel)} onClick={linkEvent(this, this.handleModBanSubmitCancel)}
> >
@ -642,7 +650,7 @@ export class Profile extends Component<any, ProfileState> {
</button> </button>
<button <button
type="submit" type="submit"
class="btn btn-secondary" className="btn btn-secondary"
aria-label={i18n.t("ban")} aria-label={i18n.t("ban")}
> >
{i18n.t("ban")} {pv.person.name} {i18n.t("ban")} {pv.person.name}
@ -656,25 +664,28 @@ export class Profile extends Component<any, ProfileState> {
}); });
} }
// TODO test this, make sure its good
moderates() { moderates() {
return this.state.personRes return this.state.personRes
.map(r => r.moderates) .map(r => r.moderates)
.match({ .match({
some: moderates => { some: moderates => {
if (moderates.length > 0) { if (moderates.length > 0) {
<div class="card border-secondary mb-3"> return (
<div class="card-body"> <div className="card border-secondary mb-3">
<h5>{i18n.t("moderates")}</h5> <div className="card-body">
<ul class="list-unstyled mb-0"> <h5>{i18n.t("moderates")}</h5>
{moderates.map(cmv => ( <ul className="list-unstyled mb-0">
<li> {moderates.map(cmv => (
<CommunityLink community={cmv.community} /> <li key={cmv.community.id}>
</li> <CommunityLink community={cmv.community} />
))} </li>
</ul> ))}
</ul>
</div>
</div> </div>
</div>; );
} else {
return <></>;
} }
}, },
none: void 0, none: void 0,
@ -687,18 +698,22 @@ export class Profile extends Component<any, ProfileState> {
.match({ .match({
some: follows => { some: follows => {
if (follows.length > 0) { if (follows.length > 0) {
<div class="card border-secondary mb-3"> return (
<div class="card-body"> <div className="card border-secondary mb-3">
<h5>{i18n.t("subscribed")}</h5> <div className="card-body">
<ul class="list-unstyled mb-0"> <h5>{i18n.t("subscribed")}</h5>
{follows.map(cfv => ( <ul className="list-unstyled mb-0">
<li> {follows.map(cfv => (
<CommunityLink community={cfv.community} /> <li key={cfv.community.id}>
</li> <CommunityLink community={cfv.community} />
))} </li>
</ul> ))}
</ul>
</div>
</div> </div>
</div>; );
} else {
return <></>;
} }
}, },
none: void 0, none: void 0,
@ -715,8 +730,7 @@ export class Profile extends Component<any, ProfileState> {
this.props.history.push( this.props.history.push(
`${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}` `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}`
); );
this.state.loading = true; this.setState({ loading: true });
this.setState(this.state);
this.fetchUserData(); this.fetchUserData();
} }
@ -736,29 +750,24 @@ export class Profile extends Component<any, ProfileState> {
} }
handleModBanShow(i: Profile) { handleModBanShow(i: Profile) {
i.state.showBanDialog = true; i.setState({ showBanDialog: true });
i.setState(i.state);
} }
handleModBanReasonChange(i: Profile, event: any) { handleModBanReasonChange(i: Profile, event: any) {
i.state.banReason = event.target.value; i.setState({ banReason: event.target.value });
i.setState(i.state);
} }
handleModBanExpireDaysChange(i: Profile, event: any) { handleModBanExpireDaysChange(i: Profile, event: any) {
i.state.banExpireDays = event.target.value; i.setState({ banExpireDays: event.target.value });
i.setState(i.state);
} }
handleModRemoveDataChange(i: Profile, event: any) { handleModRemoveDataChange(i: Profile, event: any) {
i.state.removeData = event.target.checked; i.setState({ removeData: event.target.checked });
i.setState(i.state);
} }
handleModBanSubmitCancel(i: Profile, event?: any) { handleModBanSubmitCancel(i: Profile, event?: any) {
event.preventDefault(); event.preventDefault();
i.state.showBanDialog = false; i.setState({ showBanDialog: false });
i.setState(i.state);
} }
handleModBanSubmit(i: Profile, event?: any) { handleModBanSubmit(i: Profile, event?: any) {
@ -771,7 +780,7 @@ export class Profile extends Component<any, ProfileState> {
// If its an unban, restore all their data // If its an unban, restore all their data
let ban = !person.banned; let ban = !person.banned;
if (ban == false) { if (ban == false) {
i.state.removeData = false; i.setState({ removeData: false });
} }
let form = new BanPerson({ let form = new BanPerson({
person_id: person.id, person_id: person.id,
@ -783,8 +792,7 @@ export class Profile extends Component<any, ProfileState> {
}); });
WebSocketService.Instance.send(wsClient.banPerson(form)); WebSocketService.Instance.send(wsClient.banPerson(form));
i.state.showBanDialog = false; i.setState({ showBanDialog: false });
i.setState(i.state);
}, },
none: void 0, none: void 0,
}); });
@ -809,15 +817,12 @@ export class Profile extends Component<any, ProfileState> {
msg, msg,
GetPersonDetailsResponse GetPersonDetailsResponse
); );
this.state.personRes = Some(data); this.setState({ personRes: Some(data), loading: false });
this.state.loading = false;
this.setPersonBlock(); this.setPersonBlock();
this.setState(this.state);
restoreScrollPosition(this.context); restoreScrollPosition(this.context);
} else if (op == UserOperation.AddAdmin) { } else if (op == UserOperation.AddAdmin) {
let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse); let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
this.state.siteRes.admins = data.admins; this.setState(s => ((s.siteRes.admins = data.admins), s));
this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
createCommentLikeRes( createCommentLikeRes(

View file

@ -75,10 +75,13 @@ export class RegistrationApplications extends Component<
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state.listRegistrationApplicationsResponse = Some( this.state = {
this.isoData.routeData[0] as ListRegistrationApplicationsResponse ...this.state,
); listRegistrationApplicationsResponse: Some(
this.state.loading = false; this.isoData.routeData[0] as ListRegistrationApplicationsResponse
),
loading: false,
};
} else { } else {
this.refetch(); this.refetch();
} }
@ -110,21 +113,21 @@ export class RegistrationApplications extends Component<
render() { render() {
return ( return (
<div class="container"> <div className="container">
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
<div class="row"> <div className="row">
<div class="col-12"> <div className="col-12">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None} description={None}
image={None} image={None}
/> />
<h5 class="mb-2">{i18n.t("registration_applications")}</h5> <h5 className="mb-2">{i18n.t("registration_applications")}</h5>
{this.selects()} {this.selects()}
{this.applicationList()} {this.applicationList()}
<Paginator <Paginator
@ -140,7 +143,7 @@ export class RegistrationApplications extends Component<
unreadOrAllRadios() { unreadOrAllRadios() {
return ( return (
<div class="btn-group btn-group-toggle flex-wrap mb-2"> <div className="btn-group btn-group-toggle flex-wrap mb-2">
<label <label
className={`btn btn-outline-secondary pointer className={`btn btn-outline-secondary pointer
${this.state.unreadOrAll == UnreadOrAll.Unread && "active"} ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
@ -174,7 +177,7 @@ export class RegistrationApplications extends Component<
selects() { selects() {
return ( return (
<div className="mb-2"> <div className="mb-2">
<span class="mr-3">{this.unreadOrAllRadios()}</span> <span className="mr-3">{this.unreadOrAllRadios()}</span>
</div> </div>
); );
} }
@ -199,9 +202,7 @@ export class RegistrationApplications extends Component<
} }
handleUnreadOrAllChange(i: RegistrationApplications, event: any) { handleUnreadOrAllChange(i: RegistrationApplications, event: any) {
i.state.unreadOrAll = Number(event.target.value); i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
i.state.page = 1;
i.setState(i.state);
i.refetch(); i.refetch();
} }
@ -248,10 +249,11 @@ export class RegistrationApplications extends Component<
msg, msg,
ListRegistrationApplicationsResponse ListRegistrationApplicationsResponse
); );
this.state.listRegistrationApplicationsResponse = Some(data); this.setState({
this.state.loading = false; listRegistrationApplicationsResponse: Some(data),
loading: false,
});
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state);
} else if (op == UserOperation.ApproveRegistrationApplication) { } else if (op == UserOperation.ApproveRegistrationApplication) {
let data = wsJsonToRes<RegistrationApplicationResponse>( let data = wsJsonToRes<RegistrationApplicationResponse>(
msg, msg,

View file

@ -8,8 +8,12 @@ import {
ListCommentReportsResponse, ListCommentReportsResponse,
ListPostReports, ListPostReports,
ListPostReportsResponse, ListPostReportsResponse,
ListPrivateMessageReports,
ListPrivateMessageReportsResponse,
PostReportResponse, PostReportResponse,
PostReportView, PostReportView,
PrivateMessageReportResponse,
PrivateMessageReportView,
UserOperation, UserOperation,
wsJsonToRes, wsJsonToRes,
wsUserOp, wsUserOp,
@ -19,6 +23,7 @@ import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
amAdmin,
auth, auth,
fetchLimit, fetchLimit,
isBrowser, isBrowser,
@ -27,6 +32,7 @@ import {
toast, toast,
updateCommentReportRes, updateCommentReportRes,
updatePostReportRes, updatePostReportRes,
updatePrivateMessageReportRes,
wsClient, wsClient,
wsSubscribe, wsSubscribe,
} from "../../utils"; } from "../../utils";
@ -35,6 +41,7 @@ import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import { Paginator } from "../common/paginator"; import { Paginator } from "../common/paginator";
import { PostReport } from "../post/post-report"; import { PostReport } from "../post/post-report";
import { PrivateMessageReport } from "../private_message/private-message-report";
enum UnreadOrAll { enum UnreadOrAll {
Unread, Unread,
@ -45,23 +52,26 @@ enum MessageType {
All, All,
CommentReport, CommentReport,
PostReport, PostReport,
PrivateMessageReport,
} }
enum MessageEnum { enum MessageEnum {
CommentReport, CommentReport,
PostReport, PostReport,
PrivateMessageReport,
} }
type ItemType = { type ItemType = {
id: number; id: number;
type_: MessageEnum; type_: MessageEnum;
view: CommentReportView | PostReportView; view: CommentReportView | PostReportView | PrivateMessageReportView;
published: string; published: string;
}; };
interface ReportsState { interface ReportsState {
listCommentReportsResponse: Option<ListCommentReportsResponse>; listCommentReportsResponse: Option<ListCommentReportsResponse>;
listPostReportsResponse: Option<ListPostReportsResponse>; listPostReportsResponse: Option<ListPostReportsResponse>;
listPrivateMessageReportsResponse: Option<ListPrivateMessageReportsResponse>;
unreadOrAll: UnreadOrAll; unreadOrAll: UnreadOrAll;
messageType: MessageType; messageType: MessageType;
combined: ItemType[]; combined: ItemType[];
@ -74,12 +84,14 @@ export class Reports extends Component<any, ReportsState> {
private isoData = setIsoData( private isoData = setIsoData(
this.context, this.context,
ListCommentReportsResponse, ListCommentReportsResponse,
ListPostReportsResponse ListPostReportsResponse,
ListPrivateMessageReportsResponse
); );
private subscription: Subscription; private subscription: Subscription;
private emptyState: ReportsState = { private emptyState: ReportsState = {
listCommentReportsResponse: None, listCommentReportsResponse: None,
listPostReportsResponse: None, listPostReportsResponse: None,
listPrivateMessageReportsResponse: None,
unreadOrAll: UnreadOrAll.Unread, unreadOrAll: UnreadOrAll.Unread,
messageType: MessageType.All, messageType: MessageType.All,
combined: [], combined: [],
@ -104,14 +116,28 @@ export class Reports extends Component<any, ReportsState> {
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state.listCommentReportsResponse = Some( this.state = {
this.isoData.routeData[0] as ListCommentReportsResponse ...this.state,
); listCommentReportsResponse: Some(
this.state.listPostReportsResponse = Some( this.isoData.routeData[0] as ListCommentReportsResponse
this.isoData.routeData[1] as ListPostReportsResponse ),
); listPostReportsResponse: Some(
this.state.combined = this.buildCombined(); this.isoData.routeData[1] as ListPostReportsResponse
this.state.loading = false; ),
};
if (amAdmin()) {
this.state = {
...this.state,
listPrivateMessageReportsResponse: Some(
this.isoData.routeData[2] as ListPrivateMessageReportsResponse
),
};
}
this.state = {
...this.state,
combined: this.buildCombined(),
loading: false,
};
} else { } else {
this.refetch(); this.refetch();
} }
@ -139,27 +165,29 @@ export class Reports extends Component<any, ReportsState> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
<div class="row"> <div className="row">
<div class="col-12"> <div className="col-12">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None} description={None}
image={None} image={None}
/> />
<h5 class="mb-2">{i18n.t("reports")}</h5> <h5 className="mb-2">{i18n.t("reports")}</h5>
{this.selects()} {this.selects()}
{this.state.messageType == MessageType.All && this.all()} {this.state.messageType == MessageType.All && this.all()}
{this.state.messageType == MessageType.CommentReport && {this.state.messageType == MessageType.CommentReport &&
this.commentReports()} this.commentReports()}
{this.state.messageType == MessageType.PostReport && {this.state.messageType == MessageType.PostReport &&
this.postReports()} this.postReports()}
{this.state.messageType == MessageType.PrivateMessageReport &&
this.privateMessageReports()}
<Paginator <Paginator
page={this.state.page} page={this.state.page}
onChange={this.handlePageChange} onChange={this.handlePageChange}
@ -173,7 +201,7 @@ export class Reports extends Component<any, ReportsState> {
unreadOrAllRadios() { unreadOrAllRadios() {
return ( return (
<div class="btn-group btn-group-toggle flex-wrap mb-2"> <div className="btn-group btn-group-toggle flex-wrap mb-2">
<label <label
className={`btn btn-outline-secondary pointer className={`btn btn-outline-secondary pointer
${this.state.unreadOrAll == UnreadOrAll.Unread && "active"} ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
@ -206,7 +234,7 @@ export class Reports extends Component<any, ReportsState> {
messageTypeRadios() { messageTypeRadios() {
return ( return (
<div class="btn-group btn-group-toggle flex-wrap mb-2"> <div className="btn-group btn-group-toggle flex-wrap mb-2">
<label <label
className={`btn btn-outline-secondary pointer className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.All && "active"} ${this.state.messageType == MessageType.All && "active"}
@ -246,6 +274,26 @@ export class Reports extends Component<any, ReportsState> {
/> />
{i18n.t("posts")} {i18n.t("posts")}
</label> </label>
{amAdmin() && (
<label
className={`btn btn-outline-secondary pointer
${
this.state.messageType == MessageType.PrivateMessageReport &&
"active"
}
`}
>
<input
type="radio"
value={MessageType.PrivateMessageReport}
checked={
this.state.messageType == MessageType.PrivateMessageReport
}
onChange={linkEvent(this, this.handleMessageTypeChange)}
/>
{i18n.t("messages")}
</label>
)}
</div> </div>
); );
} }
@ -253,13 +301,13 @@ export class Reports extends Component<any, ReportsState> {
selects() { selects() {
return ( return (
<div className="mb-2"> <div className="mb-2">
<span class="mr-3">{this.unreadOrAllRadios()}</span> <span className="mr-3">{this.unreadOrAllRadios()}</span>
<span class="mr-3">{this.messageTypeRadios()}</span> <span className="mr-3">{this.messageTypeRadios()}</span>
</div> </div>
); );
} }
replyToReplyType(r: CommentReportView): ItemType { commentReportToItemType(r: CommentReportView): ItemType {
return { return {
id: r.comment_report.id, id: r.comment_report.id,
type_: MessageEnum.CommentReport, type_: MessageEnum.CommentReport,
@ -268,7 +316,7 @@ export class Reports extends Component<any, ReportsState> {
}; };
} }
mentionToReplyType(r: PostReportView): ItemType { postReportToItemType(r: PostReportView): ItemType {
return { return {
id: r.post_report.id, id: r.post_report.id,
type_: MessageEnum.PostReport, type_: MessageEnum.PostReport,
@ -277,17 +325,31 @@ export class Reports extends Component<any, ReportsState> {
}; };
} }
privateMessageReportToItemType(r: PrivateMessageReportView): ItemType {
return {
id: r.private_message_report.id,
type_: MessageEnum.PrivateMessageReport,
view: r,
published: r.private_message_report.published,
};
}
buildCombined(): ItemType[] { buildCombined(): ItemType[] {
let comments: ItemType[] = this.state.listCommentReportsResponse let comments: ItemType[] = this.state.listCommentReportsResponse
.map(r => r.comment_reports) .map(r => r.comment_reports)
.unwrapOr([]) .unwrapOr([])
.map(r => this.replyToReplyType(r)); .map(r => this.commentReportToItemType(r));
let posts: ItemType[] = this.state.listPostReportsResponse let posts: ItemType[] = this.state.listPostReportsResponse
.map(r => r.post_reports) .map(r => r.post_reports)
.unwrapOr([]) .unwrapOr([])
.map(r => this.mentionToReplyType(r)); .map(r => this.postReportToItemType(r));
let privateMessages: ItemType[] =
this.state.listPrivateMessageReportsResponse
.map(r => r.private_message_reports)
.unwrapOr([])
.map(r => this.privateMessageReportToItemType(r));
return [...comments, ...posts].sort((a, b) => return [...comments, ...posts, ...privateMessages].sort((a, b) =>
b.published.localeCompare(a.published) b.published.localeCompare(a.published)
); );
} }
@ -300,6 +362,13 @@ export class Reports extends Component<any, ReportsState> {
); );
case MessageEnum.PostReport: case MessageEnum.PostReport:
return <PostReport key={i.id} report={i.view as PostReportView} />; return <PostReport key={i.id} report={i.view as PostReportView} />;
case MessageEnum.PrivateMessageReport:
return (
<PrivateMessageReport
key={i.id}
report={i.view as PrivateMessageReportView}
/>
);
default: default:
return <div />; return <div />;
} }
@ -350,22 +419,37 @@ export class Reports extends Component<any, ReportsState> {
}); });
} }
privateMessageReports() {
return this.state.listPrivateMessageReportsResponse.match({
some: res => (
<div>
{res.private_message_reports.map(pmr => (
<>
<hr />
<PrivateMessageReport
key={pmr.private_message_report.id}
report={pmr}
/>
</>
))}
</div>
),
none: <></>,
});
}
handlePageChange(page: number) { handlePageChange(page: number) {
this.setState({ page }); this.setState({ page });
this.refetch(); this.refetch();
} }
handleUnreadOrAllChange(i: Reports, event: any) { handleUnreadOrAllChange(i: Reports, event: any) {
i.state.unreadOrAll = Number(event.target.value); i.setState({ unreadOrAll: Number(event.target.value), page: 1 });
i.state.page = 1;
i.setState(i.state);
i.refetch(); i.refetch();
} }
handleMessageTypeChange(i: Reports, event: any) { handleMessageTypeChange(i: Reports, event: any) {
i.state.messageType = Number(event.target.value); i.setState({ messageType: Number(event.target.value), page: 1 });
i.state.page = 1;
i.setState(i.state);
i.refetch(); i.refetch();
} }
@ -398,6 +482,18 @@ export class Reports extends Component<any, ReportsState> {
}); });
promises.push(req.client.listPostReports(postReportsForm)); promises.push(req.client.listPostReports(postReportsForm));
if (amAdmin()) {
let privateMessageReportsForm = new ListPrivateMessageReports({
unresolved_only,
page,
limit,
auth,
});
promises.push(
req.client.listPrivateMessageReports(privateMessageReportsForm)
);
}
return promises; return promises;
} }
@ -428,6 +524,18 @@ export class Reports extends Component<any, ReportsState> {
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.listPostReports(postReportsForm)); WebSocketService.Instance.send(wsClient.listPostReports(postReportsForm));
if (amAdmin()) {
let privateMessageReportsForm = new ListPrivateMessageReports({
unresolved_only,
page,
limit,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(
wsClient.listPrivateMessageReports(privateMessageReportsForm)
);
}
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -443,24 +551,30 @@ export class Reports extends Component<any, ReportsState> {
msg, msg,
ListCommentReportsResponse ListCommentReportsResponse
); );
this.state.listCommentReportsResponse = Some(data); this.setState({ listCommentReportsResponse: Some(data) });
this.state.combined = this.buildCombined(); this.setState({ combined: this.buildCombined(), loading: false });
this.state.loading = false;
// this.sendUnreadCount(); // this.sendUnreadCount();
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.ListPostReports) { } else if (op == UserOperation.ListPostReports) {
let data = wsJsonToRes<ListPostReportsResponse>( let data = wsJsonToRes<ListPostReportsResponse>(
msg, msg,
ListPostReportsResponse ListPostReportsResponse
); );
this.state.listPostReportsResponse = Some(data); this.setState({ listPostReportsResponse: Some(data) });
this.state.combined = this.buildCombined(); this.setState({ combined: this.buildCombined(), loading: false });
this.state.loading = false; // this.sendUnreadCount();
window.scrollTo(0, 0);
setupTippy();
} else if (op == UserOperation.ListPrivateMessageReports) {
let data = wsJsonToRes<ListPrivateMessageReportsResponse>(
msg,
ListPrivateMessageReportsResponse
);
this.setState({ listPrivateMessageReportsResponse: Some(data) });
this.setState({ combined: this.buildCombined(), loading: false });
// this.sendUnreadCount(); // this.sendUnreadCount();
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.ResolvePostReport) { } else if (op == UserOperation.ResolvePostReport) {
let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse); let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
@ -490,6 +604,24 @@ export class Reports extends Component<any, ReportsState> {
urcs.next(urcs.getValue() + 1); urcs.next(urcs.getValue() + 1);
} }
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.ResolvePrivateMessageReport) {
let data = wsJsonToRes<PrivateMessageReportResponse>(
msg,
PrivateMessageReportResponse
);
updatePrivateMessageReportRes(
data.private_message_report_view,
this.state.listPrivateMessageReportsResponse
.map(r => r.private_message_reports)
.unwrapOr([])
);
let urcs = UserService.Instance.unreadReportCountSub;
if (data.private_message_report_view.private_message_report.resolved) {
urcs.next(urcs.getValue() - 1);
} else {
urcs.next(urcs.getValue() + 1);
}
this.setState(this.state);
} }
} }
} }

View file

@ -54,6 +54,7 @@ import {
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
import { ImageUploadForm } from "../common/image-upload-form"; import { ImageUploadForm } from "../common/image-upload-form";
import { LanguageSelect } from "../common/language-select";
import { ListingTypeSelect } from "../common/listing-type-select"; import { ListingTypeSelect } from "../common/listing-type-select";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
import { SortSelect } from "../common/sort-select"; import { SortSelect } from "../common/sort-select";
@ -99,7 +100,8 @@ export class Settings extends Component<any, SettingsState> {
default_sort_type: None, default_sort_type: None,
default_listing_type: None, default_listing_type: None,
theme: None, theme: None,
lang: None, interface_language: None,
discussion_languages: None,
avatar: None, avatar: None,
banner: None, banner: None,
display_name: None, display_name: None,
@ -140,6 +142,8 @@ export class Settings extends Component<any, SettingsState> {
this.handleSortTypeChange = this.handleSortTypeChange.bind(this); this.handleSortTypeChange = this.handleSortTypeChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.bind(this); this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
this.handleBioChange = this.handleBioChange.bind(this); this.handleBioChange = this.handleBioChange.bind(this);
this.handleDiscussionLanguageChange =
this.handleDiscussionLanguageChange.bind(this);
this.handleAvatarUpload = this.handleAvatarUpload.bind(this); this.handleAvatarUpload = this.handleAvatarUpload.bind(this);
this.handleAvatarRemove = this.handleAvatarRemove.bind(this); this.handleAvatarRemove = this.handleAvatarRemove.bind(this);
@ -150,13 +154,44 @@ export class Settings extends Component<any, SettingsState> {
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
this.setUserInfo(); if (UserService.Instance.myUserInfo.isSome()) {
let mui = UserService.Instance.myUserInfo.unwrap();
let luv = mui.local_user_view;
this.state = {
...this.state,
personBlocks: mui.person_blocks,
communityBlocks: mui.community_blocks,
saveUserSettingsForm: {
...this.state.saveUserSettingsForm,
show_nsfw: Some(luv.local_user.show_nsfw),
theme: Some(luv.local_user.theme ? luv.local_user.theme : "browser"),
default_sort_type: Some(luv.local_user.default_sort_type),
default_listing_type: Some(luv.local_user.default_listing_type),
interface_language: Some(luv.local_user.interface_language),
discussion_languages: Some(mui.discussion_languages.map(l => l.id)),
avatar: luv.person.avatar,
banner: luv.person.banner,
display_name: luv.person.display_name,
show_avatars: Some(luv.local_user.show_avatars),
bot_account: Some(luv.person.bot_account),
show_bot_accounts: Some(luv.local_user.show_bot_accounts),
show_scores: Some(luv.local_user.show_scores),
show_read_posts: Some(luv.local_user.show_read_posts),
show_new_post_notifs: Some(luv.local_user.show_new_post_notifs),
email: luv.local_user.email,
bio: luv.person.bio,
send_notifications_to_email: Some(
luv.local_user.send_notifications_to_email
),
matrix_user_id: luv.person.matrix_user_id,
},
};
}
} }
async componentDidMount() { async componentDidMount() {
setupTippy(); setupTippy();
this.state.themeList = await fetchThemeList(); this.setState({ themeList: await fetchThemeList() });
this.setState(this.state);
} }
componentWillUnmount() { componentWillUnmount() {
@ -169,7 +204,7 @@ export class Settings extends Component<any, SettingsState> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
<> <>
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
@ -177,10 +212,10 @@ export class Settings extends Component<any, SettingsState> {
description={Some(this.documentTitle)} description={Some(this.documentTitle)}
image={this.state.saveUserSettingsForm.avatar} image={this.state.saveUserSettingsForm.avatar}
/> />
<ul class="nav nav-tabs mb-2"> <ul className="nav nav-tabs mb-2">
<li class="nav-item"> <li className="nav-item">
<button <button
class={`nav-link btn ${ className={`nav-link btn ${
this.state.currentTab == "settings" && "active" this.state.currentTab == "settings" && "active"
}`} }`}
onClick={linkEvent( onClick={linkEvent(
@ -191,9 +226,9 @@ export class Settings extends Component<any, SettingsState> {
{i18n.t("settings")} {i18n.t("settings")}
</button> </button>
</li> </li>
<li class="nav-item"> <li className="nav-item">
<button <button
class={`nav-link btn ${ className={`nav-link btn ${
this.state.currentTab == "blocks" && "active" this.state.currentTab == "blocks" && "active"
}`} }`}
onClick={linkEvent( onClick={linkEvent(
@ -214,15 +249,15 @@ export class Settings extends Component<any, SettingsState> {
userSettings() { userSettings() {
return ( return (
<div class="row"> <div className="row">
<div class="col-12 col-md-6"> <div className="col-12 col-md-6">
<div class="card border-secondary mb-3"> <div className="card border-secondary mb-3">
<div class="card-body">{this.saveUserSettingsHtmlForm()}</div> <div className="card-body">{this.saveUserSettingsHtmlForm()}</div>
</div> </div>
</div> </div>
<div class="col-12 col-md-6"> <div className="col-12 col-md-6">
<div class="card border-secondary mb-3"> <div className="card border-secondary mb-3">
<div class="card-body">{this.changePasswordHtmlForm()}</div> <div className="card-body">{this.changePasswordHtmlForm()}</div>
</div> </div>
</div> </div>
</div> </div>
@ -231,15 +266,15 @@ export class Settings extends Component<any, SettingsState> {
blockCards() { blockCards() {
return ( return (
<div class="row"> <div className="row">
<div class="col-12 col-md-6"> <div className="col-12 col-md-6">
<div class="card border-secondary mb-3"> <div className="card border-secondary mb-3">
<div class="card-body">{this.blockUserCard()}</div> <div className="card-body">{this.blockUserCard()}</div>
</div> </div>
</div> </div>
<div class="col-12 col-md-6"> <div className="col-12 col-md-6">
<div class="card border-secondary mb-3"> <div className="card border-secondary mb-3">
<div class="card-body">{this.blockCommunityCard()}</div> <div className="card-body">{this.blockCommunityCard()}</div>
</div> </div>
</div> </div>
</div> </div>
@ -251,15 +286,15 @@ export class Settings extends Component<any, SettingsState> {
<> <>
<h5>{i18n.t("change_password")}</h5> <h5>{i18n.t("change_password")}</h5>
<form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}> <form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-5 col-form-label" htmlFor="user-password"> <label className="col-sm-5 col-form-label" htmlFor="user-password">
{i18n.t("new_password")} {i18n.t("new_password")}
</label> </label>
<div class="col-sm-7"> <div className="col-sm-7">
<input <input
type="password" type="password"
id="user-password" id="user-password"
class="form-control" className="form-control"
value={this.state.changePasswordForm.new_password} value={this.state.changePasswordForm.new_password}
autoComplete="new-password" autoComplete="new-password"
maxLength={60} maxLength={60}
@ -267,18 +302,18 @@ export class Settings extends Component<any, SettingsState> {
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label <label
class="col-sm-5 col-form-label" className="col-sm-5 col-form-label"
htmlFor="user-verify-password" htmlFor="user-verify-password"
> >
{i18n.t("verify_password")} {i18n.t("verify_password")}
</label> </label>
<div class="col-sm-7"> <div className="col-sm-7">
<input <input
type="password" type="password"
id="user-verify-password" id="user-verify-password"
class="form-control" className="form-control"
value={this.state.changePasswordForm.new_password_verify} value={this.state.changePasswordForm.new_password_verify}
autoComplete="new-password" autoComplete="new-password"
maxLength={60} maxLength={60}
@ -286,15 +321,18 @@ export class Settings extends Component<any, SettingsState> {
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-5 col-form-label" htmlFor="user-old-password"> <label
className="col-sm-5 col-form-label"
htmlFor="user-old-password"
>
{i18n.t("old_password")} {i18n.t("old_password")}
</label> </label>
<div class="col-sm-7"> <div className="col-sm-7">
<input <input
type="password" type="password"
id="user-old-password" id="user-old-password"
class="form-control" className="form-control"
value={this.state.changePasswordForm.old_password} value={this.state.changePasswordForm.old_password}
autoComplete="new-password" autoComplete="new-password"
maxLength={60} maxLength={60}
@ -302,8 +340,8 @@ export class Settings extends Component<any, SettingsState> {
/> />
</div> </div>
</div> </div>
<div class="form-group"> <div className="form-group">
<button type="submit" class="btn btn-block btn-secondary mr-4"> <button type="submit" className="btn btn-block btn-secondary mr-4">
{this.state.changePasswordLoading ? ( {this.state.changePasswordLoading ? (
<Spinner /> <Spinner />
) : ( ) : (
@ -329,9 +367,9 @@ export class Settings extends Component<any, SettingsState> {
return ( return (
<> <>
<h5>{i18n.t("blocked_users")}</h5> <h5>{i18n.t("blocked_users")}</h5>
<ul class="list-unstyled mb-0"> <ul className="list-unstyled mb-0">
{this.state.personBlocks.map(pb => ( {this.state.personBlocks.map(pb => (
<li> <li key={pb.target.id}>
<span> <span>
<PersonListing person={pb.target} /> <PersonListing person={pb.target} />
<button <button
@ -354,13 +392,16 @@ export class Settings extends Component<any, SettingsState> {
blockUserForm() { blockUserForm() {
return ( return (
<div class="form-group row"> <div className="form-group row">
<label class="col-md-4 col-form-label" htmlFor="block-person-filter"> <label
className="col-md-4 col-form-label"
htmlFor="block-person-filter"
>
{i18n.t("block_user")} {i18n.t("block_user")}
</label> </label>
<div class="col-md-8"> <div className="col-md-8">
<select <select
class="form-control" className="form-control"
id="block-person-filter" id="block-person-filter"
value={this.state.blockPerson.map(p => p.person.id).unwrapOr(0)} value={this.state.blockPerson.map(p => p.person.id).unwrapOr(0)}
> >
@ -392,9 +433,9 @@ export class Settings extends Component<any, SettingsState> {
return ( return (
<> <>
<h5>{i18n.t("blocked_communities")}</h5> <h5>{i18n.t("blocked_communities")}</h5>
<ul class="list-unstyled mb-0"> <ul className="list-unstyled mb-0">
{this.state.communityBlocks.map(cb => ( {this.state.communityBlocks.map(cb => (
<li> <li key={cb.community.id}>
<span> <span>
<CommunityLink community={cb.community} /> <CommunityLink community={cb.community} />
<button <button
@ -417,13 +458,16 @@ export class Settings extends Component<any, SettingsState> {
blockCommunityForm() { blockCommunityForm() {
return ( return (
<div class="form-group row"> <div className="form-group row">
<label class="col-md-4 col-form-label" htmlFor="block-community-filter"> <label
className="col-md-4 col-form-label"
htmlFor="block-community-filter"
>
{i18n.t("block_community")} {i18n.t("block_community")}
</label> </label>
<div class="col-md-8"> <div className="col-md-8">
<select <select
class="form-control" className="form-control"
id="block-community-filter" id="block-community-filter"
value={this.state.blockCommunityId} value={this.state.blockCommunityId}
> >
@ -440,19 +484,21 @@ export class Settings extends Component<any, SettingsState> {
} }
saveUserSettingsHtmlForm() { saveUserSettingsHtmlForm() {
let selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
return ( return (
<> <>
<h5>{i18n.t("settings")}</h5> <h5>{i18n.t("settings")}</h5>
<form onSubmit={linkEvent(this, this.handleSaveSettingsSubmit)}> <form onSubmit={linkEvent(this, this.handleSaveSettingsSubmit)}>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-5 col-form-label" htmlFor="display-name"> <label className="col-sm-5 col-form-label" htmlFor="display-name">
{i18n.t("display_name")} {i18n.t("display_name")}
</label> </label>
<div class="col-sm-7"> <div className="col-sm-7">
<input <input
id="display-name" id="display-name"
type="text" type="text"
class="form-control" className="form-control"
placeholder={i18n.t("optional")} placeholder={i18n.t("optional")}
value={toUndefined( value={toUndefined(
this.state.saveUserSettingsForm.display_name this.state.saveUserSettingsForm.display_name
@ -463,30 +509,32 @@ export class Settings extends Component<any, SettingsState> {
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-3 col-form-label" htmlFor="user-bio"> <label className="col-sm-3 col-form-label" htmlFor="user-bio">
{i18n.t("bio")} {i18n.t("bio")}
</label> </label>
<div class="col-sm-9"> <div className="col-sm-9">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.saveUserSettingsForm.bio} initialContent={this.state.saveUserSettingsForm.bio}
initialLanguageId={None}
onContentChange={this.handleBioChange} onContentChange={this.handleBioChange}
maxLength={Some(300)} maxLength={Some(300)}
placeholder={None} placeholder={None}
buttonTitle={None} buttonTitle={None}
hideNavigationWarnings hideNavigationWarnings
allLanguages={this.state.siteRes.all_languages}
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-3 col-form-label" htmlFor="user-email"> <label className="col-sm-3 col-form-label" htmlFor="user-email">
{i18n.t("email")} {i18n.t("email")}
</label> </label>
<div class="col-sm-9"> <div className="col-sm-9">
<input <input
type="email" type="email"
id="user-email" id="user-email"
class="form-control" className="form-control"
placeholder={i18n.t("optional")} placeholder={i18n.t("optional")}
value={toUndefined(this.state.saveUserSettingsForm.email)} value={toUndefined(this.state.saveUserSettingsForm.email)}
onInput={linkEvent(this, this.handleEmailChange)} onInput={linkEvent(this, this.handleEmailChange)}
@ -494,17 +542,17 @@ export class Settings extends Component<any, SettingsState> {
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-5 col-form-label" htmlFor="matrix-user-id"> <label className="col-sm-5 col-form-label" htmlFor="matrix-user-id">
<a href={elementUrl} rel={relTags}> <a href={elementUrl} rel={relTags}>
{i18n.t("matrix_user_id")} {i18n.t("matrix_user_id")}
</a> </a>
</label> </label>
<div class="col-sm-7"> <div className="col-sm-7">
<input <input
id="matrix-user-id" id="matrix-user-id"
type="text" type="text"
class="form-control" className="form-control"
placeholder="@user:example.com" placeholder="@user:example.com"
value={toUndefined( value={toUndefined(
this.state.saveUserSettingsForm.matrix_user_id this.state.saveUserSettingsForm.matrix_user_id
@ -514,9 +562,9 @@ export class Settings extends Component<any, SettingsState> {
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-3">{i18n.t("avatar")}</label> <label className="col-sm-3">{i18n.t("avatar")}</label>
<div class="col-sm-9"> <div className="col-sm-9">
<ImageUploadForm <ImageUploadForm
uploadTitle={i18n.t("upload_avatar")} uploadTitle={i18n.t("upload_avatar")}
imageSrc={this.state.saveUserSettingsForm.avatar} imageSrc={this.state.saveUserSettingsForm.avatar}
@ -526,9 +574,9 @@ export class Settings extends Component<any, SettingsState> {
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-3">{i18n.t("banner")}</label> <label className="col-sm-3">{i18n.t("banner")}</label>
<div class="col-sm-9"> <div className="col-sm-9">
<ImageUploadForm <ImageUploadForm
uploadTitle={i18n.t("upload_banner")} uploadTitle={i18n.t("upload_banner")}
imageSrc={this.state.saveUserSettingsForm.banner} imageSrc={this.state.saveUserSettingsForm.banner}
@ -537,19 +585,21 @@ export class Settings extends Component<any, SettingsState> {
/> />
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-3" htmlFor="user-language"> <label className="col-sm-3" htmlFor="user-language">
{i18n.t("language")} {i18n.t("interface_language")}
</label> </label>
<div class="col-sm-9"> <div className="col-sm-9">
<select <select
id="user-language" id="user-language"
value={toUndefined(this.state.saveUserSettingsForm.lang)} value={toUndefined(
onChange={linkEvent(this, this.handleLangChange)} this.state.saveUserSettingsForm.interface_language
class="custom-select w-auto" )}
onChange={linkEvent(this, this.handleInterfaceLangChange)}
className="custom-select w-auto"
> >
<option disabled aria-hidden="true"> <option disabled aria-hidden="true">
{i18n.t("language")} {i18n.t("interface_language")}
</option> </option>
<option value="browser">{i18n.t("browser_default")}</option> <option value="browser">{i18n.t("browser_default")}</option>
<option disabled aria-hidden="true"> <option disabled aria-hidden="true">
@ -558,35 +608,45 @@ export class Settings extends Component<any, SettingsState> {
{languages {languages
.sort((a, b) => a.code.localeCompare(b.code)) .sort((a, b) => a.code.localeCompare(b.code))
.map(lang => ( .map(lang => (
<option value={lang.code}>{lang.name}</option> <option key={lang.code} value={lang.code}>
{lang.name}
</option>
))} ))}
</select> </select>
</div> </div>
</div> </div>
<div class="form-group row"> <LanguageSelect
<label class="col-sm-3" htmlFor="user-theme"> allLanguages={this.state.siteRes.all_languages}
selectedLanguageIds={selectedLangs}
multiple={true}
onChange={this.handleDiscussionLanguageChange}
/>
<div className="form-group row">
<label className="col-sm-3" htmlFor="user-theme">
{i18n.t("theme")} {i18n.t("theme")}
</label> </label>
<div class="col-sm-9"> <div className="col-sm-9">
<select <select
id="user-theme" id="user-theme"
value={toUndefined(this.state.saveUserSettingsForm.theme)} value={toUndefined(this.state.saveUserSettingsForm.theme)}
onChange={linkEvent(this, this.handleThemeChange)} onChange={linkEvent(this, this.handleThemeChange)}
class="custom-select w-auto" className="custom-select w-auto"
> >
<option disabled aria-hidden="true"> <option disabled aria-hidden="true">
{i18n.t("theme")} {i18n.t("theme")}
</option> </option>
<option value="browser">{i18n.t("browser_default")}</option> <option value="browser">{i18n.t("browser_default")}</option>
{this.state.themeList.map(theme => ( {this.state.themeList.map(theme => (
<option value={theme}>{theme}</option> <option key={theme} value={theme}>
{theme}
</option>
))} ))}
</select> </select>
</div> </div>
</div> </div>
<form className="form-group row"> <form className="form-group row">
<label class="col-sm-3">{i18n.t("type")}</label> <label className="col-sm-3">{i18n.t("type")}</label>
<div class="col-sm-9"> <div className="col-sm-9">
<ListingTypeSelect <ListingTypeSelect
type_={ type_={
Object.values(ListingType)[ Object.values(ListingType)[
@ -602,8 +662,8 @@ export class Settings extends Component<any, SettingsState> {
</div> </div>
</form> </form>
<form className="form-group row"> <form className="form-group row">
<label class="col-sm-3">{i18n.t("sort_type")}</label> <label className="col-sm-3">{i18n.t("sort_type")}</label>
<div class="col-sm-9"> <div className="col-sm-9">
<SortSelect <SortSelect
sort={ sort={
Object.values(SortType)[ Object.values(SortType)[
@ -617,10 +677,10 @@ export class Settings extends Component<any, SettingsState> {
</div> </div>
</form> </form>
{enableNsfw(this.state.siteRes) && ( {enableNsfw(this.state.siteRes) && (
<div class="form-group"> <div className="form-group">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="user-show-nsfw" id="user-show-nsfw"
type="checkbox" type="checkbox"
checked={toUndefined( checked={toUndefined(
@ -628,16 +688,16 @@ export class Settings extends Component<any, SettingsState> {
)} )}
onChange={linkEvent(this, this.handleShowNsfwChange)} onChange={linkEvent(this, this.handleShowNsfwChange)}
/> />
<label class="form-check-label" htmlFor="user-show-nsfw"> <label className="form-check-label" htmlFor="user-show-nsfw">
{i18n.t("show_nsfw")} {i18n.t("show_nsfw")}
</label> </label>
</div> </div>
</div> </div>
)} )}
<div class="form-group"> <div className="form-group">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="user-show-scores" id="user-show-scores"
type="checkbox" type="checkbox"
checked={toUndefined( checked={toUndefined(
@ -645,15 +705,15 @@ export class Settings extends Component<any, SettingsState> {
)} )}
onChange={linkEvent(this, this.handleShowScoresChange)} onChange={linkEvent(this, this.handleShowScoresChange)}
/> />
<label class="form-check-label" htmlFor="user-show-scores"> <label className="form-check-label" htmlFor="user-show-scores">
{i18n.t("show_scores")} {i18n.t("show_scores")}
</label> </label>
</div> </div>
</div> </div>
<div class="form-group"> <div className="form-group">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="user-show-avatars" id="user-show-avatars"
type="checkbox" type="checkbox"
checked={toUndefined( checked={toUndefined(
@ -661,15 +721,15 @@ export class Settings extends Component<any, SettingsState> {
)} )}
onChange={linkEvent(this, this.handleShowAvatarsChange)} onChange={linkEvent(this, this.handleShowAvatarsChange)}
/> />
<label class="form-check-label" htmlFor="user-show-avatars"> <label className="form-check-label" htmlFor="user-show-avatars">
{i18n.t("show_avatars")} {i18n.t("show_avatars")}
</label> </label>
</div> </div>
</div> </div>
<div class="form-group"> <div className="form-group">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="user-bot-account" id="user-bot-account"
type="checkbox" type="checkbox"
checked={toUndefined( checked={toUndefined(
@ -677,15 +737,15 @@ export class Settings extends Component<any, SettingsState> {
)} )}
onChange={linkEvent(this, this.handleBotAccount)} onChange={linkEvent(this, this.handleBotAccount)}
/> />
<label class="form-check-label" htmlFor="user-bot-account"> <label className="form-check-label" htmlFor="user-bot-account">
{i18n.t("bot_account")} {i18n.t("bot_account")}
</label> </label>
</div> </div>
</div> </div>
<div class="form-group"> <div className="form-group">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="user-show-bot-accounts" id="user-show-bot-accounts"
type="checkbox" type="checkbox"
checked={toUndefined( checked={toUndefined(
@ -693,15 +753,18 @@ export class Settings extends Component<any, SettingsState> {
)} )}
onChange={linkEvent(this, this.handleShowBotAccounts)} onChange={linkEvent(this, this.handleShowBotAccounts)}
/> />
<label class="form-check-label" htmlFor="user-show-bot-accounts"> <label
className="form-check-label"
htmlFor="user-show-bot-accounts"
>
{i18n.t("show_bot_accounts")} {i18n.t("show_bot_accounts")}
</label> </label>
</div> </div>
</div> </div>
<div class="form-group"> <div className="form-group">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="user-show-read-posts" id="user-show-read-posts"
type="checkbox" type="checkbox"
checked={toUndefined( checked={toUndefined(
@ -709,15 +772,18 @@ export class Settings extends Component<any, SettingsState> {
)} )}
onChange={linkEvent(this, this.handleReadPosts)} onChange={linkEvent(this, this.handleReadPosts)}
/> />
<label class="form-check-label" htmlFor="user-show-read-posts"> <label
className="form-check-label"
htmlFor="user-show-read-posts"
>
{i18n.t("show_read_posts")} {i18n.t("show_read_posts")}
</label> </label>
</div> </div>
</div> </div>
<div class="form-group"> <div className="form-group">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="user-show-new-post-notifs" id="user-show-new-post-notifs"
type="checkbox" type="checkbox"
checked={toUndefined( checked={toUndefined(
@ -726,17 +792,17 @@ export class Settings extends Component<any, SettingsState> {
onChange={linkEvent(this, this.handleShowNewPostNotifs)} onChange={linkEvent(this, this.handleShowNewPostNotifs)}
/> />
<label <label
class="form-check-label" className="form-check-label"
htmlFor="user-show-new-post-notifs" htmlFor="user-show-new-post-notifs"
> >
{i18n.t("show_new_post_notifs")} {i18n.t("show_new_post_notifs")}
</label> </label>
</div> </div>
</div> </div>
<div class="form-group"> <div className="form-group">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="user-send-notifications-to-email" id="user-send-notifications-to-email"
type="checkbox" type="checkbox"
disabled={!this.state.saveUserSettingsForm.email} disabled={!this.state.saveUserSettingsForm.email}
@ -749,15 +815,15 @@ export class Settings extends Component<any, SettingsState> {
)} )}
/> />
<label <label
class="form-check-label" className="form-check-label"
htmlFor="user-send-notifications-to-email" htmlFor="user-send-notifications-to-email"
> >
{i18n.t("send_notifications_to_email")} {i18n.t("send_notifications_to_email")}
</label> </label>
</div> </div>
</div> </div>
<div class="form-group"> <div className="form-group">
<button type="submit" class="btn btn-block btn-secondary mr-4"> <button type="submit" className="btn btn-block btn-secondary mr-4">
{this.state.saveUserSettingsLoading ? ( {this.state.saveUserSettingsLoading ? (
<Spinner /> <Spinner />
) : ( ) : (
@ -766,9 +832,9 @@ export class Settings extends Component<any, SettingsState> {
</button> </button>
</div> </div>
<hr /> <hr />
<div class="form-group"> <div className="form-group">
<button <button
class="btn btn-block btn-danger" className="btn btn-block btn-danger"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleDeleteAccountShowConfirmToggle this.handleDeleteAccountShowConfirmToggle
@ -778,7 +844,7 @@ export class Settings extends Component<any, SettingsState> {
</button> </button>
{this.state.deleteAccountShowConfirm && ( {this.state.deleteAccountShowConfirm && (
<> <>
<div class="my-2 alert alert-danger" role="alert"> <div className="my-2 alert alert-danger" role="alert">
{i18n.t("delete_account_confirm")} {i18n.t("delete_account_confirm")}
</div> </div>
<input <input
@ -790,10 +856,10 @@ export class Settings extends Component<any, SettingsState> {
this, this,
this.handleDeleteAccountPasswordChange this.handleDeleteAccountPasswordChange
)} )}
class="form-control my-2" className="form-control my-2"
/> />
<button <button
class="btn btn-danger mr-4" className="btn btn-danger mr-4"
disabled={!this.state.deleteAccountForm.password} disabled={!this.state.deleteAccountForm.password}
onClick={linkEvent(this, this.handleDeleteAccount)} onClick={linkEvent(this, this.handleDeleteAccount)}
> >
@ -804,7 +870,7 @@ export class Settings extends Component<any, SettingsState> {
)} )}
</button> </button>
<button <button
class="btn btn-secondary" className="btn btn-secondary"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleDeleteAccountShowConfirmToggle this.handleDeleteAccountShowConfirmToggle
@ -991,26 +1057,40 @@ export class Settings extends Component<any, SettingsState> {
i.setState(i.state); i.setState(i.state);
} }
handleLangChange(i: Settings, event: any) { handleInterfaceLangChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.lang = Some(event.target.value); i.state.saveUserSettingsForm.interface_language = Some(event.target.value);
i18n.changeLanguage( i18n.changeLanguage(
getLanguages(i.state.saveUserSettingsForm.lang.unwrap())[0] getLanguages(i.state.saveUserSettingsForm.interface_language.unwrap())[0]
); );
i.setState(i.state); i.setState(i.state);
} }
handleSortTypeChange(val: SortType) { handleDiscussionLanguageChange(val: number[]) {
this.state.saveUserSettingsForm.default_sort_type = Some( this.setState(
Object.keys(SortType).indexOf(val) s => ((s.saveUserSettingsForm.discussion_languages = Some(val)), s)
);
}
handleSortTypeChange(val: SortType) {
this.setState(
s => (
(s.saveUserSettingsForm.default_sort_type = Some(
Object.keys(SortType).indexOf(val)
)),
s
)
); );
this.setState(this.state);
} }
handleListingTypeChange(val: ListingType) { handleListingTypeChange(val: ListingType) {
this.state.saveUserSettingsForm.default_listing_type = Some( this.setState(
Object.keys(ListingType).indexOf(val) s => (
(s.saveUserSettingsForm.default_listing_type = Some(
Object.keys(ListingType).indexOf(val)
)),
s
)
); );
this.setState(this.state);
} }
handleEmailChange(i: Settings, event: any) { handleEmailChange(i: Settings, event: any) {
@ -1019,28 +1099,23 @@ export class Settings extends Component<any, SettingsState> {
} }
handleBioChange(val: string) { handleBioChange(val: string) {
this.state.saveUserSettingsForm.bio = Some(val); this.setState(s => ((s.saveUserSettingsForm.bio = Some(val)), s));
this.setState(this.state);
} }
handleAvatarUpload(url: string) { handleAvatarUpload(url: string) {
this.state.saveUserSettingsForm.avatar = Some(url); this.setState(s => ((s.saveUserSettingsForm.avatar = Some(url)), s));
this.setState(this.state);
} }
handleAvatarRemove() { handleAvatarRemove() {
this.state.saveUserSettingsForm.avatar = Some(""); this.setState(s => ((s.saveUserSettingsForm.avatar = Some("")), s));
this.setState(this.state);
} }
handleBannerUpload(url: string) { handleBannerUpload(url: string) {
this.state.saveUserSettingsForm.banner = Some(url); this.setState(s => ((s.saveUserSettingsForm.banner = Some(url)), s));
this.setState(this.state);
} }
handleBannerRemove() { handleBannerRemove() {
this.state.saveUserSettingsForm.banner = Some(""); this.setState(s => ((s.saveUserSettingsForm.banner = Some("")), s));
this.setState(this.state);
} }
handleDisplayNameChange(i: Settings, event: any) { handleDisplayNameChange(i: Settings, event: any) {
@ -1079,30 +1154,26 @@ export class Settings extends Component<any, SettingsState> {
handleSaveSettingsSubmit(i: Settings, event: any) { handleSaveSettingsSubmit(i: Settings, event: any) {
event.preventDefault(); event.preventDefault();
i.state.saveUserSettingsLoading = true; i.setState({ saveUserSettingsLoading: true });
i.state.saveUserSettingsForm.auth = auth().unwrap(); i.setState(s => ((s.saveUserSettingsForm.auth = auth().unwrap()), s));
i.setState(i.state);
WebSocketService.Instance.send( let form = new SaveUserSettings({ ...i.state.saveUserSettingsForm });
wsClient.saveUserSettings(i.state.saveUserSettingsForm) WebSocketService.Instance.send(wsClient.saveUserSettings(form));
);
} }
handleChangePasswordSubmit(i: Settings, event: any) { handleChangePasswordSubmit(i: Settings, event: any) {
event.preventDefault(); event.preventDefault();
i.state.changePasswordLoading = true; i.setState({ changePasswordLoading: true });
i.state.changePasswordForm.auth = auth().unwrap(); i.setState(s => ((s.changePasswordForm.auth = auth().unwrap()), s));
i.setState(i.state);
WebSocketService.Instance.send( let form = new ChangePassword({ ...i.state.changePasswordForm });
wsClient.changePassword(i.state.changePasswordForm)
); WebSocketService.Instance.send(wsClient.changePassword(form));
} }
handleDeleteAccountShowConfirmToggle(i: Settings, event: any) { handleDeleteAccountShowConfirmToggle(i: Settings, event: any) {
event.preventDefault(); event.preventDefault();
i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm; i.setState({ deleteAccountShowConfirm: !i.state.deleteAccountShowConfirm });
i.setState(i.state);
} }
handleDeleteAccountPasswordChange(i: Settings, event: any) { handleDeleteAccountPasswordChange(i: Settings, event: any) {
@ -1112,13 +1183,12 @@ export class Settings extends Component<any, SettingsState> {
handleDeleteAccount(i: Settings, event: any) { handleDeleteAccount(i: Settings, event: any) {
event.preventDefault(); event.preventDefault();
i.state.deleteAccountLoading = true; i.setState({ deleteAccountLoading: true });
i.state.deleteAccountForm.auth = auth().unwrap(); i.setState(s => ((s.deleteAccountForm.auth = auth().unwrap()), s));
i.setState(i.state);
WebSocketService.Instance.send( let form = new DeleteAccount({ ...i.state.deleteAccountForm });
wsClient.deleteAccount(i.state.deleteAccountForm)
); WebSocketService.Instance.send(wsClient.deleteAccount(form));
} }
handleSwitchTab(i: { ctx: Settings; tab: string }) { handleSwitchTab(i: { ctx: Settings; tab: string }) {
@ -1130,58 +1200,6 @@ export class Settings extends Component<any, SettingsState> {
} }
} }
setUserInfo() {
UserService.Instance.myUserInfo.match({
some: mui => {
let luv = mui.local_user_view;
this.state.saveUserSettingsForm.show_nsfw = Some(
luv.local_user.show_nsfw
);
this.state.saveUserSettingsForm.theme = Some(
luv.local_user.theme ? luv.local_user.theme : "browser"
);
this.state.saveUserSettingsForm.default_sort_type = Some(
luv.local_user.default_sort_type
);
this.state.saveUserSettingsForm.default_listing_type = Some(
luv.local_user.default_listing_type
);
this.state.saveUserSettingsForm.lang = Some(luv.local_user.lang);
this.state.saveUserSettingsForm.avatar = luv.person.avatar;
this.state.saveUserSettingsForm.banner = luv.person.banner;
this.state.saveUserSettingsForm.display_name = luv.person.display_name;
this.state.saveUserSettingsForm.show_avatars = Some(
luv.local_user.show_avatars
);
this.state.saveUserSettingsForm.bot_account = Some(
luv.person.bot_account
);
this.state.saveUserSettingsForm.show_bot_accounts = Some(
luv.local_user.show_bot_accounts
);
this.state.saveUserSettingsForm.show_scores = Some(
luv.local_user.show_scores
);
this.state.saveUserSettingsForm.show_read_posts = Some(
luv.local_user.show_read_posts
);
this.state.saveUserSettingsForm.show_new_post_notifs = Some(
luv.local_user.show_new_post_notifs
);
this.state.saveUserSettingsForm.email = luv.local_user.email;
this.state.saveUserSettingsForm.bio = luv.person.bio;
this.state.saveUserSettingsForm.send_notifications_to_email = Some(
luv.local_user.send_notifications_to_email
);
this.state.saveUserSettingsForm.matrix_user_id =
luv.person.matrix_user_id;
this.state.personBlocks = mui.person_blocks;
this.state.communityBlocks = mui.community_blocks;
},
none: void 0,
});
}
parseMessage(msg: any) { parseMessage(msg: any) {
let op = wsUserOp(msg); let op = wsUserOp(msg);
console.log(msg); console.log(msg);
@ -1196,15 +1214,13 @@ export class Settings extends Component<any, SettingsState> {
} else if (op == UserOperation.SaveUserSettings) { } else if (op == UserOperation.SaveUserSettings) {
let data = wsJsonToRes<LoginResponse>(msg, LoginResponse); let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
UserService.Instance.login(data); UserService.Instance.login(data);
this.state.saveUserSettingsLoading = false; this.setState({ saveUserSettingsLoading: false });
this.setState(this.state);
toast(i18n.t("saved")); toast(i18n.t("saved"));
window.scrollTo(0, 0); window.scrollTo(0, 0);
} else if (op == UserOperation.ChangePassword) { } else if (op == UserOperation.ChangePassword) {
let data = wsJsonToRes<LoginResponse>(msg, LoginResponse); let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
UserService.Instance.login(data); UserService.Instance.login(data);
this.state.changePasswordLoading = false; this.setState({ changePasswordLoading: false });
this.setState(this.state);
window.scrollTo(0, 0); window.scrollTo(0, 0);
toast(i18n.t("password_changed")); toast(i18n.t("password_changed"));
} else if (op == UserOperation.DeleteAccount) { } else if (op == UserOperation.DeleteAccount) {

View file

@ -66,15 +66,15 @@ export class VerifyEmail extends Component<any, State> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None} description={None}
image={None} image={None}
/> />
<div class="row"> <div className="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t("verify_email")}</h5> <h5>{i18n.t("verify_email")}</h5>
</div> </div>
</div> </div>
@ -94,8 +94,7 @@ export class VerifyEmail extends Component<any, State> {
let data = wsJsonToRes<VerifyEmailResponse>(msg, VerifyEmailResponse); let data = wsJsonToRes<VerifyEmailResponse>(msg, VerifyEmailResponse);
if (data) { if (data) {
toast(i18n.t("email_verified")); toast(i18n.t("email_verified"));
this.state = this.emptyState; this.setState(this.emptyState);
this.setState(this.state);
this.props.history.push("/login"); this.props.history.push("/login");
} }
} }

View file

@ -50,23 +50,27 @@ export class CreatePost extends Component<any, CreatePostState> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.handlePostCreate = this.handlePostCreate.bind(this);
this.state = this.emptyState; this.state = this.emptyState;
this.handlePostCreate = this.handlePostCreate.bind(this);
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
if (UserService.Instance.myUserInfo.isNone() && isBrowser()) { if (UserService.Instance.myUserInfo.isNone() && isBrowser()) {
toast(i18n.t("not_logged_in"), "danger"); toast(i18n.t("not_logged_in"), "danger");
this.context.router.history.push(`/login`); this.context.router.history.push(`/login`);
} }
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state.listCommunitiesResponse = Some( this.state = {
this.isoData.routeData[0] as ListCommunitiesResponse ...this.state,
); listCommunitiesResponse: Some(
this.state.loading = false; this.isoData.routeData[0] as ListCommunitiesResponse
),
loading: false,
};
} else { } else {
this.refetch(); this.refetch();
} }
@ -123,7 +127,7 @@ export class CreatePost extends Component<any, CreatePostState> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
@ -137,8 +141,8 @@ export class CreatePost extends Component<any, CreatePostState> {
) : ( ) : (
this.state.listCommunitiesResponse.match({ this.state.listCommunitiesResponse.match({
some: res => ( some: res => (
<div class="row"> <div className="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t("create_post")}</h5> <h5>{i18n.t("create_post")}</h5>
<PostForm <PostForm
post_view={None} post_view={None}
@ -147,6 +151,7 @@ export class CreatePost extends Component<any, CreatePostState> {
params={Some(this.params)} params={Some(this.params)}
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)} enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
/> />
</div> </div>
</div> </div>
@ -230,16 +235,15 @@ export class CreatePost extends Component<any, CreatePostState> {
msg, msg,
ListCommunitiesResponse ListCommunitiesResponse
); );
this.state.listCommunitiesResponse = Some(data); this.setState({ listCommunitiesResponse: Some(data), loading: false });
this.state.loading = false;
this.setState(this.state);
} else if (op == UserOperation.GetCommunity) { } else if (op == UserOperation.GetCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse); let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
this.state.listCommunitiesResponse = Some({ this.setState({
communities: [data.community_view], listCommunitiesResponse: Some({
communities: [data.community_view],
}),
loading: false,
}); });
this.state.loading = false;
this.setState(this.state);
} }
} }
} }

View file

@ -34,27 +34,33 @@ export class MetadataCard extends Component<
some: embedTitle => some: embedTitle =>
post.url.match({ post.url.match({
some: url => ( some: url => (
<div class="card border-secondary mt-3 mb-2"> <div className="card border-secondary mt-3 mb-2">
<div class="row"> <div className="row">
<div class="col-12"> <div className="col-12">
<div class="card-body"> <div className="card-body">
{post.name !== embedTitle && [ {post.name !== embedTitle && (
<h5 class="card-title d-inline"> <>
<a class="text-body" href={url} rel={relTags}> <h5 className="card-title d-inline">
{embedTitle} <a
</a> className="text-body"
</h5>, href={url}
<span class="d-inline-block ml-2 mb-2 small text-muted"> rel={relTags}
<a >
class="text-muted font-italic" {embedTitle}
href={url} </a>
rel={relTags} </h5>
> <span className="d-inline-block ml-2 mb-2 small text-muted">
{new URL(url).hostname} <a
<Icon icon="external-link" classes="ml-1" /> className="text-muted font-italic"
</a> href={url}
</span>, rel={relTags}
]} >
{new URL(url).hostname}
<Icon icon="external-link" classes="ml-1" />
</a>
</span>
</>
)}
{post.embed_description.match({ {post.embed_description.match({
some: desc => ( some: desc => (
<div <div
@ -66,9 +72,9 @@ export class MetadataCard extends Component<
), ),
none: <></>, none: <></>,
})} })}
{post.embed_html.isSome() && ( {post.embed_video_url.isSome() && (
<button <button
class="mt-2 btn btn-secondary text-monospace" className="mt-2 btn btn-secondary text-monospace"
onClick={linkEvent(this, this.handleIframeExpand)} onClick={linkEvent(this, this.handleIframeExpand)}
data-tippy-content={i18n.t("expand_here")} data-tippy-content={i18n.t("expand_here")}
> >
@ -85,10 +91,10 @@ export class MetadataCard extends Component<
none: <></>, none: <></>,
})} })}
{this.state.expanded && {this.state.expanded &&
post.embed_html.match({ post.embed_video_url.match({
some: html => ( some: html => (
<div <div
class="mt-3 mb-2" className="mt-3 mb-2"
dangerouslySetInnerHTML={{ __html: html }} dangerouslySetInnerHTML={{ __html: html }}
/> />
), ),
@ -99,7 +105,6 @@ export class MetadataCard extends Component<
} }
handleIframeExpand(i: MetadataCard) { handleIframeExpand(i: MetadataCard) {
i.state.expanded = !i.state.expanded; i.setState({ expanded: !i.state.expanded });
i.setState(i.state);
} }
} }

View file

@ -6,6 +6,7 @@ import {
CommunityView, CommunityView,
CreatePost, CreatePost,
EditPost, EditPost,
Language,
ListingType, ListingType,
PostResponse, PostResponse,
PostView, PostView,
@ -36,6 +37,7 @@ import {
ghostArchiveUrl, ghostArchiveUrl,
isBrowser, isBrowser,
isImage, isImage,
myFirstDiscussionLanguageId,
pictrsDeleteToast, pictrsDeleteToast,
relTags, relTags,
setupTippy, setupTippy,
@ -48,6 +50,7 @@ import {
wsSubscribe, wsSubscribe,
} from "../../utils"; } from "../../utils";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
import { LanguageSelect } from "../common/language-select";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
import { PostListings } from "./post-listings"; import { PostListings } from "./post-listings";
@ -60,6 +63,7 @@ const MAX_POST_TITLE_LENGTH = 200;
interface PostFormProps { interface PostFormProps {
post_view: Option<PostView>; // If a post is given, that means this is an edit post_view: Option<PostView>; // If a post is given, that means this is an edit
allLanguages: Language[];
communities: Option<CommunityView[]>; communities: Option<CommunityView[]>;
params: Option<PostFormParams>; params: Option<PostFormParams>;
onCancel?(): any; onCancel?(): any;
@ -76,6 +80,7 @@ interface PostFormState {
crossPosts: Option<PostView[]>; crossPosts: Option<PostView[]>;
loading: boolean; loading: boolean;
imageLoading: boolean; imageLoading: boolean;
communitySearchLoading: boolean;
previewMode: boolean; previewMode: boolean;
} }
@ -90,10 +95,12 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
url: None, url: None,
body: None, body: None,
honeypot: None, honeypot: None,
language_id: None,
auth: undefined, auth: undefined,
}), }),
loading: false, loading: false,
imageLoading: false, imageLoading: false,
communitySearchLoading: false,
previewMode: false, previewMode: false,
suggestedTitle: None, suggestedTitle: None,
suggestedPosts: None, suggestedPosts: None,
@ -105,35 +112,44 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.fetchSimilarPosts = debounce(this.fetchSimilarPosts.bind(this)); this.fetchSimilarPosts = debounce(this.fetchSimilarPosts.bind(this));
this.fetchPageTitle = debounce(this.fetchPageTitle.bind(this)); this.fetchPageTitle = debounce(this.fetchPageTitle.bind(this));
this.handlePostBodyChange = this.handlePostBodyChange.bind(this); this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
this.handleLanguageChange = this.handleLanguageChange.bind(this);
this.state = this.emptyState; this.state = this.emptyState;
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
// Means its an edit // Means its an edit
this.props.post_view.match({ if (this.props.post_view.isSome()) {
some: pv => let pv = this.props.post_view.unwrap();
(this.state.postForm = new CreatePost({
this.state = {
...this.state,
postForm: new CreatePost({
body: pv.post.body, body: pv.post.body,
name: pv.post.name, name: pv.post.name,
community_id: pv.community.id, community_id: pv.community.id,
url: pv.post.url, url: pv.post.url,
nsfw: Some(pv.post.nsfw), nsfw: Some(pv.post.nsfw),
honeypot: None, honeypot: None,
language_id: Some(pv.post.language_id),
auth: auth().unwrap(), auth: auth().unwrap(),
})), }),
none: void 0, };
}); }
this.props.params.match({ if (this.props.params.isSome()) {
some: params => { let params = this.props.params.unwrap();
this.state.postForm.name = toUndefined(params.name); this.state = {
this.state.postForm.url = params.url; ...this.state,
this.state.postForm.body = params.body; postForm: {
}, ...this.state.postForm,
none: void 0, name: toUndefined(params.name),
}); url: params.url,
body: params.body,
this.parseMessage = this.parseMessage.bind(this); },
this.subscription = wsSubscribe(this.parseMessage); };
}
} }
componentDidMount() { componentDidMount() {
@ -165,6 +181,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} }
render() { render() {
let selectedLangs = this.state.postForm.language_id
.or(myFirstDiscussionLanguageId(UserService.Instance.myUserInfo))
.map(Array.of);
return ( return (
<div> <div>
<Prompt <Prompt
@ -177,15 +197,15 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
message={i18n.t("block_leaving")} message={i18n.t("block_leaving")}
/> />
<form onSubmit={linkEvent(this, this.handlePostSubmit)}> <form onSubmit={linkEvent(this, this.handlePostSubmit)}>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="post-url"> <label className="col-sm-2 col-form-label" htmlFor="post-url">
{i18n.t("url")} {i18n.t("url")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<input <input
type="url" type="url"
id="post-url" id="post-url"
class="form-control" className="form-control"
value={toUndefined(this.state.postForm.url)} value={toUndefined(this.state.postForm.url)}
onInput={linkEvent(this, this.handlePostUrlChange)} onInput={linkEvent(this, this.handlePostUrlChange)}
onPaste={linkEvent(this, this.handleImageUploadPaste)} onPaste={linkEvent(this, this.handleImageUploadPaste)}
@ -193,7 +213,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
{this.state.suggestedTitle.match({ {this.state.suggestedTitle.match({
some: title => ( some: title => (
<div <div
class="mt-1 text-muted small font-weight-bold pointer" className="mt-1 text-muted small font-weight-bold pointer"
role="button" role="button"
onClick={linkEvent(this, this.copySuggestedTitle)} onClick={linkEvent(this, this.copySuggestedTitle)}
> >
@ -217,7 +237,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
type="file" type="file"
accept="image/*,video/*" accept="image/*,video/*"
name="file" name="file"
class="d-none" className="d-none"
disabled={UserService.Instance.myUserInfo.isNone()} disabled={UserService.Instance.myUserInfo.isNone()}
onChange={linkEvent(this, this.handleImageUpload)} onChange={linkEvent(this, this.handleImageUpload)}
/> />
@ -230,7 +250,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
href={`${webArchiveUrl}/save/${encodeURIComponent( href={`${webArchiveUrl}/save/${encodeURIComponent(
url url
)}`} )}`}
class="mr-2 d-inline-block float-right text-muted small font-weight-bold" className="mr-2 d-inline-block float-right text-muted small font-weight-bold"
rel={relTags} rel={relTags}
> >
archive.org {i18n.t("archive_link")} archive.org {i18n.t("archive_link")}
@ -239,7 +259,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
href={`${ghostArchiveUrl}/search?term=${encodeURIComponent( href={`${ghostArchiveUrl}/search?term=${encodeURIComponent(
url url
)}`} )}`}
class="mr-2 d-inline-block float-right text-muted small font-weight-bold" className="mr-2 d-inline-block float-right text-muted small font-weight-bold"
rel={relTags} rel={relTags}
> >
ghostarchive.org {i18n.t("archive_link")} ghostarchive.org {i18n.t("archive_link")}
@ -248,7 +268,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
href={`${archiveTodayUrl}/?run=1&url=${encodeURIComponent( href={`${archiveTodayUrl}/?run=1&url=${encodeURIComponent(
url url
)}`} )}`}
class="mr-2 d-inline-block float-right text-muted small font-weight-bold" className="mr-2 d-inline-block float-right text-muted small font-weight-bold"
rel={relTags} rel={relTags}
> >
archive.today {i18n.t("archive_link")} archive.today {i18n.t("archive_link")}
@ -260,14 +280,16 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
{this.state.imageLoading && <Spinner />} {this.state.imageLoading && <Spinner />}
{this.state.postForm.url.match({ {this.state.postForm.url.match({
some: url => some: url =>
isImage(url) && <img src={url} class="img-fluid" alt="" />, isImage(url) && (
<img src={url} className="img-fluid" alt="" />
),
none: <></>, none: <></>,
})} })}
{this.state.crossPosts.match({ {this.state.crossPosts.match({
some: xPosts => some: xPosts =>
xPosts.length > 0 && ( xPosts.length > 0 && (
<> <>
<div class="my-1 text-muted small font-weight-bold"> <div className="my-1 text-muted small font-weight-bold">
{i18n.t("cross_posts")} {i18n.t("cross_posts")}
</div> </div>
<PostListings <PostListings
@ -275,6 +297,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
posts={xPosts} posts={xPosts}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
allLanguages={this.props.allLanguages}
/> />
</> </>
), ),
@ -282,16 +305,16 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
})} })}
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="post-title"> <label className="col-sm-2 col-form-label" htmlFor="post-title">
{i18n.t("title")} {i18n.t("title")}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<textarea <textarea
value={this.state.postForm.name} value={this.state.postForm.name}
id="post-title" id="post-title"
onInput={linkEvent(this, this.handlePostNameChange)} onInput={linkEvent(this, this.handlePostNameChange)}
class={`form-control ${ className={`form-control ${
!validTitle(this.state.postForm.name) && "is-invalid" !validTitle(this.state.postForm.name) && "is-invalid"
}`} }`}
required required
@ -300,7 +323,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
maxLength={MAX_POST_TITLE_LENGTH} maxLength={MAX_POST_TITLE_LENGTH}
/> />
{!validTitle(this.state.postForm.name) && ( {!validTitle(this.state.postForm.name) && (
<div class="invalid-feedback"> <div className="invalid-feedback">
{i18n.t("invalid_post_title")} {i18n.t("invalid_post_title")}
</div> </div>
)} )}
@ -308,7 +331,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
some: sPosts => some: sPosts =>
sPosts.length > 0 && ( sPosts.length > 0 && (
<> <>
<div class="my-1 text-muted small font-weight-bold"> <div className="my-1 text-muted small font-weight-bold">
{i18n.t("related_posts")} {i18n.t("related_posts")}
</div> </div>
<PostListings <PostListings
@ -316,6 +339,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
posts={sPosts} posts={sPosts}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
allLanguages={this.props.allLanguages}
/> />
</> </>
), ),
@ -324,33 +348,42 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</div> </div>
</div> </div>
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label">{i18n.t("body")}</label> <label className="col-sm-2 col-form-label">{i18n.t("body")}</label>
<div class="col-sm-10"> <div className="col-sm-10">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.postForm.body} initialContent={this.state.postForm.body}
initialLanguageId={None}
onContentChange={this.handlePostBodyChange} onContentChange={this.handlePostBodyChange}
placeholder={None} placeholder={None}
buttonTitle={None} buttonTitle={None}
maxLength={None} maxLength={None}
allLanguages={this.props.allLanguages}
/> />
</div> </div>
</div> </div>
{this.props.post_view.isNone() && ( {this.props.post_view.isNone() && (
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="post-community"> <label
{i18n.t("community")} className="col-sm-2 col-form-label"
htmlFor="post-community"
>
{this.state.communitySearchLoading ? (
<Spinner />
) : (
i18n.t("community")
)}
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<select <select
class="form-control" className="form-control"
id="post-community" id="post-community"
value={this.state.postForm.community_id} value={this.state.postForm.community_id}
onInput={linkEvent(this, this.handlePostCommunityChange)} onInput={linkEvent(this, this.handlePostCommunityChange)}
> >
<option>{i18n.t("select_a_community")}</option> <option>{i18n.t("select_a_community")}</option>
{this.props.communities.unwrapOr([]).map(cv => ( {this.props.communities.unwrapOr([]).map(cv => (
<option value={cv.community.id}> <option key={cv.community.id} value={cv.community.id}>
{communitySelectName(cv)} {communitySelectName(cv)}
</option> </option>
))} ))}
@ -359,14 +392,14 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</div> </div>
)} )}
{this.props.enableNsfw && ( {this.props.enableNsfw && (
<div class="form-group row"> <div className="form-group row">
<legend class="col-form-label col-sm-2 pt-0"> <legend className="col-form-label col-sm-2 pt-0">
{i18n.t("nsfw")} {i18n.t("nsfw")}
</legend> </legend>
<div class="col-sm-10"> <div className="col-sm-10">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input position-static" className="form-check-input position-static"
id="post-nsfw" id="post-nsfw"
type="checkbox" type="checkbox"
checked={toUndefined(this.state.postForm.nsfw)} checked={toUndefined(this.state.postForm.nsfw)}
@ -376,24 +409,30 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</div> </div>
</div> </div>
)} )}
<LanguageSelect
allLanguages={this.props.allLanguages}
selectedLanguageIds={selectedLangs}
multiple={false}
onChange={this.handleLanguageChange}
/>
<input <input
tabIndex={-1} tabIndex={-1}
autoComplete="false" autoComplete="false"
name="a_password" name="a_password"
type="text" type="text"
class="form-control honeypot" className="form-control honeypot"
id="register-honey" id="register-honey"
value={toUndefined(this.state.postForm.honeypot)} value={toUndefined(this.state.postForm.honeypot)}
onInput={linkEvent(this, this.handleHoneyPotChange)} onInput={linkEvent(this, this.handleHoneyPotChange)}
/> />
<div class="form-group row"> <div className="form-group row">
<div class="col-sm-10"> <div className="col-sm-10">
<button <button
disabled={ disabled={
!this.state.postForm.community_id || this.state.loading !this.state.postForm.community_id || this.state.loading
} }
type="submit" type="submit"
class="btn btn-secondary mr-2" className="btn btn-secondary mr-2"
> >
{this.state.loading ? ( {this.state.loading ? (
<Spinner /> <Spinner />
@ -406,7 +445,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
{this.props.post_view.isSome() && ( {this.props.post_view.isSome() && (
<button <button
type="button" type="button"
class="btn btn-secondary" className="btn btn-secondary"
onClick={linkEvent(this, this.handleCancel)} onClick={linkEvent(this, this.handleCancel)}
> >
{i18n.t("cancel")} {i18n.t("cancel")}
@ -422,12 +461,14 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
handlePostSubmit(i: PostForm, event: any) { handlePostSubmit(i: PostForm, event: any) {
event.preventDefault(); event.preventDefault();
i.setState({ loading: true });
// Coerce empty url string to undefined // Coerce empty url string to undefined
if ( if (
i.state.postForm.url.isSome() && i.state.postForm.url.isSome() &&
i.state.postForm.url.unwrapOr("blank") === "" i.state.postForm.url.unwrapOr("blank") === ""
) { ) {
i.state.postForm.url = None; i.setState(s => ((s.postForm.url = None), s));
} }
let pForm = i.state.postForm; let pForm = i.state.postForm;
@ -439,37 +480,39 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
body: pForm.body, body: pForm.body,
nsfw: pForm.nsfw, nsfw: pForm.nsfw,
post_id: pv.post.id, post_id: pv.post.id,
language_id: Some(pv.post.language_id),
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.editPost(form)); WebSocketService.Instance.send(wsClient.editPost(form));
}, },
none: () => { none: () => {
i.state.postForm.auth = auth().unwrap(); i.setState(s => ((s.postForm.auth = auth().unwrap()), s));
WebSocketService.Instance.send(wsClient.createPost(i.state.postForm)); let form = new CreatePost({ ...i.state.postForm });
WebSocketService.Instance.send(wsClient.createPost(form));
}, },
}); });
i.state.loading = true;
i.setState(i.state);
} }
copySuggestedTitle(i: PostForm) { copySuggestedTitle(i: PostForm) {
i.state.suggestedTitle.match({ i.state.suggestedTitle.match({
some: sTitle => { some: sTitle => {
i.state.postForm.name = sTitle.substring(0, MAX_POST_TITLE_LENGTH); i.setState(
i.state.suggestedTitle = None; s => (
(s.postForm.name = sTitle.substring(0, MAX_POST_TITLE_LENGTH)), s
)
);
i.setState({ suggestedTitle: None });
setTimeout(() => { setTimeout(() => {
let textarea: any = document.getElementById("post-title"); let textarea: any = document.getElementById("post-title");
autosize.update(textarea); autosize.update(textarea);
}, 10); }, 10);
i.setState(i.state);
}, },
none: void 0, none: void 0,
}); });
} }
handlePostUrlChange(i: PostForm, event: any) { handlePostUrlChange(i: PostForm, event: any) {
i.state.postForm.url = Some(event.target.value); i.setState(s => ((s.postForm.url = Some(event.target.value)), s));
i.setState(i.state);
i.fetchPageTitle(); i.fetchPageTitle();
} }
@ -494,12 +537,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
// Fetch the page title // Fetch the page title
getSiteMetadata(url).then(d => { getSiteMetadata(url).then(d => {
this.state.suggestedTitle = d.metadata.title; this.setState({ suggestedTitle: d.metadata.title });
this.setState(this.state);
}); });
} else { } else {
this.state.suggestedTitle = None; this.setState({ suggestedTitle: None, crossPosts: None });
this.state.crossPosts = None;
} }
}, },
none: void 0, none: void 0,
@ -507,8 +548,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} }
handlePostNameChange(i: PostForm, event: any) { handlePostNameChange(i: PostForm, event: any) {
i.state.postForm.name = event.target.value; i.setState(s => ((s.postForm.name = event.target.value), s));
i.setState(i.state);
i.fetchSimilarPosts(); i.fetchSimilarPosts();
} }
@ -529,30 +569,30 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
if (this.state.postForm.name !== "") { if (this.state.postForm.name !== "") {
WebSocketService.Instance.send(wsClient.search(form)); WebSocketService.Instance.send(wsClient.search(form));
} else { } else {
this.state.suggestedPosts = None; this.setState({ suggestedPosts: None });
} }
this.setState(this.state);
} }
handlePostBodyChange(val: string) { handlePostBodyChange(val: string) {
this.state.postForm.body = Some(val); this.setState(s => ((s.postForm.body = Some(val)), s));
this.setState(this.state);
} }
handlePostCommunityChange(i: PostForm, event: any) { handlePostCommunityChange(i: PostForm, event: any) {
i.state.postForm.community_id = Number(event.target.value); i.setState(
i.setState(i.state); s => ((s.postForm.community_id = Number(event.target.value)), s)
);
} }
handlePostNsfwChange(i: PostForm, event: any) { handlePostNsfwChange(i: PostForm, event: any) {
i.state.postForm.nsfw = Some(event.target.checked); i.setState(s => ((s.postForm.nsfw = Some(event.target.checked)), s));
i.setState(i.state); }
handleLanguageChange(val: number[]) {
this.setState(s => ((s.postForm.language_id = Some(val[0])), s));
} }
handleHoneyPotChange(i: PostForm, event: any) { handleHoneyPotChange(i: PostForm, event: any) {
i.state.postForm.honeypot = Some(event.target.value); i.setState(s => ((s.postForm.honeypot = Some(event.target.value)), s));
i.setState(i.state);
} }
handleCancel(i: PostForm) { handleCancel(i: PostForm) {
@ -561,8 +601,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
handlePreviewToggle(i: PostForm, event: any) { handlePreviewToggle(i: PostForm, event: any) {
event.preventDefault(); event.preventDefault();
i.state.previewMode = !i.state.previewMode; i.setState({ previewMode: !i.state.previewMode });
i.setState(i.state);
} }
handleImageUploadPaste(i: PostForm, event: any) { handleImageUploadPaste(i: PostForm, event: any) {
@ -584,8 +623,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
const formData = new FormData(); const formData = new FormData();
formData.append("images[]", file); formData.append("images[]", file);
i.state.imageLoading = true; i.setState({ imageLoading: true });
i.setState(i.state);
fetch(pictrsUri, { fetch(pictrsUri, {
method: "POST", method: "POST",
@ -601,22 +639,19 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
let deleteToken = res.files[0].delete_token; let deleteToken = res.files[0].delete_token;
let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`; let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`;
i.state.postForm.url = Some(url); i.state.postForm.url = Some(url);
i.state.imageLoading = false; i.setState({ imageLoading: false });
i.setState(i.state);
pictrsDeleteToast( pictrsDeleteToast(
i18n.t("click_to_delete_picture"), i18n.t("click_to_delete_picture"),
i18n.t("picture_deleted"), i18n.t("picture_deleted"),
deleteUrl deleteUrl
); );
} else { } else {
i.state.imageLoading = false; i.setState({ imageLoading: false });
i.setState(i.state);
toast(JSON.stringify(res), "danger"); toast(JSON.stringify(res), "danger");
} }
}) })
.catch(error => { .catch(error => {
i.state.imageLoading = false; i.setState({ imageLoading: false });
i.setState(i.state);
console.error(error); console.error(error);
toast(error, "danger"); toast(error, "danger");
}); });
@ -631,11 +666,17 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.choices.passedElement.element.addEventListener( this.choices.passedElement.element.addEventListener(
"choice", "choice",
(e: any) => { (e: any) => {
this.state.postForm.community_id = Number(e.detail.choice.value); this.setState(
this.setState(this.state); s => (
(s.postForm.community_id = Number(e.detail.choice.value)), s
)
);
}, },
false false
); );
this.choices.passedElement.element.addEventListener("search", () => {
this.setState({ communitySearchLoading: true });
});
this.choices.passedElement.element.addEventListener( this.choices.passedElement.element.addEventListener(
"search", "search",
debounce(async (e: any) => { debounce(async (e: any) => {
@ -648,6 +689,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
"label", "label",
true true
); );
this.setState({ communitySearchLoading: false });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
@ -658,7 +700,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} }
this.props.post_view.match({ this.props.post_view.match({
some: pv => (this.state.postForm.community_id = pv.community.id), some: pv =>
this.setState(s => ((s.postForm.community_id = pv.community.id), s)),
none: void 0, none: void 0,
}); });
this.props.params.match({ this.props.params.match({
@ -670,9 +713,12 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
let foundCommunityId = this.props.communities let foundCommunityId = this.props.communities
.unwrapOr([]) .unwrapOr([])
.find(r => r.community.name == name).community.id; .find(r => r.community.name == name).community.id;
this.state.postForm.community_id = foundCommunityId; this.setState(
s => ((s.postForm.community_id = foundCommunityId), s)
);
}, },
right: id => (this.state.postForm.community_id = id), right: id =>
this.setState(s => ((s.postForm.community_id = id), s)),
}), }),
none: void 0, none: void 0,
}), }),
@ -693,15 +739,13 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
if (msg.error) { if (msg.error) {
// Errors handled by top level pages // Errors handled by top level pages
// toast(i18n.t(msg.error), "danger"); // toast(i18n.t(msg.error), "danger");
this.state.loading = false; this.setState({ loading: false });
this.setState(this.state);
return; return;
} else if (op == UserOperation.CreatePost) { } else if (op == UserOperation.CreatePost) {
let data = wsJsonToRes<PostResponse>(msg, PostResponse); let data = wsJsonToRes<PostResponse>(msg, PostResponse);
UserService.Instance.myUserInfo.match({ UserService.Instance.myUserInfo.match({
some: mui => { some: mui => {
if (data.post_view.creator.id == mui.local_user_view.person.id) { if (data.post_view.creator.id == mui.local_user_view.person.id) {
this.state.loading = false;
this.props.onCreate(data.post_view); this.props.onCreate(data.post_view);
} }
}, },
@ -712,7 +756,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
UserService.Instance.myUserInfo.match({ UserService.Instance.myUserInfo.match({
some: mui => { some: mui => {
if (data.post_view.creator.id == mui.local_user_view.person.id) { if (data.post_view.creator.id == mui.local_user_view.person.id) {
this.state.loading = false; this.setState({ loading: false });
this.props.onEdit(data.post_view); this.props.onEdit(data.post_view);
} }
}, },
@ -722,11 +766,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
let data = wsJsonToRes<SearchResponse>(msg, SearchResponse); let data = wsJsonToRes<SearchResponse>(msg, SearchResponse);
if (data.type_ == SearchType[SearchType.Posts]) { if (data.type_ == SearchType[SearchType.Posts]) {
this.state.suggestedPosts = Some(data.posts); this.setState({ suggestedPosts: Some(data.posts) });
} else if (data.type_ == SearchType[SearchType.Url]) { } else if (data.type_ == SearchType[SearchType.Url]) {
this.state.crossPosts = Some(data.posts); this.setState({ crossPosts: Some(data.posts) });
} }
this.setState(this.state);
} }
} }
} }

View file

@ -12,6 +12,7 @@ import {
CreatePostLike, CreatePostLike,
CreatePostReport, CreatePostReport,
DeletePost, DeletePost,
Language,
LockPost, LockPost,
PersonViewSafe, PersonViewSafe,
PostView, PostView,
@ -88,6 +89,7 @@ interface PostListingProps {
duplicates: Option<PostView[]>; duplicates: Option<PostView[]>;
moderators: Option<CommunityModeratorView[]>; moderators: Option<CommunityModeratorView[]>;
admins: Option<PersonViewSafe[]>; admins: Option<PersonViewSafe[]>;
allLanguages: Language[];
showCommunity?: boolean; showCommunity?: boolean;
showBody?: boolean; showBody?: boolean;
enableDownvotes?: boolean; enableDownvotes?: boolean;
@ -135,20 +137,21 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
componentWillReceiveProps(nextProps: PostListingProps) { componentWillReceiveProps(nextProps: PostListingProps) {
this.state.my_vote = nextProps.post_view.my_vote; this.setState({
this.state.upvotes = nextProps.post_view.counts.upvotes; my_vote: nextProps.post_view.my_vote,
this.state.downvotes = nextProps.post_view.counts.downvotes; upvotes: nextProps.post_view.counts.upvotes,
this.state.score = nextProps.post_view.counts.score; downvotes: nextProps.post_view.counts.downvotes,
score: nextProps.post_view.counts.score,
});
if (this.props.post_view.post.id !== nextProps.post_view.post.id) { if (this.props.post_view.post.id !== nextProps.post_view.post.id) {
this.state.imageExpanded = false; this.setState({ imageExpanded: false });
} }
this.setState(this.state);
} }
render() { render() {
let post = this.props.post_view.post; let post = this.props.post_view.post;
return ( return (
<div class="post-listing"> <div className="post-listing">
{!this.state.showEdit ? ( {!this.state.showEdit ? (
<> <>
{this.listing()} {this.listing()}
@ -159,7 +162,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{this.showBody && this.body()} {this.showBody && this.body()}
</> </>
) : ( ) : (
<div class="col-12"> <div className="col-12">
<PostForm <PostForm
post_view={Some(this.props.post_view)} post_view={Some(this.props.post_view)}
communities={None} communities={None}
@ -168,6 +171,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
onCancel={this.handleEditCancel} onCancel={this.handleEditCancel}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
allLanguages={this.props.allLanguages}
/> />
</div> </div>
)} )}
@ -178,7 +182,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
body() { body() {
return this.props.post_view.post.body.match({ return this.props.post_view.post.body.match({
some: body => ( some: body => (
<div class="col-12 card my-2 p-2"> <div className="col-12 card my-2 p-2">
{this.state.viewSource ? ( {this.state.viewSource ? (
<pre>{body}</pre> <pre>{body}</pre>
) : ( ) : (
@ -194,14 +198,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
return this.imageSrc.match({ return this.imageSrc.match({
some: src => ( some: src => (
<> <>
<div class="offset-sm-3 my-2 d-none d-sm-block"> <div className="offset-sm-3 my-2 d-none d-sm-block">
<a href={src} class="d-inline-block"> <a href={src} className="d-inline-block">
<PictrsImage src={src} /> <PictrsImage src={src} />
</a> </a>
</div> </div>
<div className="my-2 d-block d-sm-none"> <div className="my-2 d-block d-sm-none">
<a <a
class="d-inline-block" className="d-inline-block"
onClick={linkEvent(this, this.handleImageExpandClick)} onClick={linkEvent(this, this.handleImageExpandClick)}
> >
<PictrsImage src={src} /> <PictrsImage src={src} />
@ -254,7 +258,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
return ( return (
<a <a
href={this.imageSrc.unwrap()} href={this.imageSrc.unwrap()}
class="text-body d-inline-block position-relative mb-2" className="text-body d-inline-block position-relative mb-2"
data-tippy-content={i18n.t("expand_here")} data-tippy-content={i18n.t("expand_here")}
onClick={linkEvent(this, this.handleImageExpandClick)} onClick={linkEvent(this, this.handleImageExpandClick)}
aria-label={i18n.t("expand_here")} aria-label={i18n.t("expand_here")}
@ -266,7 +270,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} else if (url.isSome() && thumbnail.isSome()) { } else if (url.isSome() && thumbnail.isSome()) {
return ( return (
<a <a
class="text-body d-inline-block position-relative mb-2" className="text-body d-inline-block position-relative mb-2"
href={url.unwrap()} href={url.unwrap()}
rel={relTags} rel={relTags}
title={url.unwrap()} title={url.unwrap()}
@ -278,13 +282,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} else if (url.isSome()) { } else if (url.isSome()) {
if (isVideo(url.unwrap())) { if (isVideo(url.unwrap())) {
return ( return (
<div class="embed-responsive embed-responsive-16by9"> <div className="embed-responsive embed-responsive-16by9">
<video <video
playsinline playsInline
muted muted
loop loop
controls controls
class="embed-responsive-item" className="embed-responsive-item"
> >
<source src={url.unwrap()} type="video/mp4" /> <source src={url.unwrap()} type="video/mp4" />
</video> </video>
@ -298,7 +302,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
title={url.unwrap()} title={url.unwrap()}
rel={relTags} rel={relTags}
> >
<div class="thumbnail rounded bg-light d-flex justify-content-center"> <div className="thumbnail rounded bg-light d-flex justify-content-center">
<Icon icon="external-link" classes="d-flex align-items-center" /> <Icon icon="external-link" classes="d-flex align-items-center" />
</div> </div>
</a> </a>
@ -311,7 +315,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
to={`/post/${post.id}`} to={`/post/${post.id}`}
title={i18n.t("comments")} title={i18n.t("comments")}
> >
<div class="thumbnail rounded bg-light d-flex justify-content-center"> <div className="thumbnail rounded bg-light d-flex justify-content-center">
<Icon icon="message-square" classes="d-flex align-items-center" /> <Icon icon="message-square" classes="d-flex align-items-center" />
</div> </div>
</Link> </Link>
@ -322,7 +326,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
createdLine() { createdLine() {
let post_view = this.props.post_view; let post_view = this.props.post_view;
return ( return (
<ul class="list-inline mb-1 text-muted small"> <ul className="list-inline mb-1 text-muted small">
<li className="list-inline-item"> <li className="list-inline-item">
<PersonListing person={post_view.creator} /> <PersonListing person={post_view.creator} />
@ -346,7 +350,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
)} )}
{this.props.showCommunity && ( {this.props.showCommunity && (
<span> <span>
<span class="mx-1"> {i18n.t("to")} </span> <span className="mx-1"> {i18n.t("to")} </span>
<CommunityLink community={post_view.community} /> <CommunityLink community={post_view.community} />
</span> </span>
)} )}
@ -416,13 +420,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</button> </button>
{showScores() ? ( {showScores() ? (
<div <div
class={`unselectable pointer font-weight-bold text-muted px-1`} className={`unselectable pointer font-weight-bold text-muted px-1`}
data-tippy-content={this.pointsTippy} data-tippy-content={this.pointsTippy}
> >
{numToSI(this.state.score)} {numToSI(this.state.score)}
</div> </div>
) : ( ) : (
<div class="p-1"></div> <div className="p-1"></div>
)} )}
{this.props.enableDownvotes && ( {this.props.enableDownvotes && (
<button <button
@ -470,7 +474,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
})} })}
{post.url.map(isImage).or(post.thumbnail_url).unwrapOr(false) && ( {post.url.map(isImage).or(post.thumbnail_url).unwrapOr(false) && (
<button <button
class="btn btn-link text-monospace text-muted small d-inline-block ml-2" className="btn btn-link text-monospace text-muted small d-inline-block ml-2"
data-tippy-content={i18n.t("expand_here")} data-tippy-content={i18n.t("expand_here")}
onClick={linkEvent(this, this.handleImageExpandClick)} onClick={linkEvent(this, this.handleImageExpandClick)}
> >
@ -525,13 +529,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
return this.props.duplicates.match({ return this.props.duplicates.match({
some: dupes => some: dupes =>
dupes.length > 0 && ( dupes.length > 0 && (
<ul class="list-inline mb-1 small text-muted"> <ul className="list-inline mb-1 small text-muted">
<> <>
<li className="list-inline-item mr-2"> <li className="list-inline-item mr-2">
{i18n.t("cross_posted_to")} {i18n.t("cross_posted_to")}
</li> </li>
{dupes.map(pv => ( {dupes.map(pv => (
<li className="list-inline-item mr-2"> <li key={pv.post.id} className="list-inline-item mr-2">
<Link to={`/post/${pv.post.id}`}> <Link to={`/post/${pv.post.id}`}>
{pv.community.local {pv.community.local
? pv.community.name ? pv.community.name
@ -551,7 +555,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
commentsLine(mobile = false) { commentsLine(mobile = false) {
let post = this.props.post_view.post; let post = this.props.post_view.post;
return ( return (
<div class="d-flex justify-content-start flex-wrap text-muted font-weight-bold mb-1"> <div className="d-flex justify-content-start flex-wrap text-muted font-weight-bold mb-1">
{this.commentsButton} {this.commentsButton}
{!post.local && ( {!post.local && (
<a <a
@ -617,7 +621,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
get commentsButton() { get commentsButton() {
let post_view = this.props.post_view; let post_view = this.props.post_view;
return ( return (
<button class="btn btn-link text-muted py-0 pl-0"> <button className="btn btn-link text-muted py-0 pl-0">
<Link <Link
className="text-muted" className="text-muted"
title={i18n.t("number_of_comments", { title={i18n.t("number_of_comments", {
@ -627,15 +631,34 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
to={`/post/${post_view.post.id}?scrollToComments=true`} to={`/post/${post_view.post.id}?scrollToComments=true`}
> >
<Icon icon="message-square" classes="mr-1" inline /> <Icon icon="message-square" classes="mr-1" inline />
{i18n.t("number_of_comments", { <span className="mr-2">
count: post_view.counts.comments, {i18n.t("number_of_comments", {
formattedCount: numToSI(post_view.counts.comments), count: post_view.counts.comments,
formattedCount: numToSI(post_view.counts.comments),
})}
</span>
{this.unreadCount.match({
some: unreadCount => (
<span className="small text-warning">
({unreadCount} {i18n.t("new")})
</span>
),
none: <></>,
})} })}
</Link> </Link>
</button> </button>
); );
} }
get unreadCount(): Option<number> {
let pv = this.props.post_view;
if (pv.unread_comments == pv.counts.comments || pv.unread_comments == 0) {
return None;
} else {
return Some(pv.unread_comments);
}
}
get mobileVotes() { get mobileVotes() {
// TODO: make nicer // TODO: make nicer
let tippy = showScores() ? { "data-tippy-content": this.pointsTippy } : {}; let tippy = showScores() ? { "data-tippy-content": this.pointsTippy } : {};
@ -652,7 +675,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
> >
<Icon icon="arrow-up1" classes="icon-inline small" /> <Icon icon="arrow-up1" classes="icon-inline small" />
{showScores() && ( {showScores() && (
<span class="ml-2">{numToSI(this.state.upvotes)}</span> <span className="ml-2">{numToSI(this.state.upvotes)}</span>
)} )}
</button> </button>
{this.props.enableDownvotes && ( {this.props.enableDownvotes && (
@ -669,7 +692,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<Icon icon="arrow-down1" classes="icon-inline small" /> <Icon icon="arrow-down1" classes="icon-inline small" />
{showScores() && ( {showScores() && (
<span <span
class={classNames("ml-2", { className={classNames("ml-2", {
invisible: this.state.downvotes === 0, invisible: this.state.downvotes === 0,
})} })}
> >
@ -688,7 +711,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
let label = saved ? i18n.t("unsave") : i18n.t("save"); let label = saved ? i18n.t("unsave") : i18n.t("save");
return ( return (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleSavePostClick)} onClick={linkEvent(this, this.handleSavePostClick)}
data-tippy-content={label} data-tippy-content={label}
aria-label={label} aria-label={label}
@ -717,7 +740,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
get reportButton() { get reportButton() {
return ( return (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleShowReportDialog)} onClick={linkEvent(this, this.handleShowReportDialog)}
data-tippy-content={i18n.t("show_report_dialog")} data-tippy-content={i18n.t("show_report_dialog")}
aria-label={i18n.t("show_report_dialog")} aria-label={i18n.t("show_report_dialog")}
@ -730,7 +753,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
get blockButton() { get blockButton() {
return ( return (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleBlockUserClick)} onClick={linkEvent(this, this.handleBlockUserClick)}
data-tippy-content={i18n.t("block_user")} data-tippy-content={i18n.t("block_user")}
aria-label={i18n.t("block_user")} aria-label={i18n.t("block_user")}
@ -743,7 +766,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
get editButton() { get editButton() {
return ( return (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleEditClick)} onClick={linkEvent(this, this.handleEditClick)}
data-tippy-content={i18n.t("edit")} data-tippy-content={i18n.t("edit")}
aria-label={i18n.t("edit")} aria-label={i18n.t("edit")}
@ -758,7 +781,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
let label = !deleted ? i18n.t("delete") : i18n.t("restore"); let label = !deleted ? i18n.t("delete") : i18n.t("restore");
return ( return (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleDeleteClick)} onClick={linkEvent(this, this.handleDeleteClick)}
data-tippy-content={label} data-tippy-content={label}
aria-label={label} aria-label={label}
@ -775,7 +798,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
get showMoreButton() { get showMoreButton() {
return ( return (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleShowAdvanced)} onClick={linkEvent(this, this.handleShowAdvanced)}
data-tippy-content={i18n.t("more")} data-tippy-content={i18n.t("more")}
aria-label={i18n.t("more")} aria-label={i18n.t("more")}
@ -788,7 +811,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
get viewSourceButton() { get viewSourceButton() {
return ( return (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleViewSource)} onClick={linkEvent(this, this.handleViewSource)}
data-tippy-content={i18n.t("view_source")} data-tippy-content={i18n.t("view_source")}
aria-label={i18n.t("view_source")} aria-label={i18n.t("view_source")}
@ -807,7 +830,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
let label = locked ? i18n.t("unlock") : i18n.t("lock"); let label = locked ? i18n.t("unlock") : i18n.t("lock");
return ( return (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleModLock)} onClick={linkEvent(this, this.handleModLock)}
data-tippy-content={label} data-tippy-content={label}
aria-label={label} aria-label={label}
@ -826,7 +849,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
let label = stickied ? i18n.t("unsticky") : i18n.t("sticky"); let label = stickied ? i18n.t("unsticky") : i18n.t("sticky");
return ( return (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleModSticky)} onClick={linkEvent(this, this.handleModSticky)}
data-tippy-content={label} data-tippy-content={label}
aria-label={label} aria-label={label}
@ -844,7 +867,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
let removed = this.props.post_view.post.removed; let removed = this.props.post_view.post.removed;
return ( return (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent( onClick={linkEvent(
this, this,
!removed ? this.handleModRemoveShow : this.handleModRemoveSubmit !removed ? this.handleModRemoveShow : this.handleModRemoveSubmit
@ -870,7 +893,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{!this.creatorIsMod_ && {!this.creatorIsMod_ &&
(!post_view.creator_banned_from_community ? ( (!post_view.creator_banned_from_community ? (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleModBanFromCommunityShow this.handleModBanFromCommunityShow
@ -881,7 +904,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</button> </button>
) : ( ) : (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleModBanFromCommunitySubmit this.handleModBanFromCommunitySubmit
@ -893,7 +916,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
))} ))}
{!post_view.creator_banned_from_community && ( {!post_view.creator_banned_from_community && (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleAddModToCommunity)} onClick={linkEvent(this, this.handleAddModToCommunity)}
aria-label={ aria-label={
this.creatorIsMod_ this.creatorIsMod_
@ -914,7 +937,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
this.creatorIsMod_ && this.creatorIsMod_ &&
(!this.state.showConfirmTransferCommunity ? ( (!this.state.showConfirmTransferCommunity ? (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleShowConfirmTransferCommunity this.handleShowConfirmTransferCommunity
@ -926,20 +949,20 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
) : ( ) : (
<> <>
<button <button
class="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0" className="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0"
aria-label={i18n.t("are_you_sure")} aria-label={i18n.t("are_you_sure")}
> >
{i18n.t("are_you_sure")} {i18n.t("are_you_sure")}
</button> </button>
<button <button
class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1" className="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
aria-label={i18n.t("yes")} aria-label={i18n.t("yes")}
onClick={linkEvent(this, this.handleTransferCommunity)} onClick={linkEvent(this, this.handleTransferCommunity)}
> >
{i18n.t("yes")} {i18n.t("yes")}
</button> </button>
<button <button
class="btn btn-link btn-animate text-muted py-0 d-inline-block" className="btn btn-link btn-animate text-muted py-0 d-inline-block"
onClick={linkEvent( onClick={linkEvent(
this, this,
this.handleCancelShowConfirmTransferCommunity this.handleCancelShowConfirmTransferCommunity
@ -957,7 +980,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<> <>
{!isBanned(post_view.creator) ? ( {!isBanned(post_view.creator) ? (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleModBanShow)} onClick={linkEvent(this, this.handleModBanShow)}
aria-label={i18n.t("ban_from_site")} aria-label={i18n.t("ban_from_site")}
> >
@ -965,7 +988,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</button> </button>
) : ( ) : (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleModBanSubmit)} onClick={linkEvent(this, this.handleModBanSubmit)}
aria-label={i18n.t("unban_from_site")} aria-label={i18n.t("unban_from_site")}
> >
@ -973,14 +996,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</button> </button>
)} )}
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handlePurgePersonShow)} onClick={linkEvent(this, this.handlePurgePersonShow)}
aria-label={i18n.t("purge_user")} aria-label={i18n.t("purge_user")}
> >
{i18n.t("purge_user")} {i18n.t("purge_user")}
</button> </button>
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handlePurgePostShow)} onClick={linkEvent(this, this.handlePurgePostShow)}
aria-label={i18n.t("purge_post")} aria-label={i18n.t("purge_post")}
> >
@ -990,7 +1013,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
)} )}
{!isBanned(post_view.creator) && post_view.creator.local && ( {!isBanned(post_view.creator) && post_view.creator.local && (
<button <button
class="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleAddAdmin)} onClick={linkEvent(this, this.handleAddAdmin)}
aria-label={ aria-label={
this.creatorIsAdmin_ this.creatorIsAdmin_
@ -1022,23 +1045,23 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<> <>
{this.state.showRemoveDialog && ( {this.state.showRemoveDialog && (
<form <form
class="form-inline" className="form-inline"
onSubmit={linkEvent(this, this.handleModRemoveSubmit)} onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
> >
<label class="sr-only" htmlFor="post-listing-remove-reason"> <label className="sr-only" htmlFor="post-listing-remove-reason">
{i18n.t("reason")} {i18n.t("reason")}
</label> </label>
<input <input
type="text" type="text"
id="post-listing-remove-reason" id="post-listing-remove-reason"
class="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={toUndefined(this.state.removeReason)} value={toUndefined(this.state.removeReason)}
onInput={linkEvent(this, this.handleModRemoveReasonChange)} onInput={linkEvent(this, this.handleModRemoveReasonChange)}
/> />
<button <button
type="submit" type="submit"
class="btn btn-secondary" className="btn btn-secondary"
aria-label={i18n.t("remove_post")} aria-label={i18n.t("remove_post")}
> >
{i18n.t("remove_post")} {i18n.t("remove_post")}
@ -1047,40 +1070,43 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
)} )}
{this.state.showBanDialog && ( {this.state.showBanDialog && (
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}> <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
<div class="form-group row col-12"> <div className="form-group row col-12">
<label class="col-form-label" htmlFor="post-listing-ban-reason"> <label
className="col-form-label"
htmlFor="post-listing-ban-reason"
>
{i18n.t("reason")} {i18n.t("reason")}
</label> </label>
<input <input
type="text" type="text"
id="post-listing-ban-reason" id="post-listing-ban-reason"
class="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={toUndefined(this.state.banReason)} value={toUndefined(this.state.banReason)}
onInput={linkEvent(this, this.handleModBanReasonChange)} onInput={linkEvent(this, this.handleModBanReasonChange)}
/> />
<label class="col-form-label" htmlFor={`mod-ban-expires`}> <label className="col-form-label" htmlFor={`mod-ban-expires`}>
{i18n.t("expires")} {i18n.t("expires")}
</label> </label>
<input <input
type="number" type="number"
id={`mod-ban-expires`} id={`mod-ban-expires`}
class="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("number_of_days")} placeholder={i18n.t("number_of_days")}
value={toUndefined(this.state.banExpireDays)} value={toUndefined(this.state.banExpireDays)}
onInput={linkEvent(this, this.handleModBanExpireDaysChange)} onInput={linkEvent(this, this.handleModBanExpireDaysChange)}
/> />
<div class="form-group"> <div className="form-group">
<div class="form-check"> <div className="form-check">
<input <input
class="form-check-input" className="form-check-input"
id="mod-ban-remove-data" id="mod-ban-remove-data"
type="checkbox" type="checkbox"
checked={this.state.removeData} checked={this.state.removeData}
onChange={linkEvent(this, this.handleModRemoveDataChange)} onChange={linkEvent(this, this.handleModRemoveDataChange)}
/> />
<label <label
class="form-check-label" className="form-check-label"
htmlFor="mod-ban-remove-data" htmlFor="mod-ban-remove-data"
title={i18n.t("remove_content_more")} title={i18n.t("remove_content_more")}
> >
@ -1094,10 +1120,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{/* <label class="col-form-label">Expires</label> */} {/* <label class="col-form-label">Expires</label> */}
{/* <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 className="form-group row">
<button <button
type="submit" type="submit"
class="btn btn-secondary" className="btn btn-secondary"
aria-label={i18n.t("ban")} aria-label={i18n.t("ban")}
> >
{i18n.t("ban")} {post.creator.name} {i18n.t("ban")} {post.creator.name}
@ -1107,16 +1133,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
)} )}
{this.state.showReportDialog && ( {this.state.showReportDialog && (
<form <form
class="form-inline" className="form-inline"
onSubmit={linkEvent(this, this.handleReportSubmit)} onSubmit={linkEvent(this, this.handleReportSubmit)}
> >
<label class="sr-only" htmlFor="post-report-reason"> <label className="sr-only" htmlFor="post-report-reason">
{i18n.t("reason")} {i18n.t("reason")}
</label> </label>
<input <input
type="text" type="text"
id="post-report-reason" id="post-report-reason"
class="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
required required
value={toUndefined(this.state.reportReason)} value={toUndefined(this.state.reportReason)}
@ -1124,7 +1150,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
/> />
<button <button
type="submit" type="submit"
class="btn btn-secondary" className="btn btn-secondary"
aria-label={i18n.t("create_report")} aria-label={i18n.t("create_report")}
> >
{i18n.t("create_report")} {i18n.t("create_report")}
@ -1133,17 +1159,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
)} )}
{this.state.showPurgeDialog && ( {this.state.showPurgeDialog && (
<form <form
class="form-inline" className="form-inline"
onSubmit={linkEvent(this, this.handlePurgeSubmit)} onSubmit={linkEvent(this, this.handlePurgeSubmit)}
> >
<PurgeWarning /> <PurgeWarning />
<label class="sr-only" htmlFor="purge-reason"> <label className="sr-only" htmlFor="purge-reason">
{i18n.t("reason")} {i18n.t("reason")}
</label> </label>
<input <input
type="text" type="text"
id="purge-reason" id="purge-reason"
class="form-control mr-2" className="form-control mr-2"
placeholder={i18n.t("reason")} placeholder={i18n.t("reason")}
value={toUndefined(this.state.purgeReason)} value={toUndefined(this.state.purgeReason)}
onInput={linkEvent(this, this.handlePurgeReasonChange)} onInput={linkEvent(this, this.handlePurgeReasonChange)}
@ -1153,7 +1179,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
) : ( ) : (
<button <button
type="submit" type="submit"
class="btn btn-secondary" className="btn btn-secondary"
aria-label={purgeTypeText} aria-label={purgeTypeText}
> >
{purgeTypeText} {purgeTypeText}
@ -1169,11 +1195,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
let post = this.props.post_view.post; let post = this.props.post_view.post;
return post.thumbnail_url.isSome() || return post.thumbnail_url.isSome() ||
post.url.map(isImage).unwrapOr(false) ? ( post.url.map(isImage).unwrapOr(false) ? (
<div class="row"> <div className="row">
<div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}> <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
{this.postTitleLine()} {this.postTitleLine()}
</div> </div>
<div class="col-4"> <div className="col-4">
{/* Post body prev or thumbnail */} {/* Post body prev or thumbnail */}
{!this.state.imageExpanded && this.thumbnail()} {!this.state.imageExpanded && this.thumbnail()}
</div> </div>
@ -1198,9 +1224,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
return ( return (
<> <>
{/* The mobile view*/} {/* The mobile view*/}
<div class="d-block d-sm-none"> <div className="d-block d-sm-none">
<div class="row"> <div className="row">
<div class="col-12"> <div className="col-12">
{this.createdLine()} {this.createdLine()}
{/* If it has a thumbnail, do a right aligned thumbnail */} {/* If it has a thumbnail, do a right aligned thumbnail */}
@ -1218,14 +1244,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</div> </div>
{/* The larger view*/} {/* The larger view*/}
<div class="d-none d-sm-block"> <div className="d-none d-sm-block">
<div class="row"> <div className="row">
{!this.props.viewOnly && this.voteBar()} {!this.props.viewOnly && this.voteBar()}
<div class="col-sm-2 pr-0"> <div className="col-sm-2 pr-0">
<div class="">{this.thumbnail()}</div> <div className="">{this.thumbnail()}</div>
</div> </div>
<div class="col-12 col-sm-9"> <div className="col-12 col-sm-9">
<div class="row"> <div className="row">
<div className="col-12"> <div className="col-12">
{this.postTitleLine()} {this.postTitleLine()}
{this.createdLine()} {this.createdLine()}
@ -1260,18 +1286,24 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
let newVote = myVote == 1 ? 0 : 1; let newVote = myVote == 1 ? 0 : 1;
if (myVote == 1) { if (myVote == 1) {
this.state.score--; this.setState({
this.state.upvotes--; score: this.state.score - 1,
upvotes: this.state.upvotes - 1,
});
} else if (myVote == -1) { } else if (myVote == -1) {
this.state.downvotes--; this.setState({
this.state.upvotes++; score: this.state.score + 2,
this.state.score += 2; upvotes: this.state.upvotes + 1,
downvotes: this.state.downvotes - 1,
});
} else { } else {
this.state.upvotes++; this.setState({
this.state.score++; score: this.state.score + 1,
upvotes: this.state.upvotes + 1,
});
} }
this.state.my_vote = Some(newVote); this.setState({ my_vote: Some(newVote) });
let form = new CreatePostLike({ let form = new CreatePostLike({
post_id: this.props.post_view.post.id, post_id: this.props.post_view.post.id,
@ -1294,18 +1326,24 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
let newVote = myVote == -1 ? 0 : -1; let newVote = myVote == -1 ? 0 : -1;
if (myVote == 1) { if (myVote == 1) {
this.state.score -= 2; this.setState({
this.state.upvotes--; score: this.state.score - 2,
this.state.downvotes++; upvotes: this.state.upvotes - 1,
downvotes: this.state.downvotes + 1,
});
} else if (myVote == -1) { } else if (myVote == -1) {
this.state.downvotes--; this.setState({
this.state.score++; score: this.state.score + 1,
downvotes: this.state.downvotes - 1,
});
} else { } else {
this.state.downvotes++; this.setState({
this.state.score--; score: this.state.score - 1,
downvotes: this.state.downvotes + 1,
});
} }
this.state.my_vote = Some(newVote); this.setState({ my_vote: Some(newVote) });
let form = new CreatePostLike({ let form = new CreatePostLike({
post_id: this.props.post_view.post.id, post_id: this.props.post_view.post.id,
@ -1319,29 +1357,24 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
handleEditClick(i: PostListing) { handleEditClick(i: PostListing) {
i.state.showEdit = true; i.setState({ showEdit: true });
i.setState(i.state);
} }
handleEditCancel() { handleEditCancel() {
this.state.showEdit = false; this.setState({ showEdit: false });
this.setState(this.state);
} }
// The actual editing is done in the recieve for post // The actual editing is done in the recieve for post
handleEditPost() { handleEditPost() {
this.state.showEdit = false; this.setState({ showEdit: false });
this.setState(this.state);
} }
handleShowReportDialog(i: PostListing) { handleShowReportDialog(i: PostListing) {
i.state.showReportDialog = !i.state.showReportDialog; i.setState({ showReportDialog: !i.state.showReportDialog });
i.setState(this.state);
} }
handleReportReasonChange(i: PostListing, event: any) { handleReportReasonChange(i: PostListing, event: any) {
i.state.reportReason = Some(event.target.value); i.setState({ reportReason: Some(event.target.value) });
i.setState(i.state);
} }
handleReportSubmit(i: PostListing, event: any) { handleReportSubmit(i: PostListing, event: any) {
@ -1353,8 +1386,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}); });
WebSocketService.Instance.send(wsClient.createPostReport(form)); WebSocketService.Instance.send(wsClient.createPostReport(form));
i.state.showReportDialog = false; i.setState({ showReportDialog: false });
i.setState(i.state);
} }
handleBlockUserClick(i: PostListing) { handleBlockUserClick(i: PostListing) {
@ -1413,19 +1445,18 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
handleModRemoveShow(i: PostListing) { handleModRemoveShow(i: PostListing) {
i.state.showRemoveDialog = !i.state.showRemoveDialog; i.setState({
i.state.showBanDialog = false; showRemoveDialog: !i.state.showRemoveDialog,
i.setState(i.state); showBanDialog: false,
});
} }
handleModRemoveReasonChange(i: PostListing, event: any) { handleModRemoveReasonChange(i: PostListing, event: any) {
i.state.removeReason = Some(event.target.value); i.setState({ removeReason: Some(event.target.value) });
i.setState(i.state);
} }
handleModRemoveDataChange(i: PostListing, event: any) { handleModRemoveDataChange(i: PostListing, event: any) {
i.state.removeData = event.target.checked; i.setState({ removeData: event.target.checked });
i.setState(i.state);
} }
handleModRemoveSubmit(i: PostListing, event: any) { handleModRemoveSubmit(i: PostListing, event: any) {
@ -1438,8 +1469,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}); });
WebSocketService.Instance.send(wsClient.removePost(form)); WebSocketService.Instance.send(wsClient.removePost(form));
i.state.showRemoveDialog = false; i.setState({ showRemoveDialog: false });
i.setState(i.state);
} }
handleModLock(i: PostListing) { handleModLock(i: PostListing) {
@ -1461,36 +1491,39 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
handleModBanFromCommunityShow(i: PostListing) { handleModBanFromCommunityShow(i: PostListing) {
i.state.showBanDialog = true; i.setState({
i.state.banType = BanType.Community; showBanDialog: true,
i.state.showRemoveDialog = false; banType: BanType.Community,
i.setState(i.state); showRemoveDialog: false,
});
} }
handleModBanShow(i: PostListing) { handleModBanShow(i: PostListing) {
i.state.showBanDialog = true; i.setState({
i.state.banType = BanType.Site; showBanDialog: true,
i.state.showRemoveDialog = false; banType: BanType.Site,
i.setState(i.state); showRemoveDialog: false,
});
} }
handlePurgePersonShow(i: PostListing) { handlePurgePersonShow(i: PostListing) {
i.state.showPurgeDialog = true; i.setState({
i.state.purgeType = PurgeType.Person; showPurgeDialog: true,
i.state.showRemoveDialog = false; purgeType: PurgeType.Person,
i.setState(i.state); showRemoveDialog: false,
});
} }
handlePurgePostShow(i: PostListing) { handlePurgePostShow(i: PostListing) {
i.state.showPurgeDialog = true; i.setState({
i.state.purgeType = PurgeType.Post; showPurgeDialog: true,
i.state.showRemoveDialog = false; purgeType: PurgeType.Post,
i.setState(i.state); showRemoveDialog: false,
});
} }
handlePurgeReasonChange(i: PostListing, event: any) { handlePurgeReasonChange(i: PostListing, event: any) {
i.state.purgeReason = Some(event.target.value); i.setState({ purgeReason: Some(event.target.value) });
i.setState(i.state);
} }
handlePurgeSubmit(i: PostListing, event: any) { handlePurgeSubmit(i: PostListing, event: any) {
@ -1512,29 +1545,24 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
WebSocketService.Instance.send(wsClient.purgePost(form)); WebSocketService.Instance.send(wsClient.purgePost(form));
} }
i.state.purgeLoading = true; i.setState({ purgeLoading: true });
i.setState(i.state);
} }
handleModBanReasonChange(i: PostListing, event: any) { handleModBanReasonChange(i: PostListing, event: any) {
i.state.banReason = Some(event.target.value); i.setState({ banReason: Some(event.target.value) });
i.setState(i.state);
} }
handleModBanExpireDaysChange(i: PostListing, event: any) { handleModBanExpireDaysChange(i: PostListing, event: any) {
i.state.banExpireDays = Some(event.target.value); i.setState({ banExpireDays: Some(event.target.value) });
i.setState(i.state);
} }
handleModBanFromCommunitySubmit(i: PostListing) { handleModBanFromCommunitySubmit(i: PostListing) {
i.state.banType = BanType.Community; i.setState({ banType: BanType.Community });
i.setState(i.state);
i.handleModBanBothSubmit(i); i.handleModBanBothSubmit(i);
} }
handleModBanSubmit(i: PostListing) { handleModBanSubmit(i: PostListing) {
i.state.banType = BanType.Site; i.setState({ banType: BanType.Site });
i.setState(i.state);
i.handleModBanBothSubmit(i); i.handleModBanBothSubmit(i);
} }
@ -1545,7 +1573,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
// If its an unban, restore all their data // If its an unban, restore all their data
let ban = !i.props.post_view.creator_banned_from_community; let ban = !i.props.post_view.creator_banned_from_community;
if (ban == false) { if (ban == false) {
i.state.removeData = false; i.setState({ removeData: false });
} }
let form = new BanFromCommunity({ let form = new BanFromCommunity({
person_id: i.props.post_view.creator.id, person_id: i.props.post_view.creator.id,
@ -1561,7 +1589,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
// If its an unban, restore all their data // If its an unban, restore all their data
let ban = !i.props.post_view.creator.banned; let ban = !i.props.post_view.creator.banned;
if (ban == false) { if (ban == false) {
i.state.removeData = false; i.setState({ removeData: false });
} }
let form = new BanPerson({ let form = new BanPerson({
person_id: i.props.post_view.creator.id, person_id: i.props.post_view.creator.id,
@ -1574,8 +1602,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
WebSocketService.Instance.send(wsClient.banPerson(form)); WebSocketService.Instance.send(wsClient.banPerson(form));
} }
i.state.showBanDialog = false; i.setState({ showBanDialog: false });
i.setState(i.state);
} }
handleAddModToCommunity(i: PostListing) { handleAddModToCommunity(i: PostListing) {
@ -1600,13 +1627,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
handleShowConfirmTransferCommunity(i: PostListing) { handleShowConfirmTransferCommunity(i: PostListing) {
i.state.showConfirmTransferCommunity = true; i.setState({ showConfirmTransferCommunity: true });
i.setState(i.state);
} }
handleCancelShowConfirmTransferCommunity(i: PostListing) { handleCancelShowConfirmTransferCommunity(i: PostListing) {
i.state.showConfirmTransferCommunity = false; i.setState({ showConfirmTransferCommunity: false });
i.setState(i.state);
} }
handleTransferCommunity(i: PostListing) { handleTransferCommunity(i: PostListing) {
@ -1616,48 +1641,42 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
auth: auth().unwrap(), auth: auth().unwrap(),
}); });
WebSocketService.Instance.send(wsClient.transferCommunity(form)); WebSocketService.Instance.send(wsClient.transferCommunity(form));
i.state.showConfirmTransferCommunity = false; i.setState({ showConfirmTransferCommunity: false });
i.setState(i.state);
} }
handleShowConfirmTransferSite(i: PostListing) { handleShowConfirmTransferSite(i: PostListing) {
i.state.showConfirmTransferSite = true; i.setState({ showConfirmTransferSite: true });
i.setState(i.state);
} }
handleCancelShowConfirmTransferSite(i: PostListing) { handleCancelShowConfirmTransferSite(i: PostListing) {
i.state.showConfirmTransferSite = false; i.setState({ showConfirmTransferSite: false });
i.setState(i.state);
} }
handleImageExpandClick(i: PostListing, event: any) { handleImageExpandClick(i: PostListing, event: any) {
event.preventDefault(); event.preventDefault();
i.state.imageExpanded = !i.state.imageExpanded; i.setState({ imageExpanded: !i.state.imageExpanded });
i.setState(i.state);
setupTippy(); setupTippy();
} }
handleViewSource(i: PostListing) { handleViewSource(i: PostListing) {
i.state.viewSource = !i.state.viewSource; i.setState({ viewSource: !i.state.viewSource });
i.setState(i.state);
} }
handleShowAdvanced(i: PostListing) { handleShowAdvanced(i: PostListing) {
i.state.showAdvanced = !i.state.showAdvanced; i.setState({ showAdvanced: !i.state.showAdvanced });
i.setState(i.state);
setupTippy(); setupTippy();
} }
handleShowMoreMobile(i: PostListing) { handleShowMoreMobile(i: PostListing) {
i.state.showMoreMobile = !i.state.showMoreMobile; i.setState({
i.state.showAdvanced = !i.state.showAdvanced; showMoreMobile: !i.state.showMoreMobile,
i.setState(i.state); showAdvanced: !i.state.showAdvanced,
});
setupTippy(); setupTippy();
} }
handleShowBody(i: PostListing) { handleShowBody(i: PostListing) {
i.state.showBody = !i.state.showBody; i.setState({ showBody: !i.state.showBody });
i.setState(i.state);
setupTippy(); setupTippy();
} }

View file

@ -2,12 +2,13 @@ import { None, Some } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { PostView } from "lemmy-js-client"; import { Language, PostView } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { PostListing } from "./post-listing"; import { PostListing } from "./post-listing";
interface PostListingsProps { interface PostListingsProps {
posts: PostView[]; posts: PostView[];
allLanguages: Language[];
showCommunity?: boolean; showCommunity?: boolean;
removeDuplicates?: boolean; removeDuplicates?: boolean;
enableDownvotes: boolean; enableDownvotes: boolean;
@ -41,8 +42,9 @@ export class PostListings extends Component<PostListingsProps, any> {
showCommunity={this.props.showCommunity} showCommunity={this.props.showCommunity}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
allLanguages={this.props.allLanguages}
/> />
<hr class="my-3" /> <hr className="my-3" />
</> </>
)) ))
) : ( ) : (

View file

@ -45,6 +45,7 @@ export class PostReport extends Component<PostReportProps, any> {
read: false, read: false,
creator_blocked: false, creator_blocked: false,
my_vote: r.my_vote, my_vote: r.my_vote,
unread_comments: 0,
}; };
return ( return (
@ -58,6 +59,7 @@ export class PostReport extends Component<PostReportProps, any> {
enableDownvotes={true} enableDownvotes={true}
enableNsfw={true} enableNsfw={true}
viewOnly={true} viewOnly={true}
allLanguages={[]}
/> />
<div> <div>
{i18n.t("reporter")}: <PersonListing person={r.creator} /> {i18n.t("reporter")}: <PersonListing person={r.creator} />

View file

@ -120,28 +120,31 @@ export class Post extends Component<any, PostState> {
super(props, context); super(props, context);
this.state = this.emptyState; this.state = this.emptyState;
this.state.commentSectionRef = createRef();
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
this.state = { ...this.state, commentSectionRef: createRef() };
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state.postRes = Some(this.isoData.routeData[0] as GetPostResponse); this.state = {
this.state.commentsRes = Some( ...this.state,
this.isoData.routeData[1] as GetCommentsResponse postRes: Some(this.isoData.routeData[0] as GetPostResponse),
); commentsRes: Some(this.isoData.routeData[1] as GetCommentsResponse),
};
this.state.commentsRes.match({ if (this.state.commentsRes.isSome()) {
some: res => { this.state = {
this.state.commentTree = buildCommentsTree( ...this.state,
res.comments, commentTree: buildCommentsTree(
this.state.commentsRes.unwrap().comments,
this.state.commentId.isSome() this.state.commentId.isSome()
); ),
}, };
none: void 0, }
});
this.state.loading = false; this.state = { ...this.state, loading: false };
if (isBrowser()) { if (isBrowser()) {
WebSocketService.Instance.send( WebSocketService.Instance.send(
@ -305,8 +308,9 @@ export class Post extends Component<any, PostState> {
trackCommentsBoxScrolling = () => { trackCommentsBoxScrolling = () => {
const wrappedElement = document.getElementsByClassName("comments")[0]; const wrappedElement = document.getElementsByClassName("comments")[0];
if (wrappedElement && this.isBottom(wrappedElement)) { if (wrappedElement && this.isBottom(wrappedElement)) {
this.state.maxCommentsShown += commentsShownInterval; this.setState({
this.setState(this.state); maxCommentsShown: this.state.maxCommentsShown + commentsShownInterval,
});
} }
}; };
@ -341,7 +345,7 @@ export class Post extends Component<any, PostState> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
<Spinner large /> <Spinner large />
@ -349,8 +353,8 @@ export class Post extends Component<any, PostState> {
) : ( ) : (
this.state.postRes.match({ this.state.postRes.match({
some: res => ( some: res => (
<div class="row"> <div className="row">
<div class="col-12 col-md-8 mb-3"> <div className="col-12 col-md-8 mb-3">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
@ -366,15 +370,17 @@ export class Post extends Component<any, PostState> {
admins={Some(this.state.siteRes.admins)} admins={Some(this.state.siteRes.admins)}
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)} enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
/> />
<div ref={this.state.commentSectionRef} className="mb-2" /> <div ref={this.state.commentSectionRef} className="mb-2" />
<CommentForm <CommentForm
node={Right(res.post_view.post.id)} node={Right(res.post_view.post.id)}
disabled={res.post_view.post.locked} disabled={res.post_view.post.locked}
allLanguages={this.state.siteRes.all_languages}
/> />
<div class="d-block d-md-none"> <div className="d-block d-md-none">
<button <button
class="btn btn-secondary d-inline-block mb-2 mr-3" className="btn btn-secondary d-inline-block mb-2 mr-3"
onClick={linkEvent(this, this.handleShowSidebarMobile)} onClick={linkEvent(this, this.handleShowSidebarMobile)}
> >
{i18n.t("sidebar")}{" "} {i18n.t("sidebar")}{" "}
@ -395,7 +401,9 @@ export class Post extends Component<any, PostState> {
{this.state.commentViewType == CommentViewType.Flat && {this.state.commentViewType == CommentViewType.Flat &&
this.commentsFlat()} this.commentsFlat()}
</div> </div>
<div class="d-none d-md-block col-md-4">{this.sidebar()}</div> <div className="d-none d-md-block col-md-4">
{this.sidebar()}
</div>
</div> </div>
), ),
none: <></>, none: <></>,
@ -408,7 +416,7 @@ export class Post extends Component<any, PostState> {
sortRadios() { sortRadios() {
return ( return (
<> <>
<div class="btn-group btn-group-toggle flex-wrap mr-3 mb-2"> <div className="btn-group btn-group-toggle flex-wrap mr-3 mb-2">
<label <label
className={`btn btn-outline-secondary pointer ${ className={`btn btn-outline-secondary pointer ${
CommentSortType[this.state.commentSort] === CommentSortType.Hot && CommentSortType[this.state.commentSort] === CommentSortType.Hot &&
@ -466,7 +474,7 @@ export class Post extends Component<any, PostState> {
/> />
</label> </label>
</div> </div>
<div class="btn-group btn-group-toggle flex-wrap mb-2"> <div className="btn-group btn-group-toggle flex-wrap mb-2">
<label <label
className={`btn btn-outline-secondary pointer ${ className={`btn btn-outline-secondary pointer ${
this.state.commentViewType === CommentViewType.Flat && "active" this.state.commentViewType === CommentViewType.Flat && "active"
@ -502,6 +510,7 @@ export class Post extends Component<any, PostState> {
admins={Some(this.state.siteRes.admins)} admins={Some(this.state.siteRes.admins)}
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
showContext showContext
allLanguages={this.state.siteRes.all_languages}
/> />
</div> </div>
), ),
@ -514,7 +523,7 @@ export class Post extends Component<any, PostState> {
sidebar() { sidebar() {
return this.state.postRes.match({ return this.state.postRes.match({
some: res => ( some: res => (
<div class="mb-3"> <div className="mb-3">
<Sidebar <Sidebar
community_view={res.community_view} community_view={res.community_view}
moderators={res.moderators} moderators={res.moderators}
@ -530,25 +539,26 @@ export class Post extends Component<any, PostState> {
} }
handleCommentSortChange(i: Post, event: any) { handleCommentSortChange(i: Post, event: any) {
i.state.commentSort = CommentSortType[event.target.value]; i.setState({
i.state.commentViewType = CommentViewType.Tree; commentSort: CommentSortType[event.target.value],
i.setState(i.state); commentViewType: CommentViewType.Tree,
});
i.fetchPost(); i.fetchPost();
} }
handleCommentViewTypeChange(i: Post, event: any) { handleCommentViewTypeChange(i: Post, event: any) {
i.state.commentViewType = Number(event.target.value); i.setState({
i.state.commentSort = CommentSortType.New; commentViewType: Number(event.target.value),
i.state.commentTree = buildCommentsTree( commentSort: CommentSortType.New,
i.state.commentsRes.map(r => r.comments).unwrapOr([]), commentTree: buildCommentsTree(
i.state.commentId.isSome() i.state.commentsRes.map(r => r.comments).unwrapOr([]),
); i.state.commentId.isSome()
i.setState(i.state); ),
});
} }
handleShowSidebarMobile(i: Post) { handleShowSidebarMobile(i: Post) {
i.state.showSidebarMobile = !i.state.showSidebarMobile; i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
i.setState(i.state);
} }
handleViewPost(i: Post) { handleViewPost(i: Post) {
@ -581,14 +591,14 @@ export class Post extends Component<any, PostState> {
{this.state.commentId.isSome() && ( {this.state.commentId.isSome() && (
<> <>
<button <button
class="pl-0 d-block btn btn-link text-muted" className="pl-0 d-block btn btn-link text-muted"
onClick={linkEvent(this, this.handleViewPost)} onClick={linkEvent(this, this.handleViewPost)}
> >
{i18n.t("view_all_comments")} {i18n.t("view_all_comments")}
</button> </button>
{showContextButton && ( {showContextButton && (
<button <button
class="pl-0 d-block btn btn-link text-muted" className="pl-0 d-block btn btn-link text-muted"
onClick={linkEvent(this, this.handleViewContext)} onClick={linkEvent(this, this.handleViewContext)}
> >
{i18n.t("show_context")} {i18n.t("show_context")}
@ -604,6 +614,7 @@ export class Post extends Component<any, PostState> {
moderators={Some(res.moderators)} moderators={Some(res.moderators)}
admins={Some(this.state.siteRes.admins)} admins={Some(this.state.siteRes.admins)}
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
/> />
</div> </div>
), ),
@ -636,7 +647,7 @@ export class Post extends Component<any, PostState> {
}); });
} else if (op == UserOperation.GetPost) { } else if (op == UserOperation.GetPost) {
let data = wsJsonToRes<GetPostResponse>(msg, GetPostResponse); let data = wsJsonToRes<GetPostResponse>(msg, GetPostResponse);
this.state.postRes = Some(data); this.setState({ postRes: Some(data) });
// join the rooms // join the rooms
WebSocketService.Instance.send( WebSocketService.Instance.send(
@ -651,7 +662,6 @@ export class Post extends Component<any, PostState> {
// Get cross-posts // Get cross-posts
// TODO move this into initial fetch and refetch // TODO move this into initial fetch and refetch
this.fetchCrossPosts(); this.fetchCrossPosts();
this.setState(this.state);
setupTippy(); setupTippy();
if (this.state.commentId.isNone()) restoreScrollPosition(this.context); if (this.state.commentId.isNone()) restoreScrollPosition(this.context);
@ -668,16 +678,16 @@ export class Post extends Component<any, PostState> {
newComments.shift(); newComments.shift();
res.comments.push(...newComments); res.comments.push(...newComments);
}, },
none: () => { none: () => this.setState({ commentsRes: Some(data) }),
this.state.commentsRes = Some(data);
},
}); });
// this.state.commentsRes = Some(data); // this.state.commentsRes = Some(data);
this.state.commentTree = buildCommentsTree( this.setState({
this.state.commentsRes.map(r => r.comments).unwrapOr([]), commentTree: buildCommentsTree(
this.state.commentId.isSome() this.state.commentsRes.map(r => r.comments).unwrapOr([]),
); this.state.commentId.isSome()
this.state.loading = false; ),
loading: false,
});
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg, CommentResponse); let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
@ -827,19 +837,16 @@ export class Post extends Component<any, PostState> {
}); });
} else if (op == UserOperation.AddAdmin) { } else if (op == UserOperation.AddAdmin) {
let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse); let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
this.state.siteRes.admins = data.admins; this.setState(s => ((s.siteRes.admins = data.admins), s));
this.setState(this.state);
} else if (op == UserOperation.Search) { } else if (op == UserOperation.Search) {
let data = wsJsonToRes<SearchResponse>(msg, SearchResponse); let data = wsJsonToRes<SearchResponse>(msg, SearchResponse);
let xPosts = data.posts.filter( let xPosts = data.posts.filter(
p => p.post.id != Number(this.props.match.params.id) p => p.post.id != Number(this.props.match.params.id)
); );
this.state.crossPosts = xPosts.length > 0 ? Some(xPosts) : None; this.setState({ crossPosts: xPosts.length > 0 ? Some(xPosts) : None });
this.setState(this.state);
} else if (op == UserOperation.LeaveAdmin) { } else if (op == UserOperation.LeaveAdmin) {
let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse); let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
this.state.siteRes = data; this.setState({ siteRes: data });
this.setState(this.state);
} else if (op == UserOperation.TransferCommunity) { } else if (op == UserOperation.TransferCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse); let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
this.state.postRes.match({ this.state.postRes.match({

View file

@ -45,6 +45,7 @@ export class CreatePrivateMessage extends Component<
recipient_id: getRecipientIdFromProps(this.props), recipient_id: getRecipientIdFromProps(this.props),
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;
@ -61,10 +62,13 @@ export class CreatePrivateMessage extends Component<
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (this.isoData.path == this.context.router.route.match.url) { if (this.isoData.path == this.context.router.route.match.url) {
this.state.recipientDetailsRes = Some( this.state = {
this.isoData.routeData[0] as GetPersonDetailsResponse ...this.state,
); recipientDetailsRes: Some(
this.state.loading = false; this.isoData.routeData[0] as GetPersonDetailsResponse
),
loading: false,
};
} else { } else {
this.fetchPersonDetails(); this.fetchPersonDetails();
} }
@ -115,7 +119,7 @@ export class CreatePrivateMessage extends Component<
render() { render() {
return ( return (
<div class="container"> <div className="container">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
@ -129,8 +133,8 @@ export class CreatePrivateMessage extends Component<
) : ( ) : (
this.state.recipientDetailsRes.match({ this.state.recipientDetailsRes.match({
some: res => ( some: res => (
<div class="row"> <div className="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div className="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t("create_private_message")}</h5> <h5>{i18n.t("create_private_message")}</h5>
<PrivateMessageForm <PrivateMessageForm
privateMessageView={None} privateMessageView={None}
@ -151,7 +155,7 @@ export class CreatePrivateMessage extends Component<
toast(i18n.t("message_sent")); toast(i18n.t("message_sent"));
// Navigate to the front // Navigate to the front
this.context.router.history.push(`/`); this.context.router.history.push("/");
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -159,17 +163,14 @@ export class CreatePrivateMessage extends Component<
console.log(msg); console.log(msg);
if (msg.error) { if (msg.error) {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
this.state.loading = false; this.setState({ loading: false });
this.setState(this.state);
return; return;
} else if (op == UserOperation.GetPersonDetails) { } else if (op == UserOperation.GetPersonDetails) {
let data = wsJsonToRes<GetPersonDetailsResponse>( let data = wsJsonToRes<GetPersonDetailsResponse>(
msg, msg,
GetPersonDetailsResponse GetPersonDetailsResponse
); );
this.state.recipientDetailsRes = Some(data); this.setState({ recipientDetailsRes: Some(data), loading: false });
this.state.loading = false;
this.setState(this.state);
} }
} }
} }

View file

@ -71,11 +71,10 @@ export class PrivateMessageForm extends Component<
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
// Its an edit // Its an edit
this.props.privateMessageView.match({ if (this.props.privateMessageView.isSome()) {
some: pm => this.state.privateMessageForm.content =
(this.state.privateMessageForm.content = pm.private_message.content), this.props.privateMessageView.unwrap().private_message.content;
none: void 0, }
});
} }
componentDidMount() { componentDidMount() {
@ -106,21 +105,21 @@ export class PrivateMessageForm extends Component<
/> />
<form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}> <form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
{this.props.privateMessageView.isNone() && ( {this.props.privateMessageView.isNone() && (
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label"> <label className="col-sm-2 col-form-label">
{capitalizeFirstLetter(i18n.t("to"))} {capitalizeFirstLetter(i18n.t("to"))}
</label> </label>
<div class="col-sm-10 form-control-plaintext"> <div className="col-sm-10 form-control-plaintext">
<PersonListing person={this.props.recipient} /> <PersonListing person={this.props.recipient} />
</div> </div>
</div> </div>
)} )}
<div class="form-group row"> <div className="form-group row">
<label class="col-sm-2 col-form-label"> <label className="col-sm-2 col-form-label">
{i18n.t("message")} {i18n.t("message")}
<button <button
class="btn btn-link text-warning d-inline-block" className="btn btn-link text-warning d-inline-block"
onClick={linkEvent(this, this.handleShowDisclaimer)} onClick={linkEvent(this, this.handleShowDisclaimer)}
data-tippy-content={i18n.t("private_message_disclaimer")} data-tippy-content={i18n.t("private_message_disclaimer")}
aria-label={i18n.t("private_message_disclaimer")} aria-label={i18n.t("private_message_disclaimer")}
@ -128,25 +127,27 @@ export class PrivateMessageForm extends Component<
<Icon icon="alert-triangle" classes="icon-inline" /> <Icon icon="alert-triangle" classes="icon-inline" />
</button> </button>
</label> </label>
<div class="col-sm-10"> <div className="col-sm-10">
<MarkdownTextArea <MarkdownTextArea
initialContent={Some(this.state.privateMessageForm.content)} initialContent={Some(this.state.privateMessageForm.content)}
initialLanguageId={None}
placeholder={None} placeholder={None}
buttonTitle={None} buttonTitle={None}
maxLength={None} maxLength={None}
onContentChange={this.handleContentChange} onContentChange={this.handleContentChange}
allLanguages={[]}
/> />
</div> </div>
</div> </div>
{this.state.showDisclaimer && ( {this.state.showDisclaimer && (
<div class="form-group row"> <div className="form-group row">
<div class="offset-sm-2 col-sm-10"> <div className="offset-sm-2 col-sm-10">
<div class="alert alert-danger" role="alert"> <div className="alert alert-danger" role="alert">
<T i18nKey="private_message_disclaimer"> <T i18nKey="private_message_disclaimer">
# #
<a <a
class="alert-link" className="alert-link"
rel={relTags} rel={relTags}
href="https://element.io/get-started" href="https://element.io/get-started"
> >
@ -157,11 +158,11 @@ export class PrivateMessageForm extends Component<
</div> </div>
</div> </div>
)} )}
<div class="form-group row"> <div className="form-group row">
<div class="offset-sm-2 col-sm-10"> <div className="offset-sm-2 col-sm-10">
<button <button
type="submit" type="submit"
class="btn btn-secondary mr-2" className="btn btn-secondary mr-2"
disabled={this.state.loading} disabled={this.state.loading}
> >
{this.state.loading ? ( {this.state.loading ? (
@ -175,14 +176,14 @@ export class PrivateMessageForm extends Component<
{this.props.privateMessageView.isSome() && ( {this.props.privateMessageView.isSome() && (
<button <button
type="button" type="button"
class="btn btn-secondary" className="btn btn-secondary"
onClick={linkEvent(this, this.handleCancel)} onClick={linkEvent(this, this.handleCancel)}
> >
{i18n.t("cancel")} {i18n.t("cancel")}
</button> </button>
)} )}
<ul class="d-inline-block float-right list-inline mb-1 text-muted font-weight-bold"> <ul className="d-inline-block float-right list-inline mb-1 text-muted font-weight-bold">
<li class="list-inline-item"></li> <li className="list-inline-item"></li>
</ul> </ul>
</div> </div>
</div> </div>
@ -206,13 +207,11 @@ export class PrivateMessageForm extends Component<
wsClient.createPrivateMessage(i.state.privateMessageForm) wsClient.createPrivateMessage(i.state.privateMessageForm)
), ),
}); });
i.state.loading = true; i.setState({ loading: true });
i.setState(i.state);
} }
handleContentChange(val: string) { handleContentChange(val: string) {
this.state.privateMessageForm.content = val; this.setState(s => ((s.privateMessageForm.content = val), s));
this.setState(this.state);
} }
handleCancel(i: PrivateMessageForm) { handleCancel(i: PrivateMessageForm) {
@ -221,13 +220,11 @@ export class PrivateMessageForm extends Component<
handlePreviewToggle(i: PrivateMessageForm, event: any) { handlePreviewToggle(i: PrivateMessageForm, event: any) {
event.preventDefault(); event.preventDefault();
i.state.previewMode = !i.state.previewMode; i.setState({ previewMode: !i.state.previewMode });
i.setState(i.state);
} }
handleShowDisclaimer(i: PrivateMessageForm) { handleShowDisclaimer(i: PrivateMessageForm) {
i.state.showDisclaimer = !i.state.showDisclaimer; i.setState({ showDisclaimer: !i.state.showDisclaimer });
i.setState(i.state);
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -235,8 +232,7 @@ export class PrivateMessageForm extends Component<
console.log(msg); console.log(msg);
if (msg.error) { if (msg.error) {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
this.state.loading = false; this.setState({ loading: false });
this.setState(this.state);
return; return;
} else if ( } else if (
op == UserOperation.EditPrivateMessage || op == UserOperation.EditPrivateMessage ||
@ -247,16 +243,14 @@ export class PrivateMessageForm extends Component<
msg, msg,
PrivateMessageResponse PrivateMessageResponse
); );
this.state.loading = false; this.setState({ loading: false });
this.props.onEdit(data.private_message_view); this.props.onEdit(data.private_message_view);
} else if (op == UserOperation.CreatePrivateMessage) { } else if (op == UserOperation.CreatePrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>( let data = wsJsonToRes<PrivateMessageResponse>(
msg, msg,
PrivateMessageResponse PrivateMessageResponse
); );
this.state.loading = false;
this.props.onCreate(data.private_message_view); this.props.onCreate(data.private_message_view);
this.setState(this.state);
} }
} }
} }

View file

@ -0,0 +1,92 @@
import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess";
import {
PrivateMessageReportView,
ResolvePrivateMessageReport,
} from "lemmy-js-client";
import { i18n } from "../../i18next";
import { WebSocketService } from "../../services";
import { auth, mdToHtml, wsClient } from "../../utils";
import { Icon } from "../common/icon";
import { PersonListing } from "../person/person-listing";
interface Props {
report: PrivateMessageReportView;
}
export class PrivateMessageReport extends Component<Props, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
let r = this.props.report;
let pmr = r.private_message_report;
let tippyContent = i18n.t(
r.private_message_report.resolved ? "unresolve_report" : "resolve_report"
);
return (
<div>
<div>
{i18n.t("creator")}:{" "}
<PersonListing person={r.private_message_creator} />
</div>
<div>
{i18n.t("message")}:
<div
className="md-div"
dangerouslySetInnerHTML={mdToHtml(pmr.original_pm_text)}
/>
</div>
<div>
{i18n.t("reporter")}: <PersonListing person={r.creator} />
</div>
<div>
{i18n.t("reason")}: {pmr.reason}
</div>
{r.resolver.match({
some: resolver => (
<div>
{pmr.resolved ? (
<T i18nKey="resolved_by">
#
<PersonListing person={resolver} />
</T>
) : (
<T i18nKey="unresolved_by">
#
<PersonListing person={resolver} />
</T>
)}
</div>
),
none: <></>,
})}
<button
className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleResolveReport)}
data-tippy-content={tippyContent}
aria-label={tippyContent}
>
<Icon
icon="check"
classes={`icon-inline ${
pmr.resolved ? "text-success" : "text-danger"
}`}
/>
</button>
</div>
);
}
handleResolveReport(i: PrivateMessageReport) {
let pmr = i.props.report.private_message_report;
let form = new ResolvePrivateMessageReport({
report_id: pmr.id,
resolved: !pmr.resolved,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.resolvePrivateMessageReport(form));
}
}

View file

@ -1,10 +1,12 @@
import { None, Some } from "@sniptt/monads/build"; import { None, Option, Some } from "@sniptt/monads/build";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
CreatePrivateMessageReport,
DeletePrivateMessage, DeletePrivateMessage,
MarkPrivateMessageAsRead, MarkPrivateMessageAsRead,
PersonSafe, PersonSafe,
PrivateMessageView, PrivateMessageView,
toUndefined,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
@ -19,6 +21,8 @@ interface PrivateMessageState {
showEdit: boolean; showEdit: boolean;
collapsed: boolean; collapsed: boolean;
viewSource: boolean; viewSource: boolean;
showReportDialog: boolean;
reportReason: Option<string>;
} }
interface PrivateMessageProps { interface PrivateMessageProps {
@ -34,6 +38,8 @@ export class PrivateMessage extends Component<
showEdit: false, showEdit: false,
collapsed: false, collapsed: false,
viewSource: false, viewSource: false,
showReportDialog: false,
reportReason: None,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -63,9 +69,9 @@ export class PrivateMessage extends Component<
: message_view.creator; : message_view.creator;
return ( return (
<div class="border-top border-light"> <div className="border-top border-light">
<div> <div>
<ul class="list-inline mb-0 text-muted small"> <ul className="list-inline mb-0 text-muted small">
{/* TODO refactor this */} {/* TODO refactor this */}
<li className="list-inline-item"> <li className="list-inline-item">
{this.mine ? i18n.t("to") : i18n.t("from")} {this.mine ? i18n.t("to") : i18n.t("from")}
@ -114,12 +120,12 @@ export class PrivateMessage extends Component<
dangerouslySetInnerHTML={mdToHtml(this.messageUnlessRemoved)} dangerouslySetInnerHTML={mdToHtml(this.messageUnlessRemoved)}
/> />
)} )}
<ul class="list-inline mb-0 text-muted font-weight-bold"> <ul className="list-inline mb-0 text-muted font-weight-bold">
{!this.mine && ( {!this.mine && (
<> <>
<li className="list-inline-item"> <li className="list-inline-item">
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleMarkRead)} onClick={linkEvent(this, this.handleMarkRead)}
data-tippy-content={ data-tippy-content={
message_view.private_message.read message_view.private_message.read
@ -140,9 +146,10 @@ export class PrivateMessage extends Component<
/> />
</button> </button>
</li> </li>
<li className="list-inline-item">{this.reportButton}</li>
<li className="list-inline-item"> <li className="list-inline-item">
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleReplyClick)} onClick={linkEvent(this, this.handleReplyClick)}
data-tippy-content={i18n.t("reply")} data-tippy-content={i18n.t("reply")}
aria-label={i18n.t("reply")} aria-label={i18n.t("reply")}
@ -156,7 +163,7 @@ export class PrivateMessage extends Component<
<> <>
<li className="list-inline-item"> <li className="list-inline-item">
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleEditClick)} onClick={linkEvent(this, this.handleEditClick)}
data-tippy-content={i18n.t("edit")} data-tippy-content={i18n.t("edit")}
aria-label={i18n.t("edit")} aria-label={i18n.t("edit")}
@ -166,7 +173,7 @@ export class PrivateMessage extends Component<
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleDeleteClick)} onClick={linkEvent(this, this.handleDeleteClick)}
data-tippy-content={ data-tippy-content={
!message_view.private_message.deleted !message_view.private_message.deleted
@ -192,7 +199,7 @@ export class PrivateMessage extends Component<
)} )}
<li className="list-inline-item"> <li className="list-inline-item">
<button <button
class="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleViewSource)} onClick={linkEvent(this, this.handleViewSource)}
data-tippy-content={i18n.t("view_source")} data-tippy-content={i18n.t("view_source")}
aria-label={i18n.t("view_source")} aria-label={i18n.t("view_source")}
@ -209,6 +216,32 @@ export class PrivateMessage extends Component<
</div> </div>
)} )}
</div> </div>
{this.state.showReportDialog && (
<form
className="form-inline"
onSubmit={linkEvent(this, this.handleReportSubmit)}
>
<label className="sr-only" htmlFor="pm-report-reason">
{i18n.t("reason")}
</label>
<input
type="text"
id="pm-report-reason"
className="form-control mr-2"
placeholder={i18n.t("reason")}
required
value={toUndefined(this.state.reportReason)}
onInput={linkEvent(this, this.handleReportReasonChange)}
/>
<button
type="submit"
className="btn btn-secondary"
aria-label={i18n.t("create_report")}
>
{i18n.t("create_report")}
</button>
</form>
)}
{this.state.showReply && ( {this.state.showReply && (
<PrivateMessageForm <PrivateMessageForm
recipient={otherPerson} recipient={otherPerson}
@ -217,23 +250,35 @@ export class PrivateMessage extends Component<
/> />
)} )}
{/* A collapsed clearfix */} {/* A collapsed clearfix */}
{this.state.collapsed && <div class="row col-12"></div>} {this.state.collapsed && <div className="row col-12"></div>}
</div> </div>
); );
} }
get reportButton() {
return (
<button
className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleShowReportDialog)}
data-tippy-content={i18n.t("show_report_dialog")}
aria-label={i18n.t("show_report_dialog")}
>
<Icon icon="flag" inline />
</button>
);
}
get messageUnlessRemoved(): string { get messageUnlessRemoved(): string {
let message = this.props.private_message_view.private_message; let message = this.props.private_message_view.private_message;
return message.deleted ? `*${i18n.t("deleted")}*` : message.content; return message.deleted ? `*${i18n.t("deleted")}*` : message.content;
} }
handleReplyClick(i: PrivateMessage) { handleReplyClick(i: PrivateMessage) {
i.state.showReply = true; i.setState({ showReply: true });
i.setState(i.state);
} }
handleEditClick(i: PrivateMessage) { handleEditClick(i: PrivateMessage) {
i.state.showEdit = true; i.setState({ showEdit: true });
i.setState(i.state); i.setState(i.state);
} }
@ -247,9 +292,7 @@ export class PrivateMessage extends Component<
} }
handleReplyCancel() { handleReplyCancel() {
this.state.showReply = false; this.setState({ showReply: false, showEdit: false });
this.state.showEdit = false;
this.setState(this.state);
} }
handleMarkRead(i: PrivateMessage) { handleMarkRead(i: PrivateMessage) {
@ -262,26 +305,42 @@ export class PrivateMessage extends Component<
} }
handleMessageCollapse(i: PrivateMessage) { handleMessageCollapse(i: PrivateMessage) {
i.state.collapsed = !i.state.collapsed; i.setState({ collapsed: !i.state.collapsed });
i.setState(i.state);
} }
handleViewSource(i: PrivateMessage) { handleViewSource(i: PrivateMessage) {
i.state.viewSource = !i.state.viewSource; i.setState({ viewSource: !i.state.viewSource });
i.setState(i.state); }
handleShowReportDialog(i: PrivateMessage) {
i.setState({ showReportDialog: !i.state.showReportDialog });
}
handleReportReasonChange(i: PrivateMessage, event: any) {
i.setState({ reportReason: Some(event.target.value) });
}
handleReportSubmit(i: PrivateMessage, event: any) {
event.preventDefault();
let form = new CreatePrivateMessageReport({
private_message_id: i.props.private_message_view.private_message.id,
reason: toUndefined(i.state.reportReason),
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.createPrivateMessageReport(form));
i.setState({ showReportDialog: false });
} }
handlePrivateMessageEdit() { handlePrivateMessageEdit() {
this.state.showEdit = false; this.setState({ showEdit: false });
this.setState(this.state);
} }
handlePrivateMessageCreate(message: PrivateMessageView) { handlePrivateMessageCreate(message: PrivateMessageView) {
UserService.Instance.myUserInfo.match({ UserService.Instance.myUserInfo.match({
some: mui => { some: mui => {
if (message.creator.id == mui.local_user_view.person.id) { if (message.creator.id == mui.local_user_view.person.id) {
this.state.showReply = false; this.setState({ showReply: false });
this.setState(this.state);
toast(i18n.t("message_sent")); toast(i18n.t("message_sent"));
} }
}, },

View file

@ -200,28 +200,36 @@ export class Search extends Component<any, SearchState> {
); );
// This can be single or multiple communities given // This can be single or multiple communities given
communitiesRes.match({ if (communitiesRes.isSome()) {
some: res => (this.state.communities = res.communities), this.state = {
none: void 0, ...this.state,
}); communities: communitiesRes.unwrap().communities,
};
}
communityRes.match({ if (communityRes.isSome()) {
some: res => (this.state.communities = [res.community_view]), this.state = {
none: void 0, ...this.state,
}); communities: [communityRes.unwrap().community_view],
};
}
this.state.creatorDetails = Some( this.state = {
this.isoData.routeData[2] as GetPersonDetailsResponse ...this.state,
); creatorDetails: Some(
this.isoData.routeData[2] as GetPersonDetailsResponse
),
};
if (this.state.q != "") { if (this.state.q != "") {
this.state.searchResponse = Some( this.state = {
this.isoData.routeData[3] as SearchResponse ...this.state,
); searchResponse: Some(this.isoData.routeData[3] as SearchResponse),
this.state.resolveObjectResponse = Some( resolveObjectResponse: Some(
this.isoData.routeData[4] as ResolveObjectResponse this.isoData.routeData[4] as ResolveObjectResponse
); ),
this.state.loading = false; loading: false,
};
} else { } else {
this.search(); this.search();
} }
@ -382,7 +390,7 @@ export class Search extends Component<any, SearchState> {
render() { render() {
return ( return (
<div class="container"> <div className="container">
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
@ -407,12 +415,12 @@ export class Search extends Component<any, SearchState> {
searchForm() { searchForm() {
return ( return (
<form <form
class="form-inline" className="form-inline"
onSubmit={linkEvent(this, this.handleSearchSubmit)} onSubmit={linkEvent(this, this.handleSearchSubmit)}
> >
<input <input
type="text" type="text"
class="form-control mr-2 mb-2" className="form-control mr-2 mb-2"
value={this.state.searchText} value={this.state.searchText}
placeholder={`${i18n.t("search")}...`} placeholder={`${i18n.t("search")}...`}
aria-label={i18n.t("search")} aria-label={i18n.t("search")}
@ -420,7 +428,7 @@ export class Search extends Component<any, SearchState> {
required required
minLength={1} minLength={1}
/> />
<button type="submit" class="btn btn-secondary mr-2 mb-2"> <button type="submit" className="btn btn-secondary mr-2 mb-2">
{this.state.loading ? <Spinner /> : <span>{i18n.t("search")}</span>} {this.state.loading ? <Spinner /> : <span>{i18n.t("search")}</span>}
</button> </button>
</form> </form>
@ -433,7 +441,7 @@ export class Search extends Component<any, SearchState> {
<select <select
value={this.state.type_} value={this.state.type_}
onChange={linkEvent(this, this.handleTypeChange)} onChange={linkEvent(this, this.handleTypeChange)}
class="custom-select w-auto mb-2" className="custom-select w-auto mb-2"
aria-label={i18n.t("type")} aria-label={i18n.t("type")}
> >
<option disabled aria-hidden="true"> <option disabled aria-hidden="true">
@ -448,7 +456,7 @@ export class Search extends Component<any, SearchState> {
<option value={SearchType.Users}>{i18n.t("users")}</option> <option value={SearchType.Users}>{i18n.t("users")}</option>
<option value={SearchType.Url}>{i18n.t("url")}</option> <option value={SearchType.Url}>{i18n.t("url")}</option>
</select> </select>
<span class="ml-2"> <span className="ml-2">
<ListingTypeSelect <ListingTypeSelect
type_={this.state.listingType} type_={this.state.listingType}
showLocal={showLocal(this.isoData)} showLocal={showLocal(this.isoData)}
@ -456,7 +464,7 @@ export class Search extends Component<any, SearchState> {
onChange={this.handleListingTypeChange} onChange={this.handleListingTypeChange}
/> />
</span> </span>
<span class="ml-2"> <span className="ml-2">
<SortSelect <SortSelect
sort={this.state.sort} sort={this.state.sort}
onChange={this.handleSortChange} onChange={this.handleSortChange}
@ -464,7 +472,7 @@ export class Search extends Component<any, SearchState> {
hideMostComments hideMostComments
/> />
</span> </span>
<div class="form-row"> <div className="form-row">
{this.state.communities.length > 0 && this.communityFilter()} {this.state.communities.length > 0 && this.communityFilter()}
{this.creatorFilter()} {this.creatorFilter()}
</div> </div>
@ -577,8 +585,8 @@ export class Search extends Component<any, SearchState> {
return ( return (
<div> <div>
{combined.map(i => ( {combined.map(i => (
<div class="row"> <div key={i.published} className="row">
<div class="col-12"> <div className="col-12">
{i.type_ == "posts" && ( {i.type_ == "posts" && (
<PostListing <PostListing
key={(i.data as PostView).post.id} key={(i.data as PostView).post.id}
@ -589,6 +597,8 @@ export class Search extends Component<any, SearchState> {
showCommunity showCommunity
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)} enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
viewOnly
/> />
)} )}
{i.type_ == "comments" && ( {i.type_ == "comments" && (
@ -602,19 +612,21 @@ export class Search extends Component<any, SearchState> {
}, },
]} ]}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
viewOnly
moderators={None} moderators={None}
admins={None} admins={None}
maxCommentsShown={None} maxCommentsShown={None}
locked locked
noIndent noIndent
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
/> />
)} )}
{i.type_ == "communities" && ( {i.type_ == "communities" && (
<div>{this.communityListing(i.data as CommunityView)}</div> <div>{this.communityListing(i.data as CommunityView)}</div>
)} )}
{i.type_ == "users" && ( {i.type_ == "users" && (
<div>{this.userListing(i.data as PersonViewSafe)}</div> <div>{this.personListing(i.data as PersonViewSafe)}</div>
)} )}
</div> </div>
</div> </div>
@ -639,12 +651,14 @@ export class Search extends Component<any, SearchState> {
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(comments)} nodes={commentsToFlatNodes(comments)}
viewType={CommentViewType.Flat} viewType={CommentViewType.Flat}
viewOnly
locked locked
noIndent noIndent
moderators={None} moderators={None}
admins={None} admins={None}
maxCommentsShown={None} maxCommentsShown={None}
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
/> />
); );
} }
@ -663,17 +677,19 @@ export class Search extends Component<any, SearchState> {
return ( return (
<> <>
{posts.map(post => ( {posts.map(pv => (
<div class="row"> <div key={pv.post.id} className="row">
<div class="col-12"> <div className="col-12">
<PostListing <PostListing
post_view={post} post_view={pv}
showCommunity showCommunity
duplicates={None} duplicates={None}
moderators={None} moderators={None}
admins={None} admins={None}
enableDownvotes={enableDownvotes(this.state.siteRes)} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)} enableNsfw={enableNsfw(this.state.siteRes)}
allLanguages={this.state.siteRes.all_languages}
viewOnly
/> />
</div> </div>
</div> </div>
@ -696,9 +712,9 @@ export class Search extends Component<any, SearchState> {
return ( return (
<> <>
{communities.map(community => ( {communities.map(cv => (
<div class="row"> <div key={cv.community.id} className="row">
<div class="col-12">{this.communityListing(community)}</div> <div className="col-12">{this.communityListing(cv)}</div>
</div> </div>
))} ))}
</> </>
@ -719,9 +735,9 @@ export class Search extends Component<any, SearchState> {
return ( return (
<> <>
{users.map(user => ( {users.map(pvs => (
<div class="row"> <div key={pvs.person.id} className="row">
<div class="col-12">{this.userListing(user)}</div> <div className="col-12">{this.personListing(pvs)}</div>
</div> </div>
))} ))}
</> </>
@ -744,33 +760,37 @@ export class Search extends Component<any, SearchState> {
); );
} }
userListing(person_view: PersonViewSafe) { personListing(person_view: PersonViewSafe) {
return [ return (
<span> <>
<PersonListing person={person_view.person} showApubName /> <span>
</span>, <PersonListing person={person_view.person} showApubName />
<span>{` - ${i18n.t("number_of_comments", { </span>
count: person_view.counts.comment_count, <span>{` - ${i18n.t("number_of_comments", {
formattedCount: numToSI(person_view.counts.comment_count), count: person_view.counts.comment_count,
})}`}</span>, formattedCount: numToSI(person_view.counts.comment_count),
]; })}`}</span>
</>
);
} }
communityFilter() { communityFilter() {
return ( return (
<div class="form-group col-sm-6"> <div className="form-group col-sm-6">
<label class="col-form-label" htmlFor="community-filter"> <label className="col-form-label" htmlFor="community-filter">
{i18n.t("community")} {i18n.t("community")}
</label> </label>
<div> <div>
<select <select
class="form-control" className="form-control"
id="community-filter" id="community-filter"
value={this.state.communityId} value={this.state.communityId}
> >
<option value="0">{i18n.t("all")}</option> <option value="0">{i18n.t("all")}</option>
{this.state.communities.map(cv => ( {this.state.communities.map(cv => (
<option value={cv.community.id}>{communitySelectName(cv)}</option> <option key={cv.community.id} value={cv.community.id}>
{communitySelectName(cv)}
</option>
))} ))}
</select> </select>
</div> </div>
@ -780,13 +800,13 @@ export class Search extends Component<any, SearchState> {
creatorFilter() { creatorFilter() {
return ( return (
<div class="form-group col-sm-6"> <div className="form-group col-sm-6">
<label class="col-form-label" htmlFor="creator-filter"> <label className="col-form-label" htmlFor="creator-filter">
{capitalizeFirstLetter(i18n.t("creator"))} {capitalizeFirstLetter(i18n.t("creator"))}
</label> </label>
<div> <div>
<select <select
class="form-control" className="form-control"
id="creator-filter" id="creator-filter"
value={this.state.creatorId} value={this.state.creatorId}
> >
@ -852,10 +872,11 @@ export class Search extends Component<any, SearchState> {
}); });
if (this.state.q != "") { if (this.state.q != "") {
this.state.searchResponse = None; this.setState({
this.state.resolveObjectResponse = None; searchResponse: None,
this.state.loading = true; resolveObjectResponse: None,
this.setState(this.state); loading: true,
});
WebSocketService.Instance.send(wsClient.search(form)); WebSocketService.Instance.send(wsClient.search(form));
WebSocketService.Instance.send(wsClient.resolveObject(resolveObjectForm)); WebSocketService.Instance.send(wsClient.resolveObject(resolveObjectForm));
} }
@ -996,11 +1017,13 @@ export class Search extends Component<any, SearchState> {
let op = wsUserOp(msg); let op = wsUserOp(msg);
if (msg.error) { if (msg.error) {
if (msg.error == "couldnt_find_object") { if (msg.error == "couldnt_find_object") {
this.state.resolveObjectResponse = Some({ this.setState({
comment: None, resolveObjectResponse: Some({
post: None, comment: None,
community: None, post: None,
person: None, community: None,
person: None,
}),
}); });
this.checkFinishedLoading(); this.checkFinishedLoading();
} else { } else {
@ -1009,7 +1032,7 @@ export class Search extends Component<any, SearchState> {
} }
} else if (op == UserOperation.Search) { } else if (op == UserOperation.Search) {
let data = wsJsonToRes<SearchResponse>(msg, SearchResponse); let data = wsJsonToRes<SearchResponse>(msg, SearchResponse);
this.state.searchResponse = Some(data); this.setState({ searchResponse: Some(data) });
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.checkFinishedLoading(); this.checkFinishedLoading();
restoreScrollPosition(this.context); restoreScrollPosition(this.context);
@ -1032,12 +1055,11 @@ export class Search extends Component<any, SearchState> {
msg, msg,
ListCommunitiesResponse ListCommunitiesResponse
); );
this.state.communities = data.communities; this.setState({ communities: data.communities });
this.setState(this.state);
this.setupCommunityFilter(); this.setupCommunityFilter();
} else if (op == UserOperation.ResolveObject) { } else if (op == UserOperation.ResolveObject) {
let data = wsJsonToRes<ResolveObjectResponse>(msg, ResolveObjectResponse); let data = wsJsonToRes<ResolveObjectResponse>(msg, ResolveObjectResponse);
this.state.resolveObjectResponse = Some(data); this.setState({ resolveObjectResponse: Some(data) });
this.checkFinishedLoading(); this.checkFinishedLoading();
} }
} }
@ -1047,8 +1069,7 @@ export class Search extends Component<any, SearchState> {
this.state.searchResponse.isSome() && this.state.searchResponse.isSome() &&
this.state.resolveObjectResponse.isSome() this.state.resolveObjectResponse.isSome()
) { ) {
this.state.loading = false; this.setState({ loading: false });
this.setState(this.state);
} }
} }
} }

View file

@ -1,4 +1,4 @@
import { None, Option, Result, Some } from "@sniptt/monads"; import { Err, None, Ok, Option, Result, Some } from "@sniptt/monads";
import { ClassConstructor, deserialize, serialize } from "class-transformer"; import { ClassConstructor, deserialize, serialize } from "class-transformer";
import emojiShortName from "emoji-short-name"; import emojiShortName from "emoji-short-name";
import { import {
@ -23,6 +23,7 @@ import {
PersonViewSafe, PersonViewSafe,
PostReportView, PostReportView,
PostView, PostView,
PrivateMessageReportView,
PrivateMessageView, PrivateMessageView,
RegistrationApplicationView, RegistrationApplicationView,
Search, Search,
@ -69,7 +70,7 @@ export const webArchiveUrl = "https://web.archive.org";
export const elementUrl = "https://element.io"; export const elementUrl = "https://element.io";
export const postRefetchSeconds: number = 60 * 1000; export const postRefetchSeconds: number = 60 * 1000;
export const fetchLimit = 20; export const fetchLimit = 40;
export const trendingFetchLimit = 6; export const trendingFetchLimit = 6;
export const mentionDropdownFetchLimit = 10; export const mentionDropdownFetchLimit = 10;
export const commentTreeMaxDepth = 8; export const commentTreeMaxDepth = 8;
@ -246,14 +247,10 @@ export function isAdmin(
}); });
} }
export function amAdmin( export function amAdmin(myUserInfo = UserService.Instance.myUserInfo): boolean {
admins: Option<PersonViewSafe[]>, return myUserInfo
myUserInfo = UserService.Instance.myUserInfo .map(mui => mui.local_user_view.person.admin)
): boolean { .unwrapOr(false);
return myUserInfo.match({
some: mui => isAdmin(admins, mui.local_user_view.person.id),
none: false,
});
} }
export function amCommunityCreator( export function amCommunityCreator(
@ -418,7 +415,7 @@ export function getLanguages(
myUserInfo = UserService.Instance.myUserInfo myUserInfo = UserService.Instance.myUserInfo
): string[] { ): string[] {
let myLang = myUserInfo let myLang = myUserInfo
.map(m => m.local_user_view.local_user.lang) .map(m => m.local_user_view.local_user.interface_language)
.unwrapOr("browser"); .unwrapOr("browser");
let lang = override || myLang; let lang = override || myLang;
@ -638,21 +635,18 @@ export function notifyPrivateMessage(pmv: PrivateMessageView, router: any) {
function notify(info: NotifyInfo, router: any) { function notify(info: NotifyInfo, router: any) {
messageToastify(info, router); messageToastify(info, router);
// TODO absolute nightmare bug, but notifs are currently broken. if (Notification.permission !== "granted") Notification.requestPermission();
// Notification.new will try to do a browser fetch ??? else {
var notification = new Notification(info.name, {
...{ body: info.body },
...(info.icon.isSome() && { icon: info.icon.unwrap() }),
});
// if (Notification.permission !== "granted") Notification.requestPermission(); notification.onclick = (ev: Event): any => {
// else { ev.preventDefault();
// var notification = new Notification(info.name, { router.history.push(info.link);
// icon: info.icon, };
// body: info.body, }
// });
// notification.onclick = (ev: Event): any => {
// ev.preventDefault();
// router.history.push(info.link);
// };
// }
} }
export function setupTribute() { export function setupTribute() {
@ -959,6 +953,7 @@ export function editPostRes(data: PostView, post: PostView) {
} }
} }
// TODO possible to make these generic?
export function updatePostReportRes( export function updatePostReportRes(
data: PostReportView, data: PostReportView,
reports: PostReportView[] reports: PostReportView[]
@ -979,6 +974,18 @@ export function updateCommentReportRes(
} }
} }
export function updatePrivateMessageReportRes(
data: PrivateMessageReportView,
reports: PrivateMessageReportView[]
) {
let found = reports.find(
c => c.private_message_report.id == data.private_message_report.id
);
if (found) {
found.private_message_report = data.private_message_report;
}
}
export function updateRegistrationApplicationRes( export function updateRegistrationApplicationRes(
data: RegistrationApplicationView, data: RegistrationApplicationView,
applications: RegistrationApplicationView[] applications: RegistrationApplicationView[]
@ -1338,44 +1345,11 @@ export const choicesConfig = {
shouldSort: false, shouldSort: false,
searchResultLimit: fetchLimit, searchResultLimit: fetchLimit,
classNames: { classNames: {
containerOuter: "choices", containerOuter: "choices custom-select px-0",
containerInner: "choices__inner bg-secondary border-0",
input: "form-control",
inputCloned: "choices__input--cloned",
list: "choices__list",
listItems: "choices__list--multiple",
listSingle: "choices__list--single",
listDropdown: "choices__list--dropdown",
item: "choices__item bg-secondary",
itemSelectable: "choices__item--selectable",
itemDisabled: "choices__item--disabled",
itemChoice: "choices__item--choice",
placeholder: "choices__placeholder",
group: "choices__group",
groupHeading: "choices__heading",
button: "choices__button",
activeState: "is-active",
focusState: "is-focused",
openState: "is-open",
disabledState: "is-disabled",
highlightedState: "text-info",
selectedState: "text-info",
flippedState: "is-flipped",
loadingState: "is-loading",
noResults: "has-no-results",
noChoices: "has-no-choices",
},
};
export const choicesModLogConfig = {
shouldSort: false,
searchResultLimit: fetchLimit,
classNames: {
containerOuter: "choices mb-2 custom-select col-4 px-0",
containerInner: containerInner:
"choices__inner bg-secondary border-0 py-0 modlog-choices-font-size", "choices__inner bg-secondary border-0 py-0 modlog-choices-font-size",
input: "form-control", input: "form-control",
inputCloned: "choices__input--cloned w-100", inputCloned: "choices__input--cloned",
list: "choices__list", list: "choices__list",
listItems: "choices__list--multiple", listItems: "choices__list--multiple",
listSingle: "choices__list--single py-0", listSingle: "choices__list--single py-0",
@ -1472,3 +1446,62 @@ export function postToCommentSortType(sort: SortType): CommentSortType {
return CommentSortType.Top; return CommentSortType.Top;
} }
} }
export function arrayGet<T>(arr: Array<T>, index: number): Result<T, string> {
let out = arr.at(index);
if (out == undefined) {
return Err("Index undefined");
} else {
return Ok(out);
}
}
export function myFirstDiscussionLanguageId(
myUserInfo = UserService.Instance.myUserInfo
): Option<number> {
return myUserInfo.andThen(mui =>
arrayGet(mui.discussion_languages, 0)
.ok()
.map(i => i.id)
);
}
export function canCreateCommunity(
siteRes: GetSiteResponse,
myUserInfo = UserService.Instance.myUserInfo
): boolean {
let adminOnly = siteRes.site_view
.map(s => s.site.community_creation_admin_only)
.unwrapOr(false);
return !adminOnly || amAdmin(myUserInfo);
}
export function isPostBlocked(
pv: PostView,
myUserInfo = UserService.Instance.myUserInfo
): boolean {
return myUserInfo
.map(
mui =>
mui.community_blocks
.map(c => c.community.id)
.includes(pv.community.id) ||
mui.person_blocks.map(p => p.target.id).includes(pv.creator.id)
)
.unwrapOr(false);
}
/// Checks to make sure you can view NSFW posts. Returns true if you can.
export function nsfwCheck(
pv: PostView,
myUserInfo = UserService.Instance.myUserInfo
): boolean {
let nsfw = pv.post.nsfw || pv.community.nsfw;
return (
!nsfw ||
(nsfw &&
myUserInfo
.map(m => m.local_user_view.local_user.show_nsfw)
.unwrapOr(false))
);
}

2763
yarn.lock

File diff suppressed because it is too large Load diff