Adding option types 2 (#689)

* Not working, because of wrong API types.

* Adding Rust-style Result and Option types.

- Fixes #646

* Updating to use new lemmy-js-client with Options.
This commit is contained in:
Dessalines 2022-06-21 17:42:29 -04:00 committed by GitHub
parent d41e19f3f1
commit d905c91e1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 5883 additions and 4485 deletions

View file

@ -52,11 +52,13 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.17.9", "@babel/core": "^7.17.9",
"@babel/plugin-proposal-decorators": "^7.18.2",
"@babel/plugin-transform-runtime": "^7.17.0", "@babel/plugin-transform-runtime": "^7.17.0",
"@babel/plugin-transform-typescript": "^7.16.1", "@babel/plugin-transform-typescript": "^7.16.1",
"@babel/preset-env": "7.16.11", "@babel/preset-env": "7.16.11",
"@babel/preset-typescript": "^7.16.0", "@babel/preset-typescript": "^7.16.0",
"@babel/runtime": "^7.17.9", "@babel/runtime": "^7.17.9",
"@sniptt/monads": "^0.5.10",
"@types/autosize": "^4.0.0", "@types/autosize": "^4.0.0",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/node": "^17.0.29", "@types/node": "^17.0.29",
@ -67,6 +69,7 @@
"babel-plugin-inferno": "^6.4.0", "babel-plugin-inferno": "^6.4.0",
"bootstrap": "^5.1.3", "bootstrap": "^5.1.3",
"bootswatch": "^5.1.3", "bootswatch": "^5.1.3",
"class-transformer": "^0.5.1",
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.4", "copy-webpack-plugin": "^10.2.4",
"css-loader": "^6.7.1", "css-loader": "^6.7.1",
@ -74,7 +77,7 @@
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"husky": "^7.0.4", "husky": "^7.0.4",
"import-sort-style-module": "^6.0.0", "import-sort-style-module": "^6.0.0",
"lemmy-js-client": "0.16.4", "lemmy-js-client": "0.17.0-rc.30",
"lint-staged": "^12.4.1", "lint-staged": "^12.4.1",
"mini-css-extract-plugin": "^2.6.0", "mini-css-extract-plugin": "^2.6.0",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
@ -82,6 +85,7 @@
"prettier-plugin-import-sort": "^0.0.7", "prettier-plugin-import-sort": "^0.0.7",
"prettier-plugin-organize-imports": "^2.3.4", "prettier-plugin-organize-imports": "^2.3.4",
"prettier-plugin-packagejson": "^2.2.17", "prettier-plugin-packagejson": "^2.2.17",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"run-node-webpack-plugin": "^1.3.0", "run-node-webpack-plugin": "^1.3.0",
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",

View file

@ -1,14 +1,15 @@
import { hydrate } from "inferno-hydrate"; import { hydrate } from "inferno-hydrate";
import { BrowserRouter } from "inferno-router"; import { BrowserRouter } from "inferno-router";
import { GetSiteResponse } from "lemmy-js-client";
import { App } from "../shared/components/app/app"; import { App } from "../shared/components/app/app";
import { initializeSite } from "../shared/utils"; import { convertWindowJson, initializeSite } from "../shared/utils";
const site = window.isoData.site_res; const site = convertWindowJson(GetSiteResponse, window.isoData.site_res);
initializeSite(site); initializeSite(site);
const wrapper = ( const wrapper = (
<BrowserRouter> <BrowserRouter>
<App siteRes={window.isoData.site_res} /> <App />
</BrowserRouter> </BrowserRouter>
); );

View file

@ -1,3 +1,5 @@
import { None, Option } from "@sniptt/monads";
import { serialize as serializeO } from "class-transformer";
import express from "express"; import express from "express";
import fs from "fs"; import fs from "fs";
import { IncomingHttpHeaders } from "http"; import { IncomingHttpHeaders } from "http";
@ -5,7 +7,7 @@ import { Helmet } from "inferno-helmet";
import { matchPath, StaticRouter } from "inferno-router"; import { matchPath, StaticRouter } from "inferno-router";
import { renderToString } from "inferno-server"; import { renderToString } from "inferno-server";
import IsomorphicCookie from "isomorphic-cookie"; import IsomorphicCookie from "isomorphic-cookie";
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client"; import { GetSite, GetSiteResponse, LemmyHttp, toOption } from "lemmy-js-client";
import path from "path"; import path from "path";
import process from "process"; import process from "process";
import serialize from "serialize-javascript"; import serialize from "serialize-javascript";
@ -18,7 +20,7 @@ import {
IsoData, IsoData,
} from "../shared/interfaces"; } from "../shared/interfaces";
import { routes } from "../shared/routes"; import { routes } from "../shared/routes";
import { initializeSite, setOptionalAuth } from "../shared/utils"; import { initializeSite } from "../shared/utils";
const server = express(); const server = express();
const [hostname, port] = process.env["LEMMY_UI_HOST"] const [hostname, port] = process.env["LEMMY_UI_HOST"]
@ -115,10 +117,9 @@ server.get("/*", async (req, res) => {
try { try {
const activeRoute = routes.find(route => matchPath(req.path, route)) || {}; const activeRoute = routes.find(route => matchPath(req.path, route)) || {};
const context = {} as any; const context = {} as any;
let auth: string = IsomorphicCookie.load("jwt", req); let auth: Option<string> = toOption(IsomorphicCookie.load("jwt", req));
let getSiteForm: GetSite = {}; let getSiteForm = new GetSite({ auth });
setOptionalAuth(getSiteForm, auth);
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
@ -138,8 +139,8 @@ server.get("/*", async (req, res) => {
console.error( console.error(
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie" "Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
); );
delete getSiteForm.auth; getSiteForm.auth = None;
delete initialFetchReq.auth; initialFetchReq.auth = None;
try_site = await initialFetchReq.client.getSite(getSiteForm); try_site = await initialFetchReq.client.getSite(getSiteForm);
} }
let site: GetSiteResponse = try_site; let site: GetSiteResponse = try_site;
@ -170,7 +171,7 @@ server.get("/*", async (req, res) => {
const wrapper = ( const wrapper = (
<StaticRouter location={req.url} context={isoData}> <StaticRouter location={req.url} context={isoData}>
<App siteRes={isoData.site_res} /> <App />
</StaticRouter> </StaticRouter>
); );
if (context.url) { if (context.url) {
@ -194,7 +195,7 @@ server.get("/*", async (req, res) => {
<!DOCTYPE html> <!DOCTYPE html>
<html ${helmet.htmlAttributes.toString()} lang="en"> <html ${helmet.htmlAttributes.toString()} lang="en">
<head> <head>
<script>window.isoData = ${serialize(isoData)}</script> <script>window.isoData = ${serializeO(isoData)}</script>
<script>window.lemmyConfig = ${serialize(config)}</script> <script>window.lemmyConfig = ${serialize(config)}</script>
<!-- A remote debugging utility for mobile --> <!-- A remote debugging utility for mobile -->

View file

@ -2,53 +2,46 @@ import { Component } from "inferno";
import { Helmet } from "inferno-helmet"; import { Helmet } from "inferno-helmet";
import { Provider } from "inferno-i18next-dess"; import { Provider } from "inferno-i18next-dess";
import { Route, Switch } from "inferno-router"; import { Route, Switch } from "inferno-router";
import { GetSiteResponse } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { routes } from "../../routes"; import { routes } from "../../routes";
import { favIconPngUrl, favIconUrl } from "../../utils"; import { favIconPngUrl, favIconUrl, setIsoData } from "../../utils";
import { Footer } from "./footer"; import { Footer } from "./footer";
import { Navbar } from "./navbar"; import { Navbar } from "./navbar";
import { NoMatch } from "./no-match"; import { NoMatch } from "./no-match";
import "./styles.scss"; import "./styles.scss";
import { Theme } from "./theme"; import { Theme } from "./theme";
export interface AppProps { export class App extends Component<any, any> {
siteRes: GetSiteResponse; private isoData = setIsoData(this.context);
}
export class App extends Component<AppProps, any> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
} }
render() { render() {
let siteRes = this.props.siteRes; let siteRes = this.isoData.site_res;
let siteView = siteRes.site_view;
return ( return (
<> <>
<Provider i18next={i18n}> <Provider i18next={i18n}>
<div> <div>
<Theme <Theme defaultTheme={siteView.map(s => s.site.default_theme)} />
myUserInfo={siteRes.my_user} {siteView
defaultTheme={siteRes?.site_view?.site?.default_theme} .andThen(s => s.site.icon)
/> .match({
{siteRes && some: icon => (
siteRes.site_view && <Helmet>
this.props.siteRes.site_view.site.icon && ( <link
<Helmet> id="favicon"
<link rel="shortcut icon"
id="favicon" type="image/x-icon"
rel="shortcut icon" href={icon || favIconUrl}
type="image/x-icon" />
href={this.props.siteRes.site_view.site.icon || favIconUrl} <link rel="apple-touch-icon" href={icon || favIconPngUrl} />
/> </Helmet>
<link ),
rel="apple-touch-icon" none: <></>,
href={ })}
this.props.siteRes.site_view.site.icon || favIconPngUrl <Navbar siteRes={siteRes} />
}
/>
</Helmet>
)}
<Navbar site_res={this.props.siteRes} />
<div class="mt-4 p-0 fl-1"> <div class="mt-4 p-0 fl-1">
<Switch> <Switch>
{routes.map(({ path, exact, component: C, ...rest }) => ( {routes.map(({ path, exact, component: C, ...rest }) => (
@ -62,7 +55,7 @@ export class App extends Component<AppProps, any> {
<Route render={props => <NoMatch {...props} />} /> <Route render={props => <NoMatch {...props} />} />
</Switch> </Switch>
</div> </div>
<Footer site={this.props.siteRes} /> <Footer site={siteRes} />
</div> </div>
</Provider> </Provider>
</> </>

View file

@ -32,7 +32,9 @@ export class Footer extends Component<FooterProps, any> {
{i18n.t("modlog")} {i18n.t("modlog")}
</NavLink> </NavLink>
</li> </li>
{this.props.site.site_view?.site.legal_information && ( {this.props.site.site_view
.andThen(s => s.site.legal_information)
.isSome() && (
<li className="nav-item"> <li className="nav-item">
<NavLink className="nav-link" to="/legal"> <NavLink className="nav-link" to="/legal">
{i18n.t("legal_information")} {i18n.t("legal_information")}

View file

@ -1,3 +1,4 @@
import { None, Some } 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 {
@ -11,12 +12,15 @@ import {
GetUnreadRegistrationApplicationCountResponse, GetUnreadRegistrationApplicationCountResponse,
PrivateMessageResponse, PrivateMessageResponse,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, amAdmin,
auth,
donateLemmyUrl, donateLemmyUrl,
getLanguages, getLanguages,
isBrowser, isBrowser,
@ -27,19 +31,16 @@ import {
showAvatars, showAvatars,
toast, toast,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
import { PictrsImage } from "../common/pictrs-image"; import { PictrsImage } from "../common/pictrs-image";
interface NavbarProps { interface NavbarProps {
site_res: GetSiteResponse; siteRes: GetSiteResponse;
} }
interface NavbarState { interface NavbarState {
isLoggedIn: boolean;
expanded: boolean; expanded: boolean;
unreadInboxCount: number; unreadInboxCount: number;
unreadReportCount: number; unreadReportCount: number;
@ -58,7 +59,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
private unreadApplicationCountSub: Subscription; private unreadApplicationCountSub: Subscription;
private searchTextField: RefObject<HTMLInputElement>; private searchTextField: RefObject<HTMLInputElement>;
emptyState: NavbarState = { emptyState: NavbarState = {
isLoggedIn: !!this.props.site_res.my_user,
unreadInboxCount: 0, unreadInboxCount: 0,
unreadReportCount: 0, unreadReportCount: 0,
unreadApplicationCount: 0, unreadApplicationCount: 0,
@ -81,18 +81,13 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
// Subscribe to jwt changes // Subscribe to jwt changes
if (isBrowser()) { if (isBrowser()) {
this.searchTextField = createRef(); this.searchTextField = createRef();
console.log(`isLoggedIn = ${this.state.isLoggedIn}`);
// On the first load, check the unreads // On the first load, check the unreads
if (this.state.isLoggedIn == false) { if (UserService.Instance.myUserInfo.isSome()) {
// setTheme(data.my_user.theme, true);
// i18n.changeLanguage(getLanguage());
// i18n.changeLanguage('de');
} else {
this.requestNotificationPermission(); this.requestNotificationPermission();
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.userJoin({ wsClient.userJoin({
auth: authField(), auth: auth().unwrap(),
}) })
); );
this.fetchUnreads(); this.fetchUnreads();
@ -100,13 +95,11 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
this.userSub = UserService.Instance.jwtSub.subscribe(res => { this.userSub = UserService.Instance.jwtSub.subscribe(res => {
// A login // A login
if (res !== undefined) { if (res.isSome()) {
this.requestNotificationPermission(); this.requestNotificationPermission();
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getSite({ auth: authField() }) wsClient.getSite({ auth: res.map(r => r.jwt) })
); );
} else {
this.setState({ isLoggedIn: false });
} }
}); });
@ -157,32 +150,28 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
// TODO class active corresponding to current page // TODO class active corresponding to current page
navbar() { navbar() {
let myUserInfo =
UserService.Instance.myUserInfo || this.props.site_res.my_user;
let person = myUserInfo?.local_user_view.person;
return ( return (
<nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3"> <nav class="navbar navbar-expand-md navbar-light shadow-sm p-0 px-3">
<div class="container"> <div class="container">
{this.props.site_res.site_view && ( {this.props.siteRes.site_view.match({
<NavLink some: siteView => (
to="/" <NavLink
onMouseUp={linkEvent(this, this.handleHideExpandNavbar)} to="/"
title={ onMouseUp={linkEvent(this, this.handleHideExpandNavbar)}
this.props.site_res.site_view.site.description || title={siteView.site.description.unwrapOr(siteView.site.name)}
this.props.site_res.site_view.site.name className="d-flex align-items-center navbar-brand mr-md-3"
} >
className="d-flex align-items-center navbar-brand mr-md-3" {siteView.site.icon.match({
> some: icon =>
{this.props.site_res.site_view.site.icon && showAvatars() && ( showAvatars() && <PictrsImage src={icon} icon />,
<PictrsImage none: <></>,
src={this.props.site_res.site_view.site.icon} })}
icon {siteView.site.name}
/> </NavLink>
)} ),
{this.props.site_res.site_view.site.name} none: <></>,
</NavLink> })}
)} {UserService.Instance.myUserInfo.isSome() && (
{this.state.isLoggedIn && (
<> <>
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li className="nav-item"> <li className="nav-item">
@ -204,7 +193,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</NavLink> </NavLink>
</li> </li>
</ul> </ul>
{UserService.Instance.myUserInfo?.moderates.length > 0 && ( {this.moderatesSomething && (
<ul class="navbar-nav ml-1"> <ul class="navbar-nav ml-1">
<li className="nav-item"> <li className="nav-item">
<NavLink <NavLink
@ -226,8 +215,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</li> </li>
</ul> </ul>
)} )}
{UserService.Instance.myUserInfo?.local_user_view.person {this.amAdmin && (
.admin && (
<ul class="navbar-nav ml-1"> <ul class="navbar-nav ml-1">
<li className="nav-item"> <li className="nav-item">
<NavLink <NavLink
@ -312,7 +300,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</li> </li>
</ul> </ul>
<ul class="navbar-nav my-2"> <ul class="navbar-nav my-2">
{this.canAdmin && ( {this.amAdmin && (
<li className="nav-item"> <li className="nav-item">
<NavLink <NavLink
to="/admin" to="/admin"
@ -358,7 +346,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</button> </button>
</form> </form>
)} )}
{this.state.isLoggedIn ? ( {UserService.Instance.myUserInfo.isSome() ? (
<> <>
<ul class="navbar-nav my-2"> <ul class="navbar-nav my-2">
<li className="nav-item"> <li className="nav-item">
@ -380,7 +368,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</NavLink> </NavLink>
</li> </li>
</ul> </ul>
{UserService.Instance.myUserInfo?.moderates.length > 0 && ( {this.moderatesSomething && (
<ul class="navbar-nav my-2"> <ul class="navbar-nav my-2">
<li className="nav-item"> <li className="nav-item">
<NavLink <NavLink
@ -402,8 +390,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</li> </li>
</ul> </ul>
)} )}
{UserService.Instance.myUserInfo?.local_user_view.person {this.amAdmin && (
.admin && (
<ul class="navbar-nav my-2"> <ul class="navbar-nav my-2">
<li className="nav-item"> <li className="nav-item">
<NavLink <NavLink
@ -427,69 +414,81 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</li> </li>
</ul> </ul>
)} )}
<ul class="navbar-nav"> {UserService.Instance.myUserInfo
<li class="nav-item dropdown"> .map(m => m.local_user_view.person)
<button .match({
class="nav-link btn btn-link dropdown-toggle" some: person => (
onClick={linkEvent(this, this.handleToggleDropdown)} <ul class="navbar-nav">
id="navbarDropdown" <li class="nav-item dropdown">
role="button"
aria-expanded="false"
>
<span>
{person.avatar && showAvatars() && (
<PictrsImage src={person.avatar} icon />
)}
{person.display_name
? person.display_name
: person.name}
</span>
</button>
{this.state.showDropdown && (
<div
class="dropdown-content"
onMouseLeave={linkEvent(
this,
this.handleToggleDropdown
)}
>
<li className="nav-item">
<NavLink
to={`/u/${UserService.Instance.myUserInfo.local_user_view.person.name}`}
className="nav-link"
title={i18n.t("profile")}
>
<Icon icon="user" classes="mr-1" />
{i18n.t("profile")}
</NavLink>
</li>
<li className="nav-item">
<NavLink
to="/settings"
className="nav-link"
title={i18n.t("settings")}
>
<Icon icon="settings" classes="mr-1" />
{i18n.t("settings")}
</NavLink>
</li>
<li>
<hr class="dropdown-divider" />
</li>
<li className="nav-item">
<button <button
className="nav-link btn btn-link" class="nav-link btn btn-link dropdown-toggle"
onClick={linkEvent(this, this.handleLogoutClick)} onClick={linkEvent(this, this.handleToggleDropdown)}
title="test" id="navbarDropdown"
role="button"
aria-expanded="false"
> >
<Icon icon="log-out" classes="mr-1" /> <span>
{i18n.t("logout")} {showAvatars() &&
person.avatar.match({
some: avatar => (
<PictrsImage src={avatar} icon />
),
none: <></>,
})}
{person.display_name.unwrapOr(person.name)}
</span>
</button> </button>
{this.state.showDropdown && (
<div
class="dropdown-content"
onMouseLeave={linkEvent(
this,
this.handleToggleDropdown
)}
>
<li className="nav-item">
<NavLink
to={`/u/${person.name}`}
className="nav-link"
title={i18n.t("profile")}
>
<Icon icon="user" classes="mr-1" />
{i18n.t("profile")}
</NavLink>
</li>
<li className="nav-item">
<NavLink
to="/settings"
className="nav-link"
title={i18n.t("settings")}
>
<Icon icon="settings" classes="mr-1" />
{i18n.t("settings")}
</NavLink>
</li>
<li>
<hr class="dropdown-divider" />
</li>
<li className="nav-item">
<button
className="nav-link btn btn-link"
onClick={linkEvent(
this,
this.handleLogoutClick
)}
title="test"
>
<Icon icon="log-out" classes="mr-1" />
{i18n.t("logout")}
</button>
</li>
</div>
)}
</li> </li>
</div> </ul>
)} ),
</li> none: <></>,
</ul> })}
</> </>
) : ( ) : (
<ul class="navbar-nav my-2"> <ul class="navbar-nav my-2">
@ -521,6 +520,24 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
); );
} }
get moderatesSomething(): boolean {
return (
UserService.Instance.myUserInfo.map(m => m.moderates).unwrapOr([])
.length > 0
);
}
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.state.expanded = !i.state.expanded;
i.setState(i.state); i.setState(i.state);
@ -561,8 +578,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
handleLogoutClick(i: Navbar) { handleLogoutClick(i: Navbar) {
i.setState({ showDropdown: false, expanded: false }); i.setState({ showDropdown: false, expanded: false });
UserService.Instance.logout(); UserService.Instance.logout();
window.location.href = "/";
location.reload();
} }
handleToggleDropdown(i: Navbar) { handleToggleDropdown(i: Navbar) {
@ -576,98 +591,117 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
if (msg.error) { if (msg.error) {
if (msg.error == "not_logged_in") { if (msg.error == "not_logged_in") {
UserService.Instance.logout(); UserService.Instance.logout();
location.reload();
} }
return; return;
} else if (msg.reconnect) { } else if (msg.reconnect) {
console.log(i18n.t("websocket_reconnected")); console.log(i18n.t("websocket_reconnected"));
WebSocketService.Instance.send( if (UserService.Instance.myUserInfo.isSome()) {
wsClient.userJoin({ WebSocketService.Instance.send(
auth: authField(), wsClient.userJoin({
}) auth: auth().unwrap(),
); })
this.fetchUnreads(); );
this.fetchUnreads();
}
} else if (op == UserOperation.GetUnreadCount) { } else if (op == UserOperation.GetUnreadCount) {
let data = wsJsonToRes<GetUnreadCountResponse>(msg).data; let data = wsJsonToRes<GetUnreadCountResponse>(
msg,
GetUnreadCountResponse
);
this.state.unreadInboxCount = this.state.unreadInboxCount =
data.replies + data.mentions + data.private_messages; data.replies + data.mentions + data.private_messages;
this.setState(this.state); this.setState(this.state);
this.sendUnreadCount(); this.sendUnreadCount();
} else if (op == UserOperation.GetReportCount) { } else if (op == UserOperation.GetReportCount) {
let data = wsJsonToRes<GetReportCountResponse>(msg).data; let data = wsJsonToRes<GetReportCountResponse>(
msg,
GetReportCountResponse
);
this.state.unreadReportCount = data.post_reports + data.comment_reports; this.state.unreadReportCount = data.post_reports + data.comment_reports;
this.setState(this.state); this.setState(this.state);
this.sendReportUnread(); this.sendReportUnread();
} else if (op == UserOperation.GetUnreadRegistrationApplicationCount) { } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
let data = let data = wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(
wsJsonToRes<GetUnreadRegistrationApplicationCountResponse>(msg).data; msg,
GetUnreadRegistrationApplicationCountResponse
);
this.state.unreadApplicationCount = data.registration_applications; this.state.unreadApplicationCount = data.registration_applications;
this.setState(this.state); this.setState(this.state);
this.sendApplicationUnread(); this.sendApplicationUnread();
} else if (op == UserOperation.GetSite) { } else if (op == UserOperation.GetSite) {
// This is only called on a successful login // This is only called on a successful login
let data = wsJsonToRes<GetSiteResponse>(msg).data; let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
console.log(data.my_user);
UserService.Instance.myUserInfo = data.my_user; UserService.Instance.myUserInfo = data.my_user;
setTheme( UserService.Instance.myUserInfo.match({
UserService.Instance.myUserInfo.local_user_view.local_user.theme some: mui => {
); setTheme(mui.local_user_view.local_user.theme);
i18n.changeLanguage(getLanguages()[0]); i18n.changeLanguage(getLanguages()[0]);
this.state.isLoggedIn = true; this.setState(this.state);
this.setState(this.state); },
none: void 0,
});
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
if (this.state.isLoggedIn) { UserService.Instance.myUserInfo.match({
if ( some: mui => {
data.recipient_ids.includes( if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) {
UserService.Instance.myUserInfo.local_user_view.local_user.id this.state.unreadInboxCount++;
) this.setState(this.state);
) { this.sendUnreadCount();
this.state.unreadInboxCount++; notifyComment(data.comment_view, this.context.router);
this.setState(this.state); }
this.sendUnreadCount(); },
notifyComment(data.comment_view, this.context.router); none: void 0,
} });
}
} else if (op == UserOperation.CreatePrivateMessage) { } else if (op == UserOperation.CreatePrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>(msg).data; let data = wsJsonToRes<PrivateMessageResponse>(
msg,
PrivateMessageResponse
);
if (this.state.isLoggedIn) { UserService.Instance.myUserInfo.match({
if ( some: mui => {
data.private_message_view.recipient.id == if (
UserService.Instance.myUserInfo.local_user_view.person.id data.private_message_view.recipient.id ==
) { mui.local_user_view.person.id
this.state.unreadInboxCount++; ) {
this.setState(this.state); this.state.unreadInboxCount++;
this.sendUnreadCount(); this.setState(this.state);
notifyPrivateMessage(data.private_message_view, this.context.router); this.sendUnreadCount();
} notifyPrivateMessage(
} data.private_message_view,
this.context.router
);
}
},
none: void 0,
});
} }
} }
fetchUnreads() { fetchUnreads() {
console.log("Fetching inbox unreads..."); console.log("Fetching inbox unreads...");
let unreadForm: GetUnreadCount = { let unreadForm = new GetUnreadCount({
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm)); WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
console.log("Fetching reports..."); console.log("Fetching reports...");
let reportCountForm: GetReportCount = { let reportCountForm = new GetReportCount({
auth: authField(), community_id: None,
}; auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm)); WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
if (UserService.Instance.myUserInfo?.local_user_view.person.admin) { if (this.amAdmin) {
console.log("Fetching applications..."); console.log("Fetching applications...");
let applicationCountForm: GetUnreadRegistrationApplicationCount = { let applicationCountForm = new GetUnreadRegistrationApplicationCount({
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getUnreadRegistrationApplicationCount(applicationCountForm) wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
); );
@ -694,23 +728,8 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
); );
} }
get canAdmin(): boolean {
return (
UserService.Instance.myUserInfo &&
this.props.site_res.admins
.map(a => a.person.id)
.includes(UserService.Instance.myUserInfo.local_user_view.person.id)
);
}
get canCreateCommunity(): boolean {
let adminOnly =
this.props.site_res.site_view?.site.community_creation_admin_only;
return !adminOnly || this.canAdmin;
}
requestNotificationPermission() { requestNotificationPermission() {
if (UserService.Instance.myUserInfo) { if (UserService.Instance.myUserInfo.isSome()) {
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
if (!Notification) { if (!Notification) {
toast(i18n.t("notifications_error"), "danger"); toast(i18n.t("notifications_error"), "danger");

View file

@ -1,16 +1,18 @@
import { Option } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { Helmet } from "inferno-helmet"; import { Helmet } from "inferno-helmet";
import { MyUserInfo } from "lemmy-js-client"; import { UserService } from "../../services";
interface Props { interface Props {
myUserInfo: MyUserInfo | undefined; defaultTheme: Option<string>;
defaultTheme?: string;
} }
export class Theme extends Component<Props> { export class Theme extends Component<Props> {
render() { render() {
let user = this.props.myUserInfo; let user = UserService.Instance.myUserInfo;
let hasTheme = user && user.local_user_view.local_user.theme !== "browser"; let hasTheme = user
.map(m => m.local_user_view.local_user.theme !== "browser")
.unwrapOr(false);
if (hasTheme) { if (hasTheme) {
return ( return (
@ -18,20 +20,22 @@ export class Theme extends Component<Props> {
<link <link
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
href={`/css/themes/${user.local_user_view.local_user.theme}.css`} href={`/css/themes/${
user.unwrap().local_user_view.local_user.theme
}.css`}
/> />
</Helmet> </Helmet>
); );
} else if ( } else if (
this.props.defaultTheme != null && this.props.defaultTheme.isSome() &&
this.props.defaultTheme != "browser" this.props.defaultTheme.unwrap() != "browser"
) { ) {
return ( return (
<Helmet> <Helmet>
<link <link
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
href={`/css/themes/${this.props.defaultTheme}.css`} href={`/css/themes/${this.props.defaultTheme.unwrap()}.css`}
/> />
</Helmet> </Helmet>
); );

View file

@ -1,3 +1,4 @@
import { Either, None, Option, 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";
@ -6,25 +7,27 @@ import {
CreateComment, CreateComment,
EditComment, EditComment,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { CommentNode as CommentNodeI } from "../../interfaces"; import { CommentNode as CommentNodeI } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, auth,
capitalizeFirstLetter, capitalizeFirstLetter,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
interface CommentFormProps { interface CommentFormProps {
postId?: number; /**
node?: CommentNodeI; // Can either be the parent, or the editable comment * Can either be the parent, or the editable comment. The right side is a postId.
*/
node: Either<CommentNodeI, number>;
edit?: boolean; edit?: boolean;
disabled?: boolean; disabled?: boolean;
focus?: boolean; focus?: boolean;
@ -34,19 +37,19 @@ interface CommentFormProps {
interface CommentFormState { interface CommentFormState {
buttonTitle: string; buttonTitle: string;
finished: boolean; finished: boolean;
formId: string; formId: Option<string>;
} }
export class CommentForm extends Component<CommentFormProps, CommentFormState> { export class CommentForm extends Component<CommentFormProps, CommentFormState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: CommentFormState = { private emptyState: CommentFormState = {
buttonTitle: !this.props.node buttonTitle: this.props.node.isRight()
? capitalizeFirstLetter(i18n.t("post")) ? capitalizeFirstLetter(i18n.t("post"))
: this.props.edit : this.props.edit
? capitalizeFirstLetter(i18n.t("save")) ? capitalizeFirstLetter(i18n.t("save"))
: capitalizeFirstLetter(i18n.t("reply")), : capitalizeFirstLetter(i18n.t("reply")),
finished: false, finished: false,
formId: "empty_form", formId: None,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -66,23 +69,25 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
} }
render() { render() {
let initialContent = this.props.node.match({
left: node =>
this.props.edit ? Some(node.comment_view.comment.content) : None,
right: () => None,
});
return ( return (
<div class="mb-3"> <div class="mb-3">
{UserService.Instance.myUserInfo ? ( {UserService.Instance.myUserInfo.isSome() ? (
<MarkdownTextArea <MarkdownTextArea
initialContent={ initialContent={initialContent}
this.props.edit buttonTitle={Some(this.state.buttonTitle)}
? this.props.node.comment_view.comment.content maxLength={None}
: null
}
buttonTitle={this.state.buttonTitle}
finished={this.state.finished} finished={this.state.finished}
replyType={!!this.props.node} replyType={this.props.node.isLeft()}
focus={this.props.focus} focus={this.props.focus}
disabled={this.props.disabled} disabled={this.props.disabled}
onSubmit={this.handleCommentSubmit} onSubmit={this.handleCommentSubmit}
onReplyCancel={this.handleReplyCancel} onReplyCancel={this.handleReplyCancel}
placeholder={i18n.t("comment_here")} placeholder={Some(i18n.t("comment_here"))}
/> />
) : ( ) : (
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
@ -101,28 +106,40 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
handleCommentSubmit(msg: { val: string; formId: string }) { handleCommentSubmit(msg: { val: string; formId: string }) {
let content = msg.val; let content = msg.val;
this.state.formId = msg.formId; this.state.formId = Some(msg.formId);
let node = this.props.node; this.props.node.match({
left: node => {
if (this.props.edit) { if (this.props.edit) {
let form: EditComment = { let form = new EditComment({
content, content,
form_id: this.state.formId, form_id: this.state.formId,
comment_id: node.comment_view.comment.id, comment_id: node.comment_view.comment.id,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.editComment(form)); WebSocketService.Instance.send(wsClient.editComment(form));
} else { } else {
let form: CreateComment = { let form = new CreateComment({
content, content,
form_id: this.state.formId, form_id: this.state.formId,
post_id: node ? node.comment_view.post.id : this.props.postId, post_id: node.comment_view.post.id,
parent_id: node ? node.comment_view.comment.id : null, parent_id: Some(node.comment_view.comment.id),
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.createComment(form)); WebSocketService.Instance.send(wsClient.createComment(form));
} }
},
right: postId => {
let form = new CreateComment({
content,
form_id: this.state.formId,
post_id: postId,
parent_id: None,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.createComment(form));
},
});
this.setState(this.state); this.setState(this.state);
} }
@ -135,15 +152,15 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
console.log(msg); console.log(msg);
// Only do the showing and hiding if logged in // Only do the showing and hiding if logged in
if (UserService.Instance.myUserInfo) { if (UserService.Instance.myUserInfo.isSome()) {
if ( if (
op == UserOperation.CreateComment || op == UserOperation.CreateComment ||
op == UserOperation.EditComment op == UserOperation.EditComment
) { ) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
// This only finishes this form, if the randomly generated form_id matches the one received // This only finishes this form, if the randomly generated form_id matches the one received
if (this.state.formId == data.form_id) { if (this.state.formId.unwrapOr("") == data.form_id.unwrapOr("")) {
this.setState({ finished: true }); this.setState({ finished: true });
// Necessary because it broke tribute for some reason // Necessary because it broke tribute for some reason

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
import { Option } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { CommunityModeratorView, PersonViewSafe } from "lemmy-js-client"; import { CommunityModeratorView, PersonViewSafe } from "lemmy-js-client";
import { CommentNode as CommentNodeI } from "../../interfaces"; import { CommentNode as CommentNodeI } from "../../interfaces";
@ -5,9 +6,9 @@ import { CommentNode } from "./comment-node";
interface CommentNodesProps { interface CommentNodesProps {
nodes: CommentNodeI[]; nodes: CommentNodeI[];
moderators?: CommunityModeratorView[]; moderators: Option<CommunityModeratorView[]>;
admins?: PersonViewSafe[]; admins: Option<PersonViewSafe[]>;
postCreatorId?: number; maxCommentsShown: Option<number>;
noBorder?: boolean; noBorder?: boolean;
noIndent?: boolean; noIndent?: boolean;
viewOnly?: boolean; viewOnly?: boolean;
@ -15,8 +16,7 @@ interface CommentNodesProps {
markable?: boolean; markable?: boolean;
showContext?: boolean; showContext?: boolean;
showCommunity?: boolean; showCommunity?: boolean;
maxCommentsShown?: number; enableDownvotes?: boolean;
enableDownvotes: boolean;
} }
export class CommentNodes extends Component<CommentNodesProps, any> { export class CommentNodes extends Component<CommentNodesProps, any> {
@ -25,9 +25,9 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
} }
render() { render() {
let maxComments = this.props.maxCommentsShown let maxComments = this.props.maxCommentsShown.unwrapOr(
? this.props.maxCommentsShown this.props.nodes.length
: this.props.nodes.length; );
return ( return (
<div className="comments"> <div className="comments">
@ -41,7 +41,6 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
locked={this.props.locked} locked={this.props.locked}
moderators={this.props.moderators} moderators={this.props.moderators}
admins={this.props.admins} admins={this.props.admins}
postCreatorId={this.props.postCreatorId}
markable={this.props.markable} markable={this.props.markable}
showContext={this.props.showContext} showContext={this.props.showContext}
showCommunity={this.props.showCommunity} showCommunity={this.props.showCommunity}

View file

@ -1,3 +1,4 @@
import { None } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { import {
@ -8,7 +9,7 @@ import {
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { CommentNode as CommentNodeI } from "../../interfaces"; import { CommentNode as CommentNodeI } from "../../interfaces";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { authField, wsClient } from "../../utils"; import { auth, wsClient } from "../../utils";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
import { CommentNode } from "./comment-node"; import { CommentNode } from "./comment-node";
@ -42,6 +43,7 @@ export class CommentReport extends Component<CommentReportProps, any> {
subscribed: false, subscribed: false,
saved: false, saved: false,
creator_blocked: false, creator_blocked: false,
recipient: None,
my_vote: r.my_vote, my_vote: r.my_vote,
}; };
@ -53,8 +55,8 @@ export class CommentReport extends Component<CommentReportProps, any> {
<div> <div>
<CommentNode <CommentNode
node={node} node={node}
moderators={[]} moderators={None}
admins={[]} admins={None}
enableDownvotes={true} enableDownvotes={true}
viewOnly={true} viewOnly={true}
showCommunity={true} showCommunity={true}
@ -65,21 +67,24 @@ export class CommentReport extends Component<CommentReportProps, any> {
<div> <div>
{i18n.t("reason")}: {r.comment_report.reason} {i18n.t("reason")}: {r.comment_report.reason}
</div> </div>
{r.resolver && ( {r.resolver.match({
<div> some: resolver => (
{r.comment_report.resolved ? ( <div>
<T i18nKey="resolved_by"> {r.comment_report.resolved ? (
# <T i18nKey="resolved_by">
<PersonListing person={r.resolver} /> #
</T> <PersonListing person={resolver} />
) : ( </T>
<T i18nKey="unresolved_by"> ) : (
# <T i18nKey="unresolved_by">
<PersonListing person={r.resolver} /> #
</T> <PersonListing person={resolver} />
)} </T>
</div> )}
)} </div>
),
none: <></>,
})}
<button <button
className="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleResolveReport)} onClick={linkEvent(this, this.handleResolveReport)}
@ -98,11 +103,11 @@ export class CommentReport extends Component<CommentReportProps, any> {
} }
handleResolveReport(i: CommentReport) { handleResolveReport(i: CommentReport) {
let form: ResolveCommentReport = { let form = new ResolveCommentReport({
report_id: i.props.report.comment_report.id, report_id: i.props.report.comment_report.id,
resolved: !i.props.report.comment_report.resolved, resolved: !i.props.report.comment_report.resolved,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.resolveCommentReport(form)); WebSocketService.Instance.send(wsClient.resolveCommentReport(form));
} }
} }

View file

@ -1,9 +1,10 @@
import { Option } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { PictrsImage } from "./pictrs-image"; import { PictrsImage } from "./pictrs-image";
interface BannerIconHeaderProps { interface BannerIconHeaderProps {
banner?: string; banner: Option<string>;
icon?: string; icon: Option<string>;
} }
export class BannerIconHeader extends Component<BannerIconHeaderProps, any> { export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
@ -14,17 +15,21 @@ export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
render() { render() {
return ( return (
<div class="position-relative mb-2"> <div class="position-relative mb-2">
{this.props.banner && ( {this.props.banner.match({
<PictrsImage src={this.props.banner} banner alt="" /> some: banner => <PictrsImage src={banner} banner alt="" />,
)} none: <></>,
{this.props.icon && ( })}
<PictrsImage {this.props.icon.match({
src={this.props.icon} some: icon => (
iconOverlay <PictrsImage
pushup={!!this.props.banner} src={icon}
alt="" iconOverlay
/> pushup={this.props.banner.isSome()}
)} alt=""
/>
),
none: <></>,
})}
</div> </div>
); );
} }

View file

@ -1,3 +1,4 @@
import { Option } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { Helmet } from "inferno-helmet"; import { Helmet } from "inferno-helmet";
import { httpExternalPath } from "../../env"; import { httpExternalPath } from "../../env";
@ -6,8 +7,8 @@ import { md } from "../../utils";
interface HtmlTagsProps { interface HtmlTagsProps {
title: string; title: string;
path: string; path: string;
description?: string; description: Option<string>;
image?: string; image: Option<string>;
} }
/// Taken from https://metatags.io/ /// Taken from https://metatags.io/
@ -31,14 +32,17 @@ export class HtmlTags extends Component<HtmlTagsProps, any> {
<meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:card" content="summary_large_image" />
{/* Optional desc and images */} {/* Optional desc and images */}
{this.props.description && {this.props.description.isSome() &&
["description", "og:description", "twitter:description"].map(n => ( ["description", "og:description", "twitter:description"].map(n => (
<meta name={n} content={md.renderInline(this.props.description)} /> <meta
name={n}
content={md.renderInline(this.props.description.unwrap())}
/>
))} ))}
{this.props.image && {this.props.image.isSome() &&
["og:image", "twitter:image"].map(p => ( ["og:image", "twitter:image"].map(p => (
<meta property={p} content={this.props.image} /> <meta property={p} content={this.props.image.unwrap()} />
))} ))}
</Helmet> </Helmet>
); );

View file

@ -1,3 +1,4 @@
import { Option } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { pictrsUri } from "../../env"; import { pictrsUri } from "../../env";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
@ -7,7 +8,7 @@ import { Icon } from "./icon";
interface ImageUploadFormProps { interface ImageUploadFormProps {
uploadTitle: string; uploadTitle: string;
imageSrc: string; imageSrc: Option<string>;
onUpload(url: string): any; onUpload(url: string): any;
onRemove(): any; onRemove(): any;
rounded?: boolean; rounded?: boolean;
@ -38,26 +39,29 @@ export class ImageUploadForm extends Component<
htmlFor={this.id} htmlFor={this.id}
class="pointer text-muted small font-weight-bold" class="pointer text-muted small font-weight-bold"
> >
{!this.props.imageSrc ? ( {this.props.imageSrc.match({
<span class="btn btn-secondary">{this.props.uploadTitle}</span> some: imageSrc => (
) : ( <span class="d-inline-block position-relative">
<span class="d-inline-block position-relative"> <img
<img src={imageSrc}
src={this.props.imageSrc} height={this.props.rounded ? 60 : ""}
height={this.props.rounded ? 60 : ""} width={this.props.rounded ? 60 : ""}
width={this.props.rounded ? 60 : ""} className={`img-fluid ${
className={`img-fluid ${ this.props.rounded ? "rounded-circle" : ""
this.props.rounded ? "rounded-circle" : "" }`}
}`} />
/> <a
<a onClick={linkEvent(this, this.handleRemoveImage)}
onClick={linkEvent(this, this.handleRemoveImage)} aria-label={i18n.t("remove")}
aria-label={i18n.t("remove")} >
> <Icon icon="x" classes="mini-overlay" />
<Icon icon="x" classes="mini-overlay" /> </a>
</a> </span>
</span> ),
)} none: (
<span class="btn btn-secondary">{this.props.uploadTitle}</span>
),
})}
</label> </label>
<input <input
id={this.id} id={this.id}
@ -65,7 +69,7 @@ export class ImageUploadForm extends Component<
accept="image/*,video/*" accept="image/*,video/*"
name={this.id} name={this.id}
class="d-none" class="d-none"
disabled={!UserService.Instance.myUserInfo} disabled={UserService.Instance.myUserInfo.isNone()}
onChange={linkEvent(this, this.handleImageUpload)} onChange={linkEvent(this, this.handleImageUpload)}
/> />
</form> </form>

View file

@ -46,11 +46,7 @@ export class ListingTypeSelect extends Component<
title={i18n.t("subscribed_description")} title={i18n.t("subscribed_description")}
className={`btn btn-outline-secondary className={`btn btn-outline-secondary
${this.state.type_ == ListingType.Subscribed && "active"} ${this.state.type_ == ListingType.Subscribed && "active"}
${ ${UserService.Instance.myUserInfo.isNone() ? "disabled" : "pointer"}
UserService.Instance.myUserInfo == undefined
? "disabled"
: "pointer"
}
`} `}
> >
<input <input
@ -59,7 +55,7 @@ export class ListingTypeSelect extends Component<
value={ListingType.Subscribed} value={ListingType.Subscribed}
checked={this.state.type_ == ListingType.Subscribed} checked={this.state.type_ == ListingType.Subscribed}
onChange={linkEvent(this, this.handleTypeChange)} onChange={linkEvent(this, this.handleTypeChange)}
disabled={UserService.Instance.myUserInfo == undefined} disabled={UserService.Instance.myUserInfo.isNone()}
/> />
{i18n.t("subscribed")} {i18n.t("subscribed")}
</label> </label>

View file

@ -1,6 +1,8 @@
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 { pictrsUri } from "../../env"; import { pictrsUri } from "../../env";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService } from "../../services"; import { UserService } from "../../services";
@ -18,22 +20,22 @@ import {
import { Icon, Spinner } from "./icon"; import { Icon, Spinner } from "./icon";
interface MarkdownTextAreaProps { interface MarkdownTextAreaProps {
initialContent?: string; initialContent: Option<string>;
finished?: boolean; placeholder: Option<string>;
buttonTitle?: string; buttonTitle: Option<string>;
maxLength: Option<number>;
replyType?: boolean; replyType?: boolean;
focus?: boolean; focus?: boolean;
disabled?: boolean; disabled?: boolean;
maxLength?: number; finished?: boolean;
onSubmit?(msg: { val: string; formId: string }): any; hideNavigationWarnings?: boolean;
onContentChange?(val: string): any; onContentChange?(val: string): any;
onReplyCancel?(): any; onReplyCancel?(): any;
hideNavigationWarnings?: boolean; onSubmit?(msg: { val: string; formId: string }): any;
placeholder?: string;
} }
interface MarkdownTextAreaState { interface MarkdownTextAreaState {
content: string; content: Option<string>;
previewMode: boolean; previewMode: boolean;
loading: boolean; loading: boolean;
imageLoading: boolean; imageLoading: boolean;
@ -68,7 +70,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 = textarea.value; this.state.content = Some(textarea.value);
this.setState(this.state); this.setState(this.state);
autosize.update(textarea); autosize.update(textarea);
}); });
@ -85,7 +87,7 @@ export class MarkdownTextArea extends Component<
} }
componentDidUpdate() { componentDidUpdate() {
if (!this.props.hideNavigationWarnings && this.state.content) { if (!this.props.hideNavigationWarnings && this.state.content.isSome()) {
window.onbeforeunload = () => true; window.onbeforeunload = () => true;
} else { } else {
window.onbeforeunload = undefined; window.onbeforeunload = undefined;
@ -96,7 +98,7 @@ export class MarkdownTextArea extends Component<
if (nextProps.finished) { if (nextProps.finished) {
this.state.previewMode = false; this.state.previewMode = false;
this.state.loading = false; this.state.loading = false;
this.state.content = ""; this.state.content = None;
this.setState(this.state); this.setState(this.state);
if (this.props.replyType) { if (this.props.replyType) {
this.props.onReplyCancel(); this.props.onReplyCancel();
@ -118,7 +120,9 @@ export class MarkdownTextArea extends Component<
return ( return (
<form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}> <form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}>
<Prompt <Prompt
when={!this.props.hideNavigationWarnings && this.state.content} when={
!this.props.hideNavigationWarnings && this.state.content.isSome()
}
message={i18n.t("block_leaving")} message={i18n.t("block_leaving")}
/> />
<div class="form-group row"> <div class="form-group row">
@ -126,21 +130,25 @@ export class MarkdownTextArea extends Component<
<textarea <textarea
id={this.id} id={this.id}
className={`form-control ${this.state.previewMode && "d-none"}`} className={`form-control ${this.state.previewMode && "d-none"}`}
value={this.state.content} value={toUndefined(this.state.content)}
onInput={linkEvent(this, this.handleContentChange)} onInput={linkEvent(this, this.handleContentChange)}
onPaste={linkEvent(this, this.handleImageUploadPaste)} onPaste={linkEvent(this, this.handleImageUploadPaste)}
required required
disabled={this.props.disabled} disabled={this.props.disabled}
rows={2} rows={2}
maxLength={this.props.maxLength || 10000} maxLength={this.props.maxLength.unwrapOr(10000)}
placeholder={this.props.placeholder} placeholder={toUndefined(this.props.placeholder)}
/> />
{this.state.previewMode && ( {this.state.previewMode &&
<div this.state.content.match({
className="card border-secondary card-body md-div" some: content => (
dangerouslySetInnerHTML={mdToHtml(this.state.content)} <div
/> className="card border-secondary card-body md-div"
)} dangerouslySetInnerHTML={mdToHtml(content)}
/>
),
none: <></>,
})}
</div> </div>
<label class="sr-only" htmlFor={this.id}> <label class="sr-only" htmlFor={this.id}>
{i18n.t("body")} {i18n.t("body")}
@ -148,19 +156,22 @@ export class MarkdownTextArea extends Component<
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-12 d-flex flex-wrap"> <div class="col-sm-12 d-flex flex-wrap">
{this.props.buttonTitle && ( {this.props.buttonTitle.match({
<button some: buttonTitle => (
type="submit" <button
class="btn btn-sm btn-secondary mr-2" type="submit"
disabled={this.props.disabled || this.state.loading} class="btn btn-sm btn-secondary mr-2"
> disabled={this.props.disabled || this.state.loading}
{this.state.loading ? ( >
<Spinner /> {this.state.loading ? (
) : ( <Spinner />
<span>{this.props.buttonTitle}</span> ) : (
)} <span>{buttonTitle}</span>
</button> )}
)} </button>
),
none: <></>,
})}
{this.props.replyType && ( {this.props.replyType && (
<button <button
type="button" type="button"
@ -170,7 +181,7 @@ export class MarkdownTextArea extends Component<
{i18n.t("cancel")} {i18n.t("cancel")}
</button> </button>
)} )}
{this.state.content && ( {this.state.content.isSome() && (
<button <button
className={`btn btn-sm btn-secondary mr-2 ${ className={`btn btn-sm btn-secondary mr-2 ${
this.state.previewMode && "active" this.state.previewMode && "active"
@ -210,7 +221,7 @@ export class MarkdownTextArea extends Component<
<label <label
htmlFor={`file-upload-${this.id}`} htmlFor={`file-upload-${this.id}`}
className={`mb-0 ${ className={`mb-0 ${
UserService.Instance.myUserInfo && "pointer" UserService.Instance.myUserInfo.isSome() && "pointer"
}`} }`}
data-tippy-content={i18n.t("upload_image")} data-tippy-content={i18n.t("upload_image")}
> >
@ -226,7 +237,7 @@ export class MarkdownTextArea extends Component<
accept="image/*,video/*" accept="image/*,video/*"
name="file" name="file"
class="d-none" class="d-none"
disabled={!UserService.Instance.myUserInfo} disabled={UserService.Instance.myUserInfo.isNone()}
onChange={linkEvent(this, this.handleImageUpload)} onChange={linkEvent(this, this.handleImageUpload)}
/> />
</form> </form>
@ -344,9 +355,12 @@ 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})`;
let content = i.state.content; i.state.content = Some(
content = content ? `${content}\n${imageMarkdown}` : imageMarkdown; i.state.content.match({
i.state.content = content; some: content => `${content}\n${imageMarkdown}`,
none: imageMarkdown,
})
);
i.state.imageLoading = false; i.state.imageLoading = false;
i.contentChange(); i.contentChange();
i.setState(i.state); i.setState(i.state);
@ -373,12 +387,12 @@ export class MarkdownTextArea extends Component<
contentChange() { contentChange() {
if (this.props.onContentChange) { if (this.props.onContentChange) {
this.props.onContentChange(this.state.content); this.props.onContentChange(toUndefined(this.state.content));
} }
} }
handleContentChange(i: MarkdownTextArea, event: any) { handleContentChange(i: MarkdownTextArea, event: any) {
i.state.content = event.target.value; i.state.content = Some(event.target.value);
i.contentChange(); i.contentChange();
i.setState(i.state); i.setState(i.state);
} }
@ -393,7 +407,7 @@ export class MarkdownTextArea extends Component<
event.preventDefault(); event.preventDefault();
i.state.loading = true; i.state.loading = true;
i.setState(i.state); i.setState(i.state);
let msg = { val: i.state.content, formId: i.formId }; let msg = { val: toUndefined(i.state.content), formId: i.formId };
i.props.onSubmit(msg); i.props.onSubmit(msg);
} }
@ -403,23 +417,28 @@ export class MarkdownTextArea extends Component<
handleInsertLink(i: MarkdownTextArea, event: any) { handleInsertLink(i: MarkdownTextArea, event: any) {
event.preventDefault(); event.preventDefault();
if (!i.state.content) {
i.state.content = "";
}
let textarea: any = document.getElementById(i.id); let textarea: any = document.getElementById(i.id);
let start: number = textarea.selectionStart; let start: number = textarea.selectionStart;
let end: number = textarea.selectionEnd; let end: number = textarea.selectionEnd;
if (i.state.content.isNone()) {
i.state.content = Some("");
}
let content = i.state.content.unwrap();
if (start !== end) { if (start !== end) {
let selectedText = i.state.content.substring(start, end); let selectedText = content.substring(start, end);
i.state.content = `${i.state.content.substring( i.state.content = Some(
0, `${content.substring(0, start)}[${selectedText}]()${content.substring(
start end
)}[${selectedText}]()${i.state.content.substring(end)}`; )}`
);
textarea.focus(); textarea.focus();
setTimeout(() => (textarea.selectionEnd = end + 3), 10); setTimeout(() => (textarea.selectionEnd = end + 3), 10);
} else { } else {
i.state.content += "[]()"; i.state.content = Some(`${content} []()`);
textarea.focus(); textarea.focus();
setTimeout(() => (textarea.selectionEnd -= 1), 10); setTimeout(() => (textarea.selectionEnd -= 1), 10);
} }
@ -432,7 +451,7 @@ export class MarkdownTextArea extends Component<
} }
simpleBeginningofLine(chars: string) { simpleBeginningofLine(chars: string) {
this.simpleSurroundBeforeAfter(`${chars} `, "", ""); this.simpleSurroundBeforeAfter(`${chars}`, "", "");
} }
simpleSurroundBeforeAfter( simpleSurroundBeforeAfter(
@ -440,23 +459,27 @@ export class MarkdownTextArea extends Component<
afterChars: string, afterChars: string,
emptyChars = "___" emptyChars = "___"
) { ) {
if (!this.state.content) { if (this.state.content.isNone()) {
this.state.content = ""; this.state.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;
let end: number = textarea.selectionEnd; let end: number = textarea.selectionEnd;
let content = this.state.content.unwrap();
if (start !== end) { if (start !== end) {
let selectedText = this.state.content.substring(start, end); let selectedText = content.substring(start, end);
this.state.content = `${this.state.content.substring( this.state.content = Some(
0, `${content.substring(
start 0,
)}${beforeChars}${selectedText}${afterChars}${this.state.content.substring( start
end )}${beforeChars}${selectedText}${afterChars}${content.substring(end)}`
)}`; );
} else { } else {
this.state.content += `${beforeChars}${emptyChars}${afterChars}`; this.state.content = Some(
`${content}${beforeChars}${emptyChars}${afterChars}`
);
} }
this.contentChange(); this.contentChange();
this.setState(this.state); this.setState(this.state);
@ -530,10 +553,10 @@ export class MarkdownTextArea extends Component<
} }
simpleInsert(chars: string) { simpleInsert(chars: string) {
if (!this.state.content) { if (this.state.content.isNone()) {
this.state.content = `${chars} `; this.state.content = Some(`${chars} `);
} else { } else {
this.state.content += `\n${chars} `; this.state.content = Some(`${this.state.content.unwrap()}\n${chars} `);
} }
let textarea: any = document.getElementById(this.id); let textarea: any = document.getElementById(this.id);
@ -561,12 +584,12 @@ export class MarkdownTextArea extends Component<
.split("\n") .split("\n")
.map(t => `> ${t}`) .map(t => `> ${t}`)
.join("\n") + "\n\n"; .join("\n") + "\n\n";
if (this.state.content == null) { if (this.state.content.isNone()) {
this.state.content = ""; this.state.content = Some("");
} else { } else {
this.state.content += "\n"; this.state.content = Some(`${this.state.content.unwrap()}\n`);
} }
this.state.content += quotedText; this.state.content = Some(`${this.state.content.unwrap()}${quotedText}`);
this.contentChange(); this.contentChange();
this.setState(this.state); this.setState(this.state);
// Not sure why this needs a delay // Not sure why this needs a delay
@ -578,6 +601,8 @@ export class MarkdownTextArea extends Component<
let textarea: any = document.getElementById(this.id); let textarea: any = document.getElementById(this.id);
let start: number = textarea.selectionStart; let start: number = textarea.selectionStart;
let end: number = textarea.selectionEnd; let end: number = textarea.selectionEnd;
return start !== end ? this.state.content.substring(start, end) : ""; return start !== end
? this.state.content.unwrap().substring(start, end)
: "";
} }
} }

View file

@ -1,3 +1,4 @@
import { Option } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import moment from "moment"; import moment from "moment";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
@ -5,11 +6,8 @@ import { capitalizeFirstLetter, getLanguages } from "../../utils";
import { Icon } from "./icon"; import { Icon } from "./icon";
interface MomentTimeProps { interface MomentTimeProps {
data: { published: string;
published?: string; updated: Option<string>;
when_?: string;
updated?: string;
};
showAgo?: boolean; showAgo?: boolean;
ignoreUpdated?: boolean; ignoreUpdated?: boolean;
} }
@ -24,40 +22,32 @@ export class MomentTime extends Component<MomentTimeProps, any> {
} }
createdAndModifiedTimes() { createdAndModifiedTimes() {
let created = this.props.data.published || this.props.data.when_; return `${capitalizeFirstLetter(i18n.t("created"))}: ${this.format(
return ` this.props.published
<div> )}\n\n\n${
<div> this.props.updated.isSome() && capitalizeFirstLetter(i18n.t("modified"))
${capitalizeFirstLetter(i18n.t("created"))}: ${this.format(created)} } ${this.format(this.props.updated.unwrap())}`;
</div>
<div>
${capitalizeFirstLetter(i18n.t("modified"))} ${this.format(
this.props.data.updated
)}
</div>
</div>`;
} }
render() { render() {
if (!this.props.ignoreUpdated && this.props.data.updated) { if (!this.props.ignoreUpdated && this.props.updated.isSome()) {
return ( return (
<span <span
data-tippy-content={this.createdAndModifiedTimes()} data-tippy-content={this.createdAndModifiedTimes()}
data-tippy-allowHtml={true}
className="font-italics pointer unselectable" className="font-italics pointer unselectable"
> >
<Icon icon="edit-2" classes="icon-inline mr-1" /> <Icon icon="edit-2" classes="icon-inline mr-1" />
{moment.utc(this.props.data.updated).fromNow(!this.props.showAgo)} {moment.utc(this.props.updated.unwrap()).fromNow(!this.props.showAgo)}
</span> </span>
); );
} else { } else {
let created = this.props.data.published || this.props.data.when_; let published = this.props.published;
return ( return (
<span <span
className="pointer unselectable" className="pointer unselectable"
data-tippy-content={this.format(created)} data-tippy-content={this.format(published)}
> >
{moment.utc(created).fromNow(!this.props.showAgo)} {moment.utc(published).fromNow(!this.props.showAgo)}
</span> </span>
); );
} }

View file

@ -1,3 +1,4 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { import {
@ -6,7 +7,7 @@ import {
} from "lemmy-js-client"; } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { authField, mdToHtml, wsClient } from "../../utils"; import { auth, mdToHtml, wsClient } from "../../utils";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
import { MarkdownTextArea } from "./markdown-textarea"; import { MarkdownTextArea } from "./markdown-textarea";
import { MomentTime } from "./moment-time"; import { MomentTime } from "./moment-time";
@ -16,7 +17,7 @@ interface RegistrationApplicationProps {
} }
interface RegistrationApplicationState { interface RegistrationApplicationState {
denyReason?: string; denyReason: Option<string>;
denyExpanded: boolean; denyExpanded: boolean;
} }
@ -47,35 +48,44 @@ export class RegistrationApplication extends Component<
{i18n.t("applicant")}: <PersonListing person={a.creator} /> {i18n.t("applicant")}: <PersonListing person={a.creator} />
</div> </div>
<div> <div>
{i18n.t("created")}: <MomentTime showAgo data={ra} /> {i18n.t("created")}:{" "}
<MomentTime showAgo published={ra.published} updated={None} />
</div> </div>
<div>{i18n.t("answer")}:</div> <div>{i18n.t("answer")}:</div>
<div className="md-div" dangerouslySetInnerHTML={mdToHtml(ra.answer)} /> <div className="md-div" dangerouslySetInnerHTML={mdToHtml(ra.answer)} />
{a.admin && ( {a.admin.match({
<div> some: admin => (
{accepted ? ( <div>
<T i18nKey="approved_by"> {accepted ? (
# <T i18nKey="approved_by">
<PersonListing person={a.admin} />
</T>
) : (
<div>
<T i18nKey="denied_by">
# #
<PersonListing person={a.admin} /> <PersonListing person={admin} />
</T> </T>
) : (
<div> <div>
{i18n.t("deny_reason")}:{" "} <T i18nKey="denied_by">
<div #
className="md-div d-inline-flex" <PersonListing person={admin} />
dangerouslySetInnerHTML={mdToHtml(ra.deny_reason || "")} </T>
/> {ra.deny_reason.match({
some: deny_reason => (
<div>
{i18n.t("deny_reason")}:{" "}
<div
className="md-div d-inline-flex"
dangerouslySetInnerHTML={mdToHtml(deny_reason)}
/>
</div>
),
none: <></>,
})}
</div> </div>
</div> )}
)} </div>
</div> ),
)} none: <></>,
})}
{this.state.denyExpanded && ( {this.state.denyExpanded && (
<div class="form-group row"> <div class="form-group row">
@ -86,6 +96,9 @@ export class RegistrationApplication extends Component<
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.denyReason} initialContent={this.state.denyReason}
onContentChange={this.handleDenyReasonChange} onContentChange={this.handleDenyReasonChange}
placeholder={None}
buttonTitle={None}
maxLength={None}
hideNavigationWarnings hideNavigationWarnings
/> />
</div> </div>
@ -115,12 +128,12 @@ export class RegistrationApplication extends Component<
handleApprove(i: RegistrationApplication) { handleApprove(i: RegistrationApplication) {
i.setState({ denyExpanded: false }); i.setState({ denyExpanded: false });
let form: ApproveRegistrationApplication = { let form = new ApproveRegistrationApplication({
id: i.props.application.registration_application.id, id: i.props.application.registration_application.id,
deny_reason: "", deny_reason: None,
approve: true, approve: true,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.approveRegistrationApplication(form) wsClient.approveRegistrationApplication(form)
); );
@ -129,12 +142,12 @@ export class RegistrationApplication extends Component<
handleDeny(i: RegistrationApplication) { handleDeny(i: RegistrationApplication) {
if (i.state.denyExpanded) { if (i.state.denyExpanded) {
i.setState({ denyExpanded: false }); i.setState({ denyExpanded: false });
let form: ApproveRegistrationApplication = { let form = new ApproveRegistrationApplication({
id: i.props.application.registration_application.id, id: i.props.application.registration_application.id,
approve: false, approve: false,
deny_reason: i.state.denyReason, deny_reason: i.state.denyReason,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.approveRegistrationApplication(form) wsClient.approveRegistrationApplication(form)
); );
@ -144,7 +157,7 @@ export class RegistrationApplication extends Component<
} }
handleDenyReasonChange(val: string) { handleDenyReasonChange(val: string) {
this.state.denyReason = val; this.state.denyReason = Some(val);
this.setState(this.state); this.setState(this.state);
} }
} }

View file

@ -1,33 +1,32 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
CommunityResponse, CommunityResponse,
CommunityView,
FollowCommunity, FollowCommunity,
GetSiteResponse,
ListCommunities, ListCommunities,
ListCommunitiesResponse, ListCommunitiesResponse,
ListingType, ListingType,
SiteView,
SortType, SortType,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { InitialFetchRequest } from "shared/interfaces"; import { InitialFetchRequest } from "shared/interfaces";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { import {
authField, auth,
getListingTypeFromPropsNoDefault, getListingTypeFromPropsNoDefault,
getPageFromProps, getPageFromProps,
isBrowser, isBrowser,
numToSI, numToSI,
setIsoData, setIsoData,
setOptionalAuth,
showLocal, showLocal,
toast, toast,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
@ -38,10 +37,10 @@ import { CommunityLink } from "./community-link";
const communityLimit = 100; const communityLimit = 100;
interface CommunitiesState { interface CommunitiesState {
communities: CommunityView[]; listCommunitiesResponse: Option<ListCommunitiesResponse>;
page: number; page: number;
loading: boolean; loading: boolean;
site_view: SiteView; siteRes: GetSiteResponse;
searchText: string; searchText: string;
listingType: ListingType; listingType: ListingType;
} }
@ -53,13 +52,13 @@ interface CommunitiesProps {
export class Communities extends Component<any, CommunitiesState> { export class Communities extends Component<any, CommunitiesState> {
private subscription: Subscription; private subscription: Subscription;
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context, ListCommunitiesResponse);
private emptyState: CommunitiesState = { private emptyState: CommunitiesState = {
communities: [], listCommunitiesResponse: None,
loading: true, loading: true,
page: getPageFromProps(this.props), page: getPageFromProps(this.props),
listingType: getListingTypeFromPropsNoDefault(this.props), listingType: getListingTypeFromPropsNoDefault(this.props),
site_view: this.isoData.site_res.site_view, siteRes: this.isoData.site_res,
searchText: "", searchText: "",
}; };
@ -74,7 +73,8 @@ 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) {
this.state.communities = this.isoData.routeData[0].communities; let listRes = Some(this.isoData.routeData[0] as ListCommunitiesResponse);
this.state.listCommunitiesResponse = listRes;
this.state.loading = false; this.state.loading = false;
} else { } else {
this.refetch(); this.refetch();
@ -105,7 +105,10 @@ export class Communities extends Component<any, CommunitiesState> {
} }
get documentTitle(): string { get documentTitle(): string {
return `${i18n.t("communities")} - ${this.state.site_view.site.name}`; return this.state.siteRes.site_view.match({
some: siteView => `${i18n.t("communities")} - ${siteView.site.name}`,
none: "",
});
} }
render() { render() {
@ -114,6 +117,8 @@ export class Communities extends Component<any, CommunitiesState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
@ -157,48 +162,51 @@ export class Communities extends Component<any, CommunitiesState> {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{this.state.communities.map(cv => ( {this.state.listCommunitiesResponse
<tr> .map(l => l.communities)
<td> .unwrapOr([])
<CommunityLink community={cv.community} /> .map(cv => (
</td> <tr>
<td class="text-right"> <td>
{numToSI(cv.counts.subscribers)} <CommunityLink community={cv.community} />
</td> </td>
<td class="text-right"> <td class="text-right">
{numToSI(cv.counts.users_active_month)} {numToSI(cv.counts.subscribers)}
</td> </td>
<td class="text-right d-none d-lg-table-cell"> <td class="text-right">
{numToSI(cv.counts.posts)} {numToSI(cv.counts.users_active_month)}
</td> </td>
<td class="text-right d-none d-lg-table-cell"> <td class="text-right d-none d-lg-table-cell">
{numToSI(cv.counts.comments)} {numToSI(cv.counts.posts)}
</td> </td>
<td class="text-right"> <td class="text-right d-none d-lg-table-cell">
{cv.subscribed ? ( {numToSI(cv.counts.comments)}
<button </td>
class="btn btn-link d-inline-block" <td class="text-right">
onClick={linkEvent( {cv.subscribed ? (
cv.community.id, <button
this.handleUnsubscribe class="btn btn-link d-inline-block"
)} onClick={linkEvent(
> cv.community.id,
{i18n.t("unsubscribe")} this.handleUnsubscribe
</button> )}
) : ( >
<button {i18n.t("unsubscribe")}
class="btn btn-link d-inline-block" </button>
onClick={linkEvent( ) : (
cv.community.id, <button
this.handleSubscribe class="btn btn-link d-inline-block"
)} onClick={linkEvent(
> cv.community.id,
{i18n.t("subscribe")} this.handleSubscribe
</button> )}
)} >
</td> {i18n.t("subscribe")}
</tr> </button>
))} )}
</td>
</tr>
))}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -258,20 +266,20 @@ export class Communities extends Component<any, CommunitiesState> {
} }
handleUnsubscribe(communityId: number) { handleUnsubscribe(communityId: number) {
let form: FollowCommunity = { let form = new FollowCommunity({
community_id: communityId, community_id: communityId,
follow: false, follow: false,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.followCommunity(form)); WebSocketService.Instance.send(wsClient.followCommunity(form));
} }
handleSubscribe(communityId: number) { handleSubscribe(communityId: number) {
let form: FollowCommunity = { let form = new FollowCommunity({
community_id: communityId, community_id: communityId,
follow: true, follow: true,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.followCommunity(form)); WebSocketService.Instance.send(wsClient.followCommunity(form));
} }
@ -287,13 +295,13 @@ export class Communities extends Component<any, CommunitiesState> {
} }
refetch() { refetch() {
let listCommunitiesForm: ListCommunities = { let listCommunitiesForm = new ListCommunities({
type_: this.state.listingType, type_: Some(this.state.listingType),
sort: SortType.TopMonth, sort: Some(SortType.TopMonth),
limit: communityLimit, limit: Some(communityLimit),
page: this.state.page, page: Some(this.state.page),
auth: authField(false), auth: auth(false).ok(),
}; });
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.listCommunities(listCommunitiesForm) wsClient.listCommunities(listCommunitiesForm)
@ -302,17 +310,17 @@ export class Communities extends Component<any, CommunitiesState> {
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let pathSplit = req.path.split("/"); let pathSplit = req.path.split("/");
let type_: ListingType = pathSplit[3] let type_: Option<ListingType> = Some(
? ListingType[pathSplit[3]] pathSplit[3] ? ListingType[pathSplit[3]] : ListingType.Local
: ListingType.Local; );
let page = pathSplit[5] ? Number(pathSplit[5]) : 1; let page = Some(pathSplit[5] ? Number(pathSplit[5]) : 1);
let listCommunitiesForm: ListCommunities = { let listCommunitiesForm = new ListCommunities({
type_, type_,
sort: SortType.TopMonth, sort: Some(SortType.TopMonth),
limit: communityLimit, limit: Some(communityLimit),
page, page,
}; auth: req.auth,
setOptionalAuth(listCommunitiesForm, req.auth); });
return [req.client.listCommunities(listCommunitiesForm)]; return [req.client.listCommunities(listCommunitiesForm)];
} }
@ -324,18 +332,26 @@ export class Communities extends Component<any, CommunitiesState> {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
return; return;
} else if (op == UserOperation.ListCommunities) { } else if (op == UserOperation.ListCommunities) {
let data = wsJsonToRes<ListCommunitiesResponse>(msg).data; let data = wsJsonToRes<ListCommunitiesResponse>(
this.state.communities = data.communities; msg,
ListCommunitiesResponse
);
this.state.listCommunitiesResponse = Some(data);
this.state.loading = false; this.state.loading = false;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.FollowCommunity) { } else if (op == UserOperation.FollowCommunity) {
let data = wsJsonToRes<CommunityResponse>(msg).data; let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
let found = this.state.communities.find( this.state.listCommunitiesResponse.match({
c => c.community.id == data.community_view.community.id some: res => {
); let found = res.communities.find(
found.subscribed = data.community_view.subscribed; c => c.community.id == data.community_view.community.id
found.counts.subscribers = data.community_view.counts.subscribers; );
found.subscribed = data.community_view.subscribed;
found.counts.subscribers = data.community_view.counts.subscribers;
},
none: void 0,
});
this.setState(this.state); this.setState(this.state);
} }
} }

View file

@ -1,3 +1,4 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Prompt } from "inferno-router"; import { Prompt } from "inferno-router";
import { import {
@ -5,30 +6,31 @@ import {
CommunityView, CommunityView,
CreateCommunity, CreateCommunity,
EditCommunity, EditCommunity,
toUndefined,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, auth,
capitalizeFirstLetter, capitalizeFirstLetter,
randomStr, randomStr,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
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 { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
interface CommunityFormProps { interface CommunityFormProps {
community_view?: CommunityView; // If a community is given, that means this is an edit community_view: Option<CommunityView>; // If a community is given, that means this is an edit
onCancel?(): any; onCancel?(): any;
onCreate?(community: CommunityView): any; onCreate?(community: CommunityView): any;
onEdit?(community: CommunityView): any; onEdit?(community: CommunityView): any;
enableNsfw: boolean; enableNsfw?: boolean;
} }
interface CommunityFormState { interface CommunityFormState {
@ -44,15 +46,16 @@ export class CommunityForm extends Component<
private subscription: Subscription; private subscription: Subscription;
private emptyState: CommunityFormState = { private emptyState: CommunityFormState = {
communityForm: { communityForm: new CreateCommunity({
name: null, name: undefined,
title: null, title: undefined,
nsfw: false, description: None,
icon: null, nsfw: None,
banner: null, icon: None,
posting_restricted_to_mods: false, banner: None,
auth: authField(false), posting_restricted_to_mods: None,
}, auth: undefined,
}),
loading: false, loading: false,
}; };
@ -70,31 +73,34 @@ 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);
let cv = this.props.community_view; this.props.community_view.match({
if (cv) { some: cv => {
this.state.communityForm = { 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,
nsfw: cv.community.nsfw, nsfw: Some(cv.community.nsfw),
icon: cv.community.icon, icon: cv.community.icon,
banner: cv.community.banner, banner: cv.community.banner,
posting_restricted_to_mods: cv.community.posting_restricted_to_mods, posting_restricted_to_mods: Some(
auth: authField(), cv.community.posting_restricted_to_mods
}; ),
} auth: auth().unwrap(),
});
},
none: void 0,
});
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
} }
// TODO this should be checked out
componentDidUpdate() { componentDidUpdate() {
if ( if (
!this.state.loading && !this.state.loading &&
(this.state.communityForm.name || (this.state.communityForm.name ||
this.state.communityForm.title || this.state.communityForm.title ||
this.state.communityForm.description) this.state.communityForm.description.isSome())
) { ) {
window.onbeforeunload = () => true; window.onbeforeunload = () => true;
} else { } else {
@ -115,12 +121,12 @@ export class CommunityForm extends Component<
!this.state.loading && !this.state.loading &&
(this.state.communityForm.name || (this.state.communityForm.name ||
this.state.communityForm.title || this.state.communityForm.title ||
this.state.communityForm.description) this.state.communityForm.description.isSome())
} }
message={i18n.t("block_leaving")} message={i18n.t("block_leaving")}
/> />
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}> <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
{!this.props.community_view && ( {this.props.community_view.isNone() && (
<div class="form-group row"> <div class="form-group row">
<label <label
class="col-12 col-sm-2 col-form-label" class="col-12 col-sm-2 col-form-label"
@ -205,6 +211,9 @@ export class CommunityForm extends Component<
<div class="col-12 col-sm-10"> <div class="col-12 col-sm-10">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.communityForm.description} initialContent={this.state.communityForm.description}
placeholder={Some("description")}
buttonTitle={None}
maxLength={None}
onContentChange={this.handleCommunityDescriptionChange} onContentChange={this.handleCommunityDescriptionChange}
/> />
</div> </div>
@ -221,7 +230,7 @@ export class CommunityForm extends Component<
class="form-check-input position-static" class="form-check-input position-static"
id="community-nsfw" id="community-nsfw"
type="checkbox" type="checkbox"
checked={this.state.communityForm.nsfw} checked={toUndefined(this.state.communityForm.nsfw)}
onChange={linkEvent(this, this.handleCommunityNsfwChange)} onChange={linkEvent(this, this.handleCommunityNsfwChange)}
/> />
</div> </div>
@ -238,7 +247,9 @@ export class CommunityForm extends Component<
class="form-check-input position-static" class="form-check-input position-static"
id="community-only-mods-can-post" id="community-only-mods-can-post"
type="checkbox" type="checkbox"
checked={this.state.communityForm.posting_restricted_to_mods} checked={toUndefined(
this.state.communityForm.posting_restricted_to_mods
)}
onChange={linkEvent( onChange={linkEvent(
this, this,
this.handleCommunityPostingRestrictedToMods this.handleCommunityPostingRestrictedToMods
@ -256,13 +267,13 @@ export class CommunityForm extends Component<
> >
{this.state.loading ? ( {this.state.loading ? (
<Spinner /> <Spinner />
) : this.props.community_view ? ( ) : this.props.community_view.isSome() ? (
capitalizeFirstLetter(i18n.t("save")) capitalizeFirstLetter(i18n.t("save"))
) : ( ) : (
capitalizeFirstLetter(i18n.t("create")) capitalizeFirstLetter(i18n.t("create"))
)} )}
</button> </button>
{this.props.community_view && ( {this.props.community_view.isSome() && (
<button <button
type="button" type="button"
class="btn btn-secondary" class="btn btn-secondary"
@ -281,17 +292,29 @@ export class CommunityForm extends Component<
handleCreateCommunitySubmit(i: CommunityForm, event: any) { handleCreateCommunitySubmit(i: CommunityForm, event: any) {
event.preventDefault(); event.preventDefault();
i.state.loading = true; i.state.loading = true;
if (i.props.community_view) { let cForm = i.state.communityForm;
let form: EditCommunity = {
...i.state.communityForm, i.props.community_view.match({
community_id: i.props.community_view.community.id, some: cv => {
}; let form = new EditCommunity({
WebSocketService.Instance.send(wsClient.editCommunity(form)); community_id: cv.community.id,
} else { title: Some(cForm.title),
WebSocketService.Instance.send( description: cForm.description,
wsClient.createCommunity(i.state.communityForm) icon: cForm.icon,
); banner: cForm.banner,
} nsfw: cForm.nsfw,
posting_restricted_to_mods: cForm.posting_restricted_to_mods,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.editCommunity(form));
},
none: () => {
WebSocketService.Instance.send(
wsClient.createCommunity(i.state.communityForm)
);
},
});
i.setState(i.state); i.setState(i.state);
} }
@ -306,7 +329,7 @@ export class CommunityForm extends Component<
} }
handleCommunityDescriptionChange(val: string) { handleCommunityDescriptionChange(val: string) {
this.state.communityForm.description = val; this.state.communityForm.description = Some(val);
this.setState(this.state); this.setState(this.state);
} }
@ -325,22 +348,22 @@ export class CommunityForm extends Component<
} }
handleIconUpload(url: string) { handleIconUpload(url: string) {
this.state.communityForm.icon = url; this.state.communityForm.icon = Some(url);
this.setState(this.state); this.setState(this.state);
} }
handleIconRemove() { handleIconRemove() {
this.state.communityForm.icon = ""; this.state.communityForm.icon = Some("");
this.setState(this.state); this.setState(this.state);
} }
handleBannerUpload(url: string) { handleBannerUpload(url: string) {
this.state.communityForm.banner = url; this.state.communityForm.banner = Some(url);
this.setState(this.state); this.setState(this.state);
} }
handleBannerRemove() { handleBannerRemove() {
this.state.communityForm.banner = ""; this.state.communityForm.banner = Some("");
this.setState(this.state); this.setState(this.state);
} }
@ -354,42 +377,51 @@ export class CommunityForm extends Component<
this.setState(this.state); this.setState(this.state);
return; return;
} else if (op == UserOperation.CreateCommunity) { } else if (op == UserOperation.CreateCommunity) {
let data = wsJsonToRes<CommunityResponse>(msg).data; let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
this.state.loading = false; this.state.loading = false;
this.props.onCreate(data.community_view); this.props.onCreate(data.community_view);
// Update myUserInfo // Update myUserInfo
let community = data.community_view.community; let community = data.community_view.community;
let person = UserService.Instance.myUserInfo.local_user_view.person;
UserService.Instance.myUserInfo.follows.push({ UserService.Instance.myUserInfo.match({
community, some: mui => {
follower: person, let person = mui.local_user_view.person;
}); mui.follows.push({
UserService.Instance.myUserInfo.moderates.push({ community,
community, follower: person,
moderator: person, });
mui.moderates.push({
community,
moderator: person,
});
},
none: void 0,
}); });
} else if (op == UserOperation.EditCommunity) { } else if (op == UserOperation.EditCommunity) {
let data = wsJsonToRes<CommunityResponse>(msg).data; let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
this.state.loading = false; this.state.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;
let followFound = UserService.Instance.myUserInfo.follows.findIndex( UserService.Instance.myUserInfo.match({
f => f.community.id == community.id some: mui => {
); let followFound = mui.follows.findIndex(
if (followFound) { f => f.community.id == community.id
UserService.Instance.myUserInfo.follows[followFound].community = );
community; if (followFound) {
} mui.follows[followFound].community = community;
}
let moderatesFound = UserService.Instance.myUserInfo.moderates.findIndex( let moderatesFound = mui.moderates.findIndex(
f => f.community.id == community.id f => f.community.id == community.id
); );
if (moderatesFound) { if (moderatesFound) {
UserService.Instance.myUserInfo.moderates[moderatesFound].community = mui.moderates[moderatesFound].community = community;
community; }
} },
none: void 0,
});
} }
} }
} }

View file

@ -56,12 +56,14 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
} }
avatarAndName(displayName: string) { avatarAndName(displayName: string) {
let community = this.props.community;
return ( return (
<> <>
{!this.props.hideAvatar && community.icon && showAvatars() && ( {!this.props.hideAvatar &&
<PictrsImage src={community.icon} icon /> showAvatars() &&
)} this.props.community.icon.match({
some: icon => <PictrsImage src={icon} icon />,
none: <></>,
})}
<span class="overflow-wrap-anywhere">{displayName}</span> <span class="overflow-wrap-anywhere">{displayName}</span>
</> </>
); );

View file

@ -1,3 +1,4 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
AddModToCommunityResponse, AddModToCommunityResponse,
@ -19,20 +20,25 @@ import {
PostResponse, PostResponse,
PostView, PostView,
SortType, SortType,
toOption,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { DataType, InitialFetchRequest } from "../../interfaces"; import { DataType, InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, auth,
commentsToFlatNodes, commentsToFlatNodes,
communityRSSUrl, communityRSSUrl,
createCommentLikeRes, createCommentLikeRes,
createPostLikeFindRes, createPostLikeFindRes,
editCommentRes, editCommentRes,
editPostFindRes, editPostFindRes,
enableDownvotes,
enableNsfw,
fetchLimit, fetchLimit,
getDataTypeFromProps, getDataTypeFromProps,
getPageFromProps, getPageFromProps,
@ -43,15 +49,12 @@ import {
saveCommentRes, saveCommentRes,
saveScrollPosition, saveScrollPosition,
setIsoData, setIsoData,
setOptionalAuth,
setupTippy, setupTippy,
showLocal, showLocal,
toast, toast,
updatePersonBlock, updatePersonBlock,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { CommentNodes } from "../comment/comment-nodes"; import { CommentNodes } from "../comment/comment-nodes";
import { BannerIconHeader } from "../common/banner-icon-header"; import { BannerIconHeader } from "../common/banner-icon-header";
@ -66,7 +69,7 @@ import { PostListings } from "../post/post-listings";
import { CommunityLink } from "./community-link"; import { CommunityLink } from "./community-link";
interface State { interface State {
communityRes: GetCommunityResponse; communityRes: Option<GetCommunityResponse>;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
communityName: string; communityName: string;
communityLoading: boolean; communityLoading: boolean;
@ -93,10 +96,15 @@ interface UrlParams {
} }
export class Community extends Component<any, State> { export class Community extends Component<any, State> {
private isoData = setIsoData(this.context); private isoData = setIsoData(
this.context,
GetCommunityResponse,
GetPostsResponse,
GetCommentsResponse
);
private subscription: Subscription; private subscription: Subscription;
private emptyState: State = { private emptyState: State = {
communityRes: undefined, communityRes: None,
communityName: this.props.match.params.name, communityName: this.props.match.params.name,
communityLoading: true, communityLoading: true,
postsLoading: true, postsLoading: true,
@ -123,12 +131,21 @@ 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 = this.isoData.routeData[0]; this.state.communityRes = Some(
if (this.state.dataType == DataType.Post) { this.isoData.routeData[0] as GetCommunityResponse
this.state.posts = this.isoData.routeData[1].posts; );
} else { let postsRes = Some(this.isoData.routeData[1] as GetPostsResponse);
this.state.comments = this.isoData.routeData[1].comments; let commentsRes = Some(this.isoData.routeData[2] as GetCommentsResponse);
}
postsRes.match({
some: pvs => (this.state.posts = pvs.posts),
none: void 0,
});
commentsRes.match({
some: cvs => (this.state.comments = cvs.comments),
none: void 0,
});
this.state.communityLoading = false; this.state.communityLoading = false;
this.state.postsLoading = false; this.state.postsLoading = false;
this.state.commentsLoading = false; this.state.commentsLoading = false;
@ -139,10 +156,11 @@ export class Community extends Component<any, State> {
} }
fetchCommunity() { fetchCommunity() {
let form: GetCommunity = { let form = new GetCommunity({
name: this.state.communityName ? this.state.communityName : null, name: Some(this.state.communityName),
auth: authField(false), id: None,
}; auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.getCommunity(form)); WebSocketService.Instance.send(wsClient.getCommunity(form));
} }
@ -169,56 +187,62 @@ export class Community extends Component<any, State> {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let communityName = pathSplit[2]; let communityName = pathSplit[2];
let communityForm: GetCommunity = { name: communityName }; let communityForm = new GetCommunity({
setOptionalAuth(communityForm, req.auth); name: Some(communityName),
id: None,
auth: req.auth,
});
promises.push(req.client.getCommunity(communityForm)); promises.push(req.client.getCommunity(communityForm));
let dataType: DataType = pathSplit[4] let dataType: DataType = pathSplit[4]
? DataType[pathSplit[4]] ? DataType[pathSplit[4]]
: DataType.Post; : DataType.Post;
let sort: SortType = pathSplit[6] let sort: Option<SortType> = toOption(
? SortType[pathSplit[6]] pathSplit[6]
: UserService.Instance.myUserInfo ? SortType[pathSplit[6]]
? Object.values(SortType)[ : UserService.Instance.myUserInfo.match({
UserService.Instance.myUserInfo.local_user_view.local_user some: mui =>
.default_sort_type Object.values(SortType)[
] mui.local_user_view.local_user.default_sort_type
: SortType.Active; ],
none: SortType.Active,
})
);
let page = pathSplit[8] ? Number(pathSplit[8]) : 1; let page = toOption(pathSplit[8] ? Number(pathSplit[8]) : 1);
if (dataType == DataType.Post) { if (dataType == DataType.Post) {
let getPostsForm: GetPosts = { let getPostsForm = new GetPosts({
community_name: Some(communityName),
community_id: None,
page, page,
limit: fetchLimit, limit: Some(fetchLimit),
sort, sort,
type_: ListingType.Community, type_: Some(ListingType.Community),
saved_only: false, saved_only: Some(false),
}; auth: req.auth,
setOptionalAuth(getPostsForm, req.auth); });
this.setName(getPostsForm, communityName);
promises.push(req.client.getPosts(getPostsForm)); promises.push(req.client.getPosts(getPostsForm));
promises.push(Promise.resolve());
} else { } else {
let getCommentsForm: GetComments = { let getCommentsForm = new GetComments({
community_name: Some(communityName),
community_id: None,
page, page,
limit: fetchLimit, limit: Some(fetchLimit),
sort, sort,
type_: ListingType.Community, type_: Some(ListingType.Community),
saved_only: false, saved_only: Some(false),
}; auth: req.auth,
this.setName(getCommentsForm, communityName); });
setOptionalAuth(getCommentsForm, req.auth); promises.push(Promise.resolve());
promises.push(req.client.getComments(getCommentsForm)); promises.push(req.client.getComments(getCommentsForm));
} }
return promises; return promises;
} }
static setName(obj: any, name_: string) {
obj.community_name = name_;
}
componentDidUpdate(_: any, lastState: State) { componentDidUpdate(_: any, lastState: State) {
if ( if (
lastState.dataType !== this.state.dataType || lastState.dataType !== this.state.dataType ||
@ -231,11 +255,18 @@ export class Community extends Component<any, State> {
} }
get documentTitle(): string { get documentTitle(): string {
return `${this.state.communityRes.community_view.community.title} - ${this.state.siteRes.site_view.site.name}`; return this.state.communityRes.match({
some: res =>
this.state.siteRes.site_view.match({
some: siteView =>
`${res.community_view.community.title} - ${siteView.site.name}`,
none: "",
}),
none: "",
});
} }
render() { render() {
let cv = this.state.communityRes?.community_view;
return ( return (
<div class="container"> <div class="container">
{this.state.communityLoading ? ( {this.state.communityLoading ? (
@ -243,83 +274,99 @@ export class Community extends Component<any, State> {
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
<> this.state.communityRes.match({
<HtmlTags some: res => (
title={this.documentTitle} <>
path={this.context.router.route.match.url} <HtmlTags
description={cv.community.description} title={this.documentTitle}
image={cv.community.icon} path={this.context.router.route.match.url}
/> description={res.community_view.community.description}
image={res.community_view.community.icon}
/>
<div class="row"> <div class="row">
<div class="col-12 col-md-8"> <div class="col-12 col-md-8">
{this.communityInfo()} {this.communityInfo()}
<div class="d-block d-md-none"> <div class="d-block d-md-none">
<button <button
class="btn btn-secondary d-inline-block mb-2 mr-3" class="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")}{" "}
<Icon <Icon
icon={ icon={
this.state.showSidebarMobile this.state.showSidebarMobile
? `minus-square` ? `minus-square`
: `plus-square` : `plus-square`
} }
classes="icon-inline" classes="icon-inline"
/>
</button>
{this.state.showSidebarMobile && (
<>
<Sidebar
community_view={cv}
moderators={this.state.communityRes.moderators}
admins={this.state.siteRes.admins}
online={this.state.communityRes.online}
enableNsfw={
this.state.siteRes.site_view.site.enable_nsfw
}
/>
{!cv.community.local && this.state.communityRes.site && (
<SiteSidebar
site={this.state.communityRes.site}
showLocal={showLocal(this.isoData)}
/> />
</button>
{this.state.showSidebarMobile && (
<>
<Sidebar
community_view={res.community_view}
moderators={res.moderators}
admins={this.state.siteRes.admins}
online={res.online}
enableNsfw={enableNsfw(this.state.siteRes)}
/>
{!res.community_view.community.local &&
res.site.match({
some: site => (
<SiteSidebar
site={site}
showLocal={showLocal(this.isoData)}
admins={None}
counts={None}
online={None}
/>
),
none: <></>,
})}
</>
)} )}
</> </div>
)} {this.selects()}
{this.listings()}
<Paginator
page={this.state.page}
onChange={this.handlePageChange}
/>
</div>
<div class="d-none d-md-block col-md-4">
<Sidebar
community_view={res.community_view}
moderators={res.moderators}
admins={this.state.siteRes.admins}
online={res.online}
enableNsfw={enableNsfw(this.state.siteRes)}
/>
{!res.community_view.community.local &&
res.site.match({
some: site => (
<SiteSidebar
site={site}
showLocal={showLocal(this.isoData)}
admins={None}
counts={None}
online={None}
/>
),
none: <></>,
})}
</div>
</div> </div>
{this.selects()} </>
{this.listings()} ),
<Paginator none: <></>,
page={this.state.page} })
onChange={this.handlePageChange}
/>
</div>
<div class="d-none d-md-block col-md-4">
<Sidebar
community_view={cv}
moderators={this.state.communityRes.moderators}
admins={this.state.siteRes.admins}
online={this.state.communityRes.online}
enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
/>
{!cv.community.local && this.state.communityRes.site && (
<SiteSidebar
site={this.state.communityRes.site}
showLocal={showLocal(this.isoData)}
/>
)}
</div>
</div>
</>
)} )}
</div> </div>
); );
} }
listings() { listings() {
let site = this.state.siteRes.site_view.site;
return this.state.dataType == DataType.Post ? ( return this.state.dataType == DataType.Post ? (
this.state.postsLoading ? ( this.state.postsLoading ? (
<h5> <h5>
@ -329,8 +376,8 @@ export class Community extends Component<any, State> {
<PostListings <PostListings
posts={this.state.posts} posts={this.state.posts}
removeDuplicates removeDuplicates
enableDownvotes={site.enable_downvotes} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={site.enable_nsfw} enableNsfw={enableNsfw(this.state.siteRes)}
/> />
) )
) : this.state.commentsLoading ? ( ) : this.state.commentsLoading ? (
@ -342,32 +389,38 @@ export class Community extends Component<any, State> {
nodes={commentsToFlatNodes(this.state.comments)} nodes={commentsToFlatNodes(this.state.comments)}
noIndent noIndent
showContext showContext
enableDownvotes={site.enable_downvotes} enableDownvotes={enableDownvotes(this.state.siteRes)}
moderators={this.state.communityRes.map(r => r.moderators)}
admins={Some(this.state.siteRes.admins)}
maxCommentsShown={None}
/> />
); );
} }
communityInfo() { communityInfo() {
let community = this.state.communityRes.community_view.community; return this.state.communityRes
return ( .map(r => r.community_view.community)
<div class="mb-2"> .match({
<BannerIconHeader banner={community.banner} icon={community.icon} /> some: community => (
<h5 class="mb-0 overflow-wrap-anywhere">{community.title}</h5> <div class="mb-2">
<CommunityLink <BannerIconHeader banner={community.banner} icon={community.icon} />
community={community} <h5 class="mb-0 overflow-wrap-anywhere">{community.title}</h5>
realLink <CommunityLink
useApubName community={community}
muted realLink
hideAvatar useApubName
/> muted
</div> hideAvatar
); />
</div>
),
none: <></>,
});
} }
selects() { selects() {
let communityRss = communityRSSUrl( let communityRss = this.state.communityRes.map(r =>
this.state.communityRes.community_view.community.actor_id, communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
this.state.sort
); );
return ( return (
<div class="mb-3"> <div class="mb-3">
@ -380,10 +433,17 @@ export class Community extends Component<any, State> {
<span class="mr-2"> <span class="mr-2">
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} /> <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
</span> </span>
<a href={communityRss} title="RSS" rel={relTags}> {communityRss.match({
<Icon icon="rss" classes="text-muted small" /> some: rss => (
</a> <>
<link rel="alternate" type="application/atom+xml" href={communityRss} /> <a href={rss} title="RSS" rel={relTags}>
<Icon icon="rss" classes="text-muted small" />
</a>
<link rel="alternate" type="application/atom+xml" href={rss} />
</>
),
none: <></>,
})}
</div> </div>
); );
} }
@ -422,26 +482,28 @@ export class Community extends Component<any, State> {
fetchData() { fetchData() {
if (this.state.dataType == DataType.Post) { if (this.state.dataType == DataType.Post) {
let form: GetPosts = { let form = new GetPosts({
page: this.state.page, page: Some(this.state.page),
limit: fetchLimit, limit: Some(fetchLimit),
sort: this.state.sort, sort: Some(this.state.sort),
type_: ListingType.Community, type_: Some(ListingType.Community),
community_name: this.state.communityName, community_name: Some(this.state.communityName),
saved_only: false, community_id: None,
auth: authField(false), saved_only: Some(false),
}; auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.getPosts(form)); WebSocketService.Instance.send(wsClient.getPosts(form));
} else { } else {
let form: GetComments = { let form = new GetComments({
page: this.state.page, page: Some(this.state.page),
limit: fetchLimit, limit: Some(fetchLimit),
sort: this.state.sort, sort: Some(this.state.sort),
type_: ListingType.Community, type_: Some(ListingType.Community),
community_name: this.state.communityName, community_name: Some(this.state.communityName),
saved_only: false, community_id: None,
auth: authField(false), saved_only: Some(false),
}; auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.getComments(form)); WebSocketService.Instance.send(wsClient.getComments(form));
} }
} }
@ -454,15 +516,20 @@ export class Community extends Component<any, State> {
this.context.router.history.push("/"); this.context.router.history.push("/");
return; return;
} else if (msg.reconnect) { } else if (msg.reconnect) {
WebSocketService.Instance.send( this.state.communityRes.match({
wsClient.communityJoin({ some: res => {
community_id: this.state.communityRes.community_view.community.id, WebSocketService.Instance.send(
}) wsClient.communityJoin({
); community_id: res.community_view.community.id,
})
);
},
none: void 0,
});
this.fetchData(); this.fetchData();
} else if (op == UserOperation.GetCommunity) { } else if (op == UserOperation.GetCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg).data; let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
this.state.communityRes = data; this.state.communityRes = Some(data);
this.state.communityLoading = false; this.state.communityLoading = false;
this.setState(this.state); this.setState(this.state);
// TODO why is there no auth in this form? // TODO why is there no auth in this form?
@ -476,18 +543,25 @@ export class Community extends Component<any, State> {
op == UserOperation.DeleteCommunity || op == UserOperation.DeleteCommunity ||
op == UserOperation.RemoveCommunity op == UserOperation.RemoveCommunity
) { ) {
let data = wsJsonToRes<CommunityResponse>(msg).data; let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
this.state.communityRes.community_view = data.community_view; this.state.communityRes.match({
some: res => (res.community_view = data.community_view),
none: void 0,
});
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.FollowCommunity) { } else if (op == UserOperation.FollowCommunity) {
let data = wsJsonToRes<CommunityResponse>(msg).data; let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
this.state.communityRes.community_view.subscribed = this.state.communityRes.match({
data.community_view.subscribed; some: res => {
this.state.communityRes.community_view.counts.subscribers = res.community_view.subscribed = data.community_view.subscribed;
data.community_view.counts.subscribers; res.community_view.counts.subscribers =
data.community_view.counts.subscribers;
},
none: void 0,
});
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetPosts) { } else if (op == UserOperation.GetPosts) {
let data = wsJsonToRes<GetPostsResponse>(msg).data; let data = wsJsonToRes<GetPostsResponse>(msg, GetPostsResponse);
this.state.posts = data.posts; this.state.posts = data.posts;
this.state.postsLoading = false; this.state.postsLoading = false;
this.setState(this.state); this.setState(this.state);
@ -501,29 +575,39 @@ export class Community extends Component<any, State> {
op == UserOperation.StickyPost || op == UserOperation.StickyPost ||
op == UserOperation.SavePost op == UserOperation.SavePost
) { ) {
let data = wsJsonToRes<PostResponse>(msg).data; let data = wsJsonToRes<PostResponse>(msg, PostResponse);
editPostFindRes(data.post_view, this.state.posts); editPostFindRes(data.post_view, this.state.posts);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePost) { } else if (op == UserOperation.CreatePost) {
let data = wsJsonToRes<PostResponse>(msg).data; let data = wsJsonToRes<PostResponse>(msg, PostResponse);
this.state.posts.unshift(data.post_view); this.state.posts.unshift(data.post_view);
if ( if (
UserService.Instance.myUserInfo?.local_user_view.local_user UserService.Instance.myUserInfo
.show_new_post_notifs .map(m => m.local_user_view.local_user.show_new_post_notifs)
.unwrapOr(false)
) { ) {
notifyPost(data.post_view, this.context.router); 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).data; let data = wsJsonToRes<PostResponse>(msg, PostResponse);
createPostLikeFindRes(data.post_view, this.state.posts); createPostLikeFindRes(data.post_view, this.state.posts);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.AddModToCommunity) { } else if (op == UserOperation.AddModToCommunity) {
let data = wsJsonToRes<AddModToCommunityResponse>(msg).data; let data = wsJsonToRes<AddModToCommunityResponse>(
this.state.communityRes.moderators = data.moderators; msg,
AddModToCommunityResponse
);
this.state.communityRes.match({
some: res => (res.moderators = data.moderators),
none: void 0,
});
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BanFromCommunity) { } else if (op == UserOperation.BanFromCommunity) {
let data = wsJsonToRes<BanFromCommunityResponse>(msg).data; let data = wsJsonToRes<BanFromCommunityResponse>(
msg,
BanFromCommunityResponse
);
// TODO this might be incorrect // TODO this might be incorrect
this.state.posts this.state.posts
@ -532,7 +616,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).data; let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse);
this.state.comments = data.comments; this.state.comments = data.comments;
this.state.commentsLoading = false; this.state.commentsLoading = false;
this.setState(this.state); this.setState(this.state);
@ -541,11 +625,11 @@ export class Community extends Component<any, State> {
op == UserOperation.DeleteComment || op == UserOperation.DeleteComment ||
op == UserOperation.RemoveComment op == UserOperation.RemoveComment
) { ) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
editCommentRes(data.comment_view, this.state.comments); editCommentRes(data.comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
// Necessary since it might be a user reply // Necessary since it might be a user reply
if (data.form_id) { if (data.form_id) {
@ -553,23 +637,23 @@ export class Community extends Component<any, State> {
this.setState(this.state); this.setState(this.state);
} }
} else if (op == UserOperation.SaveComment) { } else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
saveCommentRes(data.comment_view, this.state.comments); saveCommentRes(data.comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
createCommentLikeRes(data.comment_view, this.state.comments); createCommentLikeRes(data.comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data; let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
updatePersonBlock(data); updatePersonBlock(data);
} else if (op == UserOperation.CreatePostReport) { } else if (op == UserOperation.CreatePostReport) {
let data = wsJsonToRes<PostReportResponse>(msg).data; let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
} else if (op == UserOperation.CreateCommentReport) { } else if (op == UserOperation.CreateCommentReport) {
let data = wsJsonToRes<CommentReportResponse>(msg).data; let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }

View file

@ -1,15 +1,22 @@
import { None } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { CommunityView, SiteView } from "lemmy-js-client"; import { CommunityView, GetSiteResponse } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService } from "../../services"; import { UserService } from "../../services";
import { isBrowser, setIsoData, toast, wsSubscribe } from "../../utils"; import {
enableNsfw,
isBrowser,
setIsoData,
toast,
wsSubscribe,
} from "../../utils";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import { CommunityForm } from "./community-form"; import { CommunityForm } from "./community-form";
interface CreateCommunityState { interface CreateCommunityState {
site_view: SiteView; siteRes: GetSiteResponse;
loading: boolean; loading: boolean;
} }
@ -17,7 +24,7 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context);
private subscription: Subscription; private subscription: Subscription;
private emptyState: CreateCommunityState = { private emptyState: CreateCommunityState = {
site_view: this.isoData.site_res.site_view, siteRes: this.isoData.site_res,
loading: false, loading: false,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -28,7 +35,7 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
if (!UserService.Instance.myUserInfo && 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`);
} }
@ -41,7 +48,10 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
} }
get documentTitle(): string { get documentTitle(): string {
return `${i18n.t("create_community")} - ${this.state.site_view.site.name}`; return this.state.siteRes.site_view.match({
some: siteView => `${i18n.t("create_community")} - ${siteView.site.name}`,
none: "",
});
} }
render() { render() {
@ -50,6 +60,8 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
@ -60,8 +72,9 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t("create_community")}</h5> <h5>{i18n.t("create_community")}</h5>
<CommunityForm <CommunityForm
community_view={None}
onCreate={this.handleCommunityCreate} onCreate={this.handleCommunityCreate}
enableNsfw={this.state.site_view.site.enable_nsfw} enableNsfw={enableNsfw(this.state.siteRes)}
/> />
</div> </div>
</div> </div>

View file

@ -1,3 +1,4 @@
import { Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { import {
@ -8,11 +9,15 @@ import {
FollowCommunity, FollowCommunity,
PersonViewSafe, PersonViewSafe,
RemoveCommunity, RemoveCommunity,
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";
import { import {
authField, amAdmin,
amMod,
amTopMod,
auth,
getUnixTime, getUnixTime,
mdToHtml, mdToHtml,
numToSI, numToSI,
@ -29,15 +34,15 @@ interface SidebarProps {
moderators: CommunityModeratorView[]; moderators: CommunityModeratorView[];
admins: PersonViewSafe[]; admins: PersonViewSafe[];
online: number; online: number;
enableNsfw: boolean; enableNsfw?: boolean;
showIcon?: boolean; showIcon?: boolean;
} }
interface SidebarState { interface SidebarState {
removeReason: Option<string>;
removeExpires: Option<string>;
showEdit: boolean; showEdit: boolean;
showRemoveDialog: boolean; showRemoveDialog: boolean;
removeReason: string;
removeExpires: string;
showConfirmLeaveModTeam: boolean; showConfirmLeaveModTeam: boolean;
} }
@ -64,7 +69,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
this.sidebar() this.sidebar()
) : ( ) : (
<CommunityForm <CommunityForm
community_view={this.props.community_view} community_view={Some(this.props.community_view)}
onEdit={this.handleEditCommunity} onEdit={this.handleEditCommunity}
onCancel={this.handleEditCancel} onCancel={this.handleEditCancel}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
@ -284,14 +289,12 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
description() { description() {
let description = this.props.community_view.community.description; let description = this.props.community_view.community.description;
return ( return description.match({
description && ( some: desc => (
<div <div className="md-div" dangerouslySetInnerHTML={mdToHtml(desc)} />
className="md-div" ),
dangerouslySetInnerHTML={mdToHtml(description)} none: <></>,
/> });
)
);
} }
adminButtons() { adminButtons() {
@ -299,7 +302,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
return ( return (
<> <>
<ul class="list-inline mb-1 text-muted font-weight-bold"> <ul class="list-inline mb-1 text-muted font-weight-bold">
{this.canMod && ( {amMod(Some(this.props.moderators)) && (
<> <>
<li className="list-inline-item-action"> <li className="list-inline-item-action">
<button <button
@ -311,7 +314,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<Icon icon="edit" classes="icon-inline" /> <Icon icon="edit" classes="icon-inline" />
</button> </button>
</li> </li>
{!this.amTopMod && {!amTopMod(Some(this.props.moderators)) &&
(!this.state.showConfirmLeaveModTeam ? ( (!this.state.showConfirmLeaveModTeam ? (
<li className="list-inline-item-action"> <li className="list-inline-item-action">
<button <button
@ -350,7 +353,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</li> </li>
</> </>
))} ))}
{this.amTopMod && ( {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" class="btn btn-link text-muted d-inline-block"
@ -377,7 +380,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
)} )}
</> </>
)} )}
{this.canAdmin && ( {amAdmin(Some(this.props.admins)) && (
<li className="list-inline-item"> <li className="list-inline-item">
{!this.props.community_view.community.removed ? ( {!this.props.community_view.community.removed ? (
<button <button
@ -408,7 +411,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
id="remove-reason" id="remove-reason"
class="form-control mr-2" class="form-control mr-2"
placeholder={i18n.t("optional")} placeholder={i18n.t("optional")}
value={this.state.removeReason} value={toUndefined(this.state.removeReason)}
onInput={linkEvent(this, this.handleModRemoveReasonChange)} onInput={linkEvent(this, this.handleModRemoveReasonChange)}
/> />
</div> </div>
@ -445,11 +448,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
handleDeleteClick(i: Sidebar, event: any) { handleDeleteClick(i: Sidebar, event: any) {
event.preventDefault(); event.preventDefault();
let deleteForm: DeleteCommunity = { let deleteForm = new DeleteCommunity({
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
deleted: !i.props.community_view.community.deleted, deleted: !i.props.community_view.community.deleted,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.deleteCommunity(deleteForm)); WebSocketService.Instance.send(wsClient.deleteCommunity(deleteForm));
} }
@ -459,15 +462,20 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
} }
handleLeaveModTeamClick(i: Sidebar) { handleLeaveModTeamClick(i: Sidebar) {
let form: AddModToCommunity = { UserService.Instance.myUserInfo.match({
person_id: UserService.Instance.myUserInfo.local_user_view.person.id, some: mui => {
community_id: i.props.community_view.community.id, let form = new AddModToCommunity({
added: false, person_id: mui.local_user_view.person.id,
auth: authField(), community_id: i.props.community_view.community.id,
}; added: false,
WebSocketService.Instance.send(wsClient.addModToCommunity(form)); auth: auth().unwrap(),
i.state.showConfirmLeaveModTeam = false; });
i.setState(i.state); WebSocketService.Instance.send(wsClient.addModToCommunity(form));
i.state.showConfirmLeaveModTeam = false;
i.setState(i.state);
},
none: void 0,
});
} }
handleCancelLeaveModTeamClick(i: Sidebar) { handleCancelLeaveModTeamClick(i: Sidebar) {
@ -478,67 +486,47 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
handleUnsubscribe(i: Sidebar, event: any) { handleUnsubscribe(i: Sidebar, event: any) {
event.preventDefault(); event.preventDefault();
let community_id = i.props.community_view.community.id; let community_id = i.props.community_view.community.id;
let form: FollowCommunity = { let form = new FollowCommunity({
community_id, community_id,
follow: false, follow: false,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.followCommunity(form)); WebSocketService.Instance.send(wsClient.followCommunity(form));
// Update myUserInfo // Update myUserInfo
UserService.Instance.myUserInfo.follows = UserService.Instance.myUserInfo.match({
UserService.Instance.myUserInfo.follows.filter( some: mui =>
i => i.community.id != community_id (mui.follows = mui.follows.filter(i => i.community.id != community_id)),
); none: void 0,
});
} }
handleSubscribe(i: Sidebar, event: any) { handleSubscribe(i: Sidebar, event: any) {
event.preventDefault(); event.preventDefault();
let community_id = i.props.community_view.community.id; let community_id = i.props.community_view.community.id;
let form: FollowCommunity = { let form = new FollowCommunity({
community_id, community_id,
follow: true, follow: true,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.followCommunity(form)); WebSocketService.Instance.send(wsClient.followCommunity(form));
// Update myUserInfo // Update myUserInfo
UserService.Instance.myUserInfo.follows.push({ UserService.Instance.myUserInfo.match({
community: i.props.community_view.community, some: mui =>
follower: UserService.Instance.myUserInfo.local_user_view.person, mui.follows.push({
community: i.props.community_view.community,
follower: mui.local_user_view.person,
}),
none: void 0,
}); });
} }
private get amTopMod(): boolean {
return (
this.props.moderators[0].moderator.id ==
UserService.Instance.myUserInfo.local_user_view.person.id
);
}
get canMod(): boolean {
return (
UserService.Instance.myUserInfo &&
this.props.moderators
.map(m => m.moderator.id)
.includes(UserService.Instance.myUserInfo.local_user_view.person.id)
);
}
get canAdmin(): boolean {
return (
UserService.Instance.myUserInfo &&
this.props.admins
.map(a => a.person.id)
.includes(UserService.Instance.myUserInfo.local_user_view.person.id)
);
}
get canPost(): boolean { get canPost(): boolean {
return ( return (
!this.props.community_view.community.posting_restricted_to_mods || !this.props.community_view.community.posting_restricted_to_mods ||
this.canMod || amMod(Some(this.props.moderators)) ||
this.canAdmin amAdmin(Some(this.props.admins))
); );
} }
@ -560,13 +548,13 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
handleModRemoveSubmit(i: Sidebar, event: any) { handleModRemoveSubmit(i: Sidebar, event: any) {
event.preventDefault(); event.preventDefault();
let removeForm: RemoveCommunity = { let removeForm = new RemoveCommunity({
community_id: i.props.community_view.community.id, community_id: i.props.community_view.community.id,
removed: !i.props.community_view.community.removed, removed: !i.props.community_view.community.removed,
reason: i.state.removeReason, reason: i.state.removeReason,
expires: getUnixTime(i.state.removeExpires), expires: i.state.removeExpires.map(getUnixTime),
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.removeCommunity(removeForm)); WebSocketService.Instance.send(wsClient.removeCommunity(removeForm));
i.state.showRemoveDialog = false; i.state.showRemoveDialog = false;

View file

@ -1,3 +1,4 @@
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 { import {
@ -9,14 +10,17 @@ import {
PersonViewSafe, PersonViewSafe,
SaveSiteConfig, SaveSiteConfig,
SiteResponse, SiteResponse,
toUndefined,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { import {
authField, auth,
capitalizeFirstLetter, capitalizeFirstLetter,
isBrowser, isBrowser,
randomStr, randomStr,
@ -24,9 +28,7 @@ import {
showLocal, showLocal,
toast, toast,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
@ -35,24 +37,26 @@ import { SiteForm } from "./site-form";
interface AdminSettingsState { interface AdminSettingsState {
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
siteConfigRes: GetSiteConfigResponse; siteConfigRes: Option<GetSiteConfigResponse>;
siteConfigHjson: string; siteConfigHjson: Option<string>;
loading: boolean;
banned: PersonViewSafe[]; banned: PersonViewSafe[];
loading: boolean;
siteConfigLoading: boolean; siteConfigLoading: boolean;
leaveAdminTeamLoading: boolean; leaveAdminTeamLoading: boolean;
} }
export class AdminSettings extends Component<any, AdminSettingsState> { export class AdminSettings extends Component<any, AdminSettingsState> {
private siteConfigTextAreaId = `site-config-${randomStr()}`; private siteConfigTextAreaId = `site-config-${randomStr()}`;
private isoData = setIsoData(this.context); private isoData = setIsoData(
this.context,
GetSiteConfigResponse,
BannedPersonsResponse
);
private subscription: Subscription; private subscription: Subscription;
private emptyState: AdminSettingsState = { private emptyState: AdminSettingsState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
siteConfigHjson: null, siteConfigHjson: None,
siteConfigRes: { siteConfigRes: None,
config_hjson: null,
},
banned: [], banned: [],
loading: true, loading: true,
siteConfigLoading: null, siteConfigLoading: null,
@ -69,20 +73,26 @@ 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.siteConfigRes = this.isoData.routeData[0]; this.state.siteConfigRes = Some(
this.state.siteConfigHjson = this.state.siteConfigRes.config_hjson; this.isoData.routeData[0] as GetSiteConfigResponse
this.state.banned = this.isoData.routeData[1].banned; );
this.state.siteConfigHjson = this.state.siteConfigRes.map(
s => s.config_hjson
);
this.state.banned = (
this.isoData.routeData[1] as BannedPersonsResponse
).banned;
this.state.siteConfigLoading = false; this.state.siteConfigLoading = false;
this.state.loading = false; this.state.loading = false;
} else { } else {
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getSiteConfig({ wsClient.getSiteConfig({
auth: authField(), auth: auth().unwrap(),
}) })
); );
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getBannedPersons({ wsClient.getBannedPersons({
auth: authField(), auth: auth().unwrap(),
}) })
); );
} }
@ -91,10 +101,10 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let siteConfigForm: GetSiteConfig = { auth: req.auth }; let siteConfigForm = new GetSiteConfig({ auth: req.auth.unwrap() });
promises.push(req.client.getSiteConfig(siteConfigForm)); promises.push(req.client.getSiteConfig(siteConfigForm));
let bannedPersonsForm: GetBannedPersons = { auth: req.auth }; let bannedPersonsForm = new GetBannedPersons({ auth: req.auth.unwrap() });
promises.push(req.client.getBannedPersons(bannedPersonsForm)); promises.push(req.client.getBannedPersons(bannedPersonsForm));
return promises; return promises;
@ -114,9 +124,10 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
} }
get documentTitle(): string { get documentTitle(): string {
return `${i18n.t("admin_settings")} - ${ return this.state.siteRes.site_view.match({
this.state.siteRes.site_view.site.name some: siteView => `${i18n.t("admin_settings")} - ${siteView.site.name}`,
}`; none: "",
});
} }
render() { render() {
@ -132,13 +143,18 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.siteRes.site_view.site.id && ( {this.state.siteRes.site_view.match({
<SiteForm some: siteView => (
site={this.state.siteRes.site_view.site} <SiteForm
showLocal={showLocal(this.isoData)} site={Some(siteView.site)}
/> showLocal={showLocal(this.isoData)}
)} />
),
none: <></>,
})}
{this.admins()} {this.admins()}
{this.bannedUsers()} {this.bannedUsers()}
</div> </div>
@ -210,7 +226,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
<div class="col-12"> <div class="col-12">
<textarea <textarea
id={this.siteConfigTextAreaId} id={this.siteConfigTextAreaId}
value={this.state.siteConfigHjson} value={toUndefined(this.state.siteConfigHjson)}
onInput={linkEvent(this, this.handleSiteConfigHjsonChange)} onInput={linkEvent(this, this.handleSiteConfigHjsonChange)}
class="form-control text-monospace" class="form-control text-monospace"
rows={3} rows={3}
@ -236,10 +252,10 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
handleSiteConfigSubmit(i: AdminSettings, event: any) { handleSiteConfigSubmit(i: AdminSettings, event: any) {
event.preventDefault(); event.preventDefault();
i.state.siteConfigLoading = true; i.state.siteConfigLoading = true;
let form: SaveSiteConfig = { let form = new SaveSiteConfig({
config_hjson: i.state.siteConfigHjson, config_hjson: toUndefined(i.state.siteConfigHjson),
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.saveSiteConfig(form)); WebSocketService.Instance.send(wsClient.saveSiteConfig(form));
i.setState(i.state); i.setState(i.state);
} }
@ -251,7 +267,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
handleLeaveAdminTeam(i: AdminSettings) { handleLeaveAdminTeam(i: AdminSettings) {
i.state.leaveAdminTeamLoading = true; i.state.leaveAdminTeamLoading = true;
WebSocketService.Instance.send(wsClient.leaveAdmin({ auth: authField() })); WebSocketService.Instance.send(
wsClient.leaveAdmin({ auth: auth().unwrap() })
);
i.setState(i.state); i.setState(i.state);
} }
@ -265,24 +283,26 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
this.setState(this.state); this.setState(this.state);
return; return;
} else if (op == UserOperation.EditSite) { } else if (op == UserOperation.EditSite) {
let data = wsJsonToRes<SiteResponse>(msg).data; let data = wsJsonToRes<SiteResponse>(msg, SiteResponse);
this.state.siteRes.site_view = data.site_view; this.state.siteRes.site_view = Some(data.site_view);
this.setState(this.state); 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).data; let data = wsJsonToRes<BannedPersonsResponse>(msg, BannedPersonsResponse);
this.state.banned = data.banned; this.state.banned = data.banned;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetSiteConfig) { } else if (op == UserOperation.GetSiteConfig) {
let data = wsJsonToRes<GetSiteConfigResponse>(msg).data; let data = wsJsonToRes<GetSiteConfigResponse>(msg, GetSiteConfigResponse);
this.state.siteConfigRes = data; this.state.siteConfigRes = Some(data);
this.state.loading = false; this.state.loading = false;
this.state.siteConfigHjson = this.state.siteConfigRes.config_hjson; this.state.siteConfigHjson = this.state.siteConfigRes.map(
s => s.config_hjson
);
this.setState(this.state); this.setState(this.state);
var textarea: any = document.getElementById(this.siteConfigTextAreaId); var textarea: any = document.getElementById(this.siteConfigTextAreaId);
autosize(textarea); autosize(textarea);
} else if (op == UserOperation.LeaveAdmin) { } else if (op == UserOperation.LeaveAdmin) {
let data = wsJsonToRes<GetSiteResponse>(msg).data; let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
this.state.siteRes.site_view = data.site_view; this.state.siteRes.site_view = data.site_view;
this.setState(this.state); this.setState(this.state);
this.state.leaveAdminTeamLoading = false; this.state.leaveAdminTeamLoading = false;
@ -290,9 +310,11 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
this.setState(this.state); this.setState(this.state);
this.context.router.history.push("/"); this.context.router.history.push("/");
} else if (op == UserOperation.SaveSiteConfig) { } else if (op == UserOperation.SaveSiteConfig) {
let data = wsJsonToRes<GetSiteConfigResponse>(msg).data; let data = wsJsonToRes<GetSiteConfigResponse>(msg, GetSiteConfigResponse);
this.state.siteConfigRes = data; this.state.siteConfigRes = Some(data);
this.state.siteConfigHjson = this.state.siteConfigRes.config_hjson; this.state.siteConfigHjson = this.state.siteConfigRes.map(
s => s.config_hjson
);
this.state.siteConfigLoading = false; this.state.siteConfigLoading = false;
toast(i18n.t("site_saved")); toast(i18n.t("site_saved"));
this.setState(this.state); this.setState(this.state);

View file

@ -1,3 +1,4 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
@ -23,38 +24,41 @@ import {
SiteResponse, SiteResponse,
SortType, SortType,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { DataType, InitialFetchRequest } from "../../interfaces"; import { DataType, InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, auth,
commentsToFlatNodes, commentsToFlatNodes,
createCommentLikeRes, createCommentLikeRes,
createPostLikeFindRes, createPostLikeFindRes,
editCommentRes, editCommentRes,
editPostFindRes, editPostFindRes,
enableDownvotes,
enableNsfw,
fetchLimit, fetchLimit,
getDataTypeFromProps, getDataTypeFromProps,
getListingTypeFromProps, getListingTypeFromProps,
getPageFromProps, getPageFromProps,
getSortTypeFromProps, getSortTypeFromProps,
isBrowser,
notifyPost, notifyPost,
relTags, relTags,
restoreScrollPosition, restoreScrollPosition,
saveCommentRes, saveCommentRes,
saveScrollPosition, saveScrollPosition,
setIsoData, setIsoData,
setOptionalAuth,
setupTippy, setupTippy,
showLocal, showLocal,
toast, toast,
trendingFetchLimit,
updatePersonBlock, updatePersonBlock,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { CommentNodes } from "../comment/comment-nodes"; import { CommentNodes } from "../comment/comment-nodes";
import { DataTypeSelect } from "../common/data-type-select"; import { DataTypeSelect } from "../common/data-type-select";
@ -70,17 +74,17 @@ import { SiteSidebar } from "./site-sidebar";
interface HomeState { interface HomeState {
trendingCommunities: CommunityView[]; trendingCommunities: CommunityView[];
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
showSubscribedMobile: boolean;
showTrendingMobile: boolean;
showSidebarMobile: boolean;
subscribedCollapsed: boolean;
loading: boolean;
posts: PostView[]; posts: PostView[];
comments: CommentView[]; comments: CommentView[];
listingType: ListingType; listingType: ListingType;
dataType: DataType; dataType: DataType;
sort: SortType; sort: SortType;
page: number; page: number;
showSubscribedMobile: boolean;
showTrendingMobile: boolean;
showSidebarMobile: boolean;
subscribedCollapsed: boolean;
loading: boolean;
} }
interface HomeProps { interface HomeProps {
@ -98,7 +102,12 @@ interface UrlParams {
} }
export class Home extends Component<any, HomeState> { export class Home extends Component<any, HomeState> {
private isoData = setIsoData(this.context); private isoData = setIsoData(
this.context,
GetPostsResponse,
GetCommentsResponse,
ListCommunitiesResponse
);
private subscription: Subscription; private subscription: Subscription;
private emptyState: HomeState = { private emptyState: HomeState = {
trendingCommunities: [], trendingCommunities: [],
@ -113,7 +122,7 @@ export class Home extends Component<any, HomeState> {
listingType: getListingTypeFromProps( listingType: getListingTypeFromProps(
this.props, this.props,
ListingType[ ListingType[
this.isoData.site_res.site_view?.site.default_post_listing_type this.isoData.site_res.site_view.unwrap().site.default_post_listing_type
] ]
), ),
dataType: getDataTypeFromProps(this.props), dataType: getDataTypeFromProps(this.props),
@ -135,12 +144,25 @@ export class Home extends Component<any, HomeState> {
// 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) {
if (this.state.dataType == DataType.Post) { let postsRes = Some(this.isoData.routeData[0] as GetPostsResponse);
this.state.posts = this.isoData.routeData[0].posts; let commentsRes = Some(this.isoData.routeData[1] as GetCommentsResponse);
} else { let trendingRes = this.isoData.routeData[2] as ListCommunitiesResponse;
this.state.comments = this.isoData.routeData[0].comments;
postsRes.match({
some: pvs => (this.state.posts = pvs.posts),
none: void 0,
});
commentsRes.match({
some: cvs => (this.state.comments = cvs.comments),
none: void 0,
});
this.state.trendingCommunities = trendingRes.communities;
if (isBrowser()) {
WebSocketService.Instance.send(
wsClient.communityJoin({ community_id: 0 })
);
} }
this.state.trendingCommunities = this.isoData.routeData[1].communities;
this.state.loading = false; this.state.loading = false;
} else { } else {
this.fetchTrendingCommunities(); this.fetchTrendingCommunities();
@ -149,12 +171,13 @@ export class Home extends Component<any, HomeState> {
} }
fetchTrendingCommunities() { fetchTrendingCommunities() {
let listCommunitiesForm: ListCommunities = { let listCommunitiesForm = new ListCommunities({
type_: ListingType.Local, type_: Some(ListingType.Local),
sort: SortType.Hot, sort: Some(SortType.Hot),
limit: 6, limit: Some(trendingFetchLimit),
auth: authField(false), page: None,
}; auth: auth(false).ok(),
});
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.listCommunities(listCommunitiesForm) wsClient.listCommunities(listCommunitiesForm)
); );
@ -165,8 +188,6 @@ export class Home extends Component<any, HomeState> {
if (!this.state.siteRes.site_view) { if (!this.state.siteRes.site_view) {
this.context.router.history.push("/setup"); this.context.router.history.push("/setup");
} }
WebSocketService.Instance.send(wsClient.communityJoin({ community_id: 0 }));
setupTippy(); setupTippy();
} }
@ -192,58 +213,69 @@ export class Home extends Component<any, HomeState> {
: DataType.Post; : DataType.Post;
// TODO figure out auth default_listingType, default_sort_type // TODO figure out auth default_listingType, default_sort_type
let type_: ListingType = pathSplit[5] let type_: Option<ListingType> = Some(
? ListingType[pathSplit[5]] pathSplit[5]
: UserService.Instance.myUserInfo ? ListingType[pathSplit[5]]
? Object.values(ListingType)[ : UserService.Instance.myUserInfo.match({
UserService.Instance.myUserInfo.local_user_view.local_user some: mui =>
.default_listing_type Object.values(ListingType)[
] mui.local_user_view.local_user.default_listing_type
: null; ],
let sort: SortType = pathSplit[7] none: ListingType.Local,
? SortType[pathSplit[7]] })
: UserService.Instance.myUserInfo );
? Object.values(SortType)[ let sort: Option<SortType> = Some(
UserService.Instance.myUserInfo.local_user_view.local_user pathSplit[7]
.default_sort_type ? SortType[pathSplit[7]]
] : UserService.Instance.myUserInfo.match({
: SortType.Active; some: mui =>
Object.values(SortType)[
mui.local_user_view.local_user.default_sort_type
],
none: SortType.Active,
})
);
let page = pathSplit[9] ? Number(pathSplit[9]) : 1; let page = Some(pathSplit[9] ? Number(pathSplit[9]) : 1);
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
if (dataType == DataType.Post) { if (dataType == DataType.Post) {
let getPostsForm: GetPosts = { let getPostsForm = new GetPosts({
community_id: None,
community_name: None,
type_,
page, page,
limit: fetchLimit, limit: Some(fetchLimit),
sort, sort,
saved_only: false, saved_only: Some(false),
}; auth: req.auth,
if (type_) { });
getPostsForm.type_ = type_;
}
setOptionalAuth(getPostsForm, req.auth);
promises.push(req.client.getPosts(getPostsForm)); promises.push(req.client.getPosts(getPostsForm));
promises.push(Promise.resolve());
} else { } else {
let getCommentsForm: GetComments = { let getCommentsForm = new GetComments({
community_id: None,
community_name: None,
page, page,
limit: fetchLimit, limit: Some(fetchLimit),
sort, sort,
type_: type_ || ListingType.Local, type_,
saved_only: false, saved_only: Some(false),
}; auth: req.auth,
setOptionalAuth(getCommentsForm, req.auth); });
promises.push(Promise.resolve());
promises.push(req.client.getComments(getCommentsForm)); promises.push(req.client.getComments(getCommentsForm));
} }
let trendingCommunitiesForm: ListCommunities = { let trendingCommunitiesForm = new ListCommunities({
type_: ListingType.Local, type_: Some(ListingType.Local),
sort: SortType.Hot, sort: Some(SortType.Hot),
limit: 6, limit: Some(trendingFetchLimit),
}; page: None,
setOptionalAuth(trendingCommunitiesForm, req.auth); auth: req.auth,
});
promises.push(req.client.listCommunities(trendingCommunitiesForm)); promises.push(req.client.listCommunities(trendingCommunitiesForm));
return promises; return promises;
@ -262,13 +294,14 @@ export class Home extends Component<any, HomeState> {
} }
get documentTitle(): string { get documentTitle(): string {
return `${ return this.state.siteRes.site_view.match({
this.state.siteRes.site_view some: siteView =>
? this.state.siteRes.site_view.site.description siteView.site.description.match({
? `${this.state.siteRes.site_view.site.name} - ${this.state.siteRes.site_view.site.description}` some: desc => `${siteView.site.name} - ${desc}`,
: this.state.siteRes.site_view.site.name none: siteView.site.name,
: "Lemmy" }),
}`; none: "Lemmy",
});
} }
render() { render() {
@ -277,8 +310,10 @@ export class Home extends Component<any, HomeState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.siteRes.site_view?.site && ( {this.state.siteRes.site_view.isSome() && (
<div class="row"> <div class="row">
<main role="main" class="col-12 col-md-8"> <main role="main" class="col-12 col-md-8">
<div class="d-block d-md-none">{this.mobileView()}</div> <div class="d-block d-md-none">{this.mobileView()}</div>
@ -291,28 +326,34 @@ export class Home extends Component<any, HomeState> {
); );
} }
get hasFollows(): boolean {
return UserService.Instance.myUserInfo.match({
some: mui => mui.follows.length > 0,
none: false,
});
}
mobileView() { mobileView() {
let siteRes = this.state.siteRes; let siteRes = this.state.siteRes;
return ( return (
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
{UserService.Instance.myUserInfo && {this.hasFollows && (
UserService.Instance.myUserInfo.follows.length > 0 && ( <button
<button class="btn btn-secondary d-inline-block mb-2 mr-3"
class="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")}{" "} <Icon
<Icon icon={
icon={ this.state.showSubscribedMobile
this.state.showSubscribedMobile ? `minus-square`
? `minus-square` : `plus-square`
: `plus-square` }
} classes="icon-inline"
classes="icon-inline" />
/> </button>
</button> )}
)}
<button <button
class="btn btn-secondary d-inline-block mb-2 mr-3" class="btn btn-secondary d-inline-block mb-2 mr-3"
onClick={linkEvent(this, this.handleShowTrendingMobile)} onClick={linkEvent(this, this.handleShowTrendingMobile)}
@ -337,15 +378,19 @@ export class Home extends Component<any, HomeState> {
classes="icon-inline" classes="icon-inline"
/> />
</button> </button>
{this.state.showSidebarMobile && ( {this.state.showSidebarMobile &&
<SiteSidebar siteRes.site_view.match({
site={siteRes.site_view.site} some: siteView => (
admins={siteRes.admins} <SiteSidebar
counts={siteRes.site_view.counts} site={siteView.site}
online={siteRes.online} admins={Some(siteRes.admins)}
showLocal={showLocal(this.isoData)} counts={Some(siteView.counts)}
/> online={Some(siteRes.online)}
)} showLocal={showLocal(this.isoData)}
/>
),
none: <></>,
})}
{this.state.showTrendingMobile && ( {this.state.showTrendingMobile && (
<div class="col-12 card border-secondary mb-3"> <div class="col-12 card border-secondary mb-3">
<div class="card-body">{this.trendingCommunities()}</div> <div class="card-body">{this.trendingCommunities()}</div>
@ -374,21 +419,23 @@ export class Home extends Component<any, HomeState> {
{this.exploreCommunitiesButton()} {this.exploreCommunitiesButton()}
</div> </div>
</div> </div>
{siteRes.site_view.match({
<SiteSidebar some: siteView => (
site={siteRes.site_view.site} <SiteSidebar
admins={siteRes.admins} site={siteView.site}
counts={siteRes.site_view.counts} admins={Some(siteRes.admins)}
online={siteRes.online} counts={Some(siteView.counts)}
showLocal={showLocal(this.isoData)} online={Some(siteRes.online)}
/> showLocal={showLocal(this.isoData)}
/>
{UserService.Instance.myUserInfo && ),
UserService.Instance.myUserInfo.follows.length > 0 && ( none: <></>,
<div class="card border-secondary mb-3"> })}
<div class="card-body">{this.subscribedCommunities()}</div> {this.hasFollows && (
</div> <div class="card border-secondary mb-3">
)} <div class="card-body">{this.subscribedCommunities()}</div>
</div>
)}
</div> </div>
)} )}
</div> </div>
@ -458,11 +505,14 @@ export class Home extends Component<any, HomeState> {
</h5> </h5>
{!this.state.subscribedCollapsed && ( {!this.state.subscribedCollapsed && (
<ul class="list-inline mb-0"> <ul class="list-inline mb-0">
{UserService.Instance.myUserInfo.follows.map(cfv => ( {UserService.Instance.myUserInfo
<li class="list-inline-item d-inline-block"> .map(m => m.follows)
<CommunityLink community={cfv.community} /> .unwrapOr([])
</li> .map(cfv => (
))} <li class="list-inline-item d-inline-block">
<CommunityLink community={cfv.community} />
</li>
))}
</ul> </ul>
)} )}
</div> </div>
@ -501,22 +551,24 @@ export class Home extends Component<any, HomeState> {
} }
listings() { listings() {
let site = this.state.siteRes.site_view.site;
return this.state.dataType == DataType.Post ? ( return this.state.dataType == DataType.Post ? (
<PostListings <PostListings
posts={this.state.posts} posts={this.state.posts}
showCommunity showCommunity
removeDuplicates removeDuplicates
enableDownvotes={site.enable_downvotes} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={site.enable_nsfw} enableNsfw={enableNsfw(this.state.siteRes)}
/> />
) : ( ) : (
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(this.state.comments)} nodes={commentsToFlatNodes(this.state.comments)}
moderators={None}
admins={None}
maxCommentsShown={None}
noIndent noIndent
showCommunity showCommunity
showContext showContext
enableDownvotes={site.enable_downvotes} enableDownvotes={enableDownvotes(this.state.siteRes)}
/> />
); );
} }
@ -524,9 +576,9 @@ export class Home extends Component<any, HomeState> {
selects() { selects() {
let allRss = `/feeds/all.xml?sort=${this.state.sort}`; let allRss = `/feeds/all.xml?sort=${this.state.sort}`;
let localRss = `/feeds/local.xml?sort=${this.state.sort}`; let localRss = `/feeds/local.xml?sort=${this.state.sort}`;
let frontRss = UserService.Instance.myUserInfo let frontRss = auth(false)
? `/feeds/front/${UserService.Instance.auth}.xml?sort=${this.state.sort}` .ok()
: ""; .map(auth => `/feeds/front/${auth}.xml?sort=${this.state.sort}`);
return ( return (
<div className="mb-3"> <div className="mb-3">
@ -563,19 +615,18 @@ export class Home extends Component<any, HomeState> {
<link rel="alternate" type="application/atom+xml" href={localRss} /> <link rel="alternate" type="application/atom+xml" href={localRss} />
</> </>
)} )}
{UserService.Instance.myUserInfo && {this.state.listingType == ListingType.Subscribed &&
this.state.listingType == ListingType.Subscribed && ( frontRss.match({
<> some: rss => (
<a href={frontRss} title="RSS" rel={relTags}> <>
<Icon icon="rss" classes="text-muted small" /> <a href={rss} title="RSS" rel={relTags}>
</a> <Icon icon="rss" classes="text-muted small" />
<link </a>
rel="alternate" <link rel="alternate" type="application/atom+xml" href={rss} />
type="application/atom+xml" </>
href={frontRss} ),
/> none: <></>,
</> })}
)}
</div> </div>
); );
} }
@ -622,27 +673,29 @@ export class Home extends Component<any, HomeState> {
fetchData() { fetchData() {
if (this.state.dataType == DataType.Post) { if (this.state.dataType == DataType.Post) {
let getPostsForm: GetPosts = { let getPostsForm = new GetPosts({
page: this.state.page, community_id: None,
limit: fetchLimit, community_name: None,
sort: this.state.sort, page: Some(this.state.page),
saved_only: false, limit: Some(fetchLimit),
auth: authField(false), sort: Some(this.state.sort),
}; saved_only: Some(false),
if (this.state.listingType) { auth: auth(false).ok(),
getPostsForm.type_ = this.state.listingType; type_: Some(this.state.listingType),
} });
WebSocketService.Instance.send(wsClient.getPosts(getPostsForm)); WebSocketService.Instance.send(wsClient.getPosts(getPostsForm));
} else { } else {
let getCommentsForm: GetComments = { let getCommentsForm = new GetComments({
page: this.state.page, community_id: None,
limit: fetchLimit, community_name: None,
sort: this.state.sort, page: Some(this.state.page),
type_: this.state.listingType, limit: Some(fetchLimit),
saved_only: false, sort: Some(this.state.sort),
auth: authField(false), saved_only: Some(false),
}; auth: auth(false).ok(),
type_: Some(this.state.listingType),
});
WebSocketService.Instance.send(wsClient.getComments(getCommentsForm)); WebSocketService.Instance.send(wsClient.getComments(getCommentsForm));
} }
} }
@ -659,46 +712,55 @@ export class Home extends Component<any, HomeState> {
); );
this.fetchData(); this.fetchData();
} else if (op == UserOperation.ListCommunities) { } else if (op == UserOperation.ListCommunities) {
let data = wsJsonToRes<ListCommunitiesResponse>(msg).data; let data = wsJsonToRes<ListCommunitiesResponse>(
msg,
ListCommunitiesResponse
);
this.state.trendingCommunities = data.communities; this.state.trendingCommunities = data.communities;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.EditSite) { } else if (op == UserOperation.EditSite) {
let data = wsJsonToRes<SiteResponse>(msg).data; let data = wsJsonToRes<SiteResponse>(msg, SiteResponse);
this.state.siteRes.site_view = data.site_view; this.state.siteRes.site_view = Some(data.site_view);
this.setState(this.state); 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).data; let data = wsJsonToRes<GetPostsResponse>(msg, GetPostsResponse);
this.state.posts = data.posts; this.state.posts = data.posts;
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
WebSocketService.Instance.send(
wsClient.communityJoin({ community_id: 0 })
);
restoreScrollPosition(this.context); restoreScrollPosition(this.context);
setupTippy(); setupTippy();
} else if (op == UserOperation.CreatePost) { } else if (op == UserOperation.CreatePost) {
let data = wsJsonToRes<PostResponse>(msg).data; let data = wsJsonToRes<PostResponse>(msg, PostResponse);
// NSFW check // NSFW check
let nsfw = data.post_view.post.nsfw || data.post_view.community.nsfw; let nsfw = data.post_view.post.nsfw || data.post_view.community.nsfw;
let nsfwCheck = let nsfwCheck =
!nsfw || !nsfw ||
(nsfw && (nsfw &&
UserService.Instance.myUserInfo && UserService.Instance.myUserInfo
UserService.Instance.myUserInfo.local_user_view.local_user.show_nsfw); .map(m => m.local_user_view.local_user.show_nsfw)
.unwrapOr(false));
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, and you pass the nsfw check // Only push these if you're on the first page, and you pass the nsfw check
if (this.state.page == 1 && nsfwCheck) { if (this.state.page == 1 && nsfwCheck) {
// 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 (
UserService.Instance.myUserInfo.follows UserService.Instance.myUserInfo
.map(m => m.follows)
.unwrapOr([])
.map(c => c.community.id) .map(c => c.community.id)
.includes(data.post_view.community.id) .includes(data.post_view.community.id)
) { ) {
this.state.posts.unshift(data.post_view); this.state.posts.unshift(data.post_view);
if ( if (showPostNotifs) {
UserService.Instance.myUserInfo?.local_user_view.local_user
.show_new_post_notifs
) {
notifyPost(data.post_view, this.context.router); notifyPost(data.post_view, this.context.router);
} }
} }
@ -706,19 +768,13 @@ export class Home extends Component<any, HomeState> {
// If you're on the local view, only push it if its local // If you're on the local view, only push it if its local
if (data.post_view.post.local) { if (data.post_view.post.local) {
this.state.posts.unshift(data.post_view); this.state.posts.unshift(data.post_view);
if ( if (showPostNotifs) {
UserService.Instance.myUserInfo?.local_user_view.local_user
.show_new_post_notifs
) {
notifyPost(data.post_view, this.context.router); notifyPost(data.post_view, this.context.router);
} }
} }
} else { } else {
this.state.posts.unshift(data.post_view); this.state.posts.unshift(data.post_view);
if ( if (showPostNotifs) {
UserService.Instance.myUserInfo?.local_user_view.local_user
.show_new_post_notifs
) {
notifyPost(data.post_view, this.context.router); notifyPost(data.post_view, this.context.router);
} }
} }
@ -732,26 +788,26 @@ export class Home extends Component<any, HomeState> {
op == UserOperation.StickyPost || op == UserOperation.StickyPost ||
op == UserOperation.SavePost op == UserOperation.SavePost
) { ) {
let data = wsJsonToRes<PostResponse>(msg).data; let data = wsJsonToRes<PostResponse>(msg, PostResponse);
editPostFindRes(data.post_view, this.state.posts); editPostFindRes(data.post_view, this.state.posts);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) { } else if (op == UserOperation.CreatePostLike) {
let data = wsJsonToRes<PostResponse>(msg).data; let data = wsJsonToRes<PostResponse>(msg, PostResponse);
createPostLikeFindRes(data.post_view, this.state.posts); createPostLikeFindRes(data.post_view, this.state.posts);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.AddAdmin) { } else if (op == UserOperation.AddAdmin) {
let data = wsJsonToRes<AddAdminResponse>(msg).data; let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
this.state.siteRes.admins = data.admins; this.state.siteRes.admins = data.admins;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BanPerson) { } else if (op == UserOperation.BanPerson) {
let data = wsJsonToRes<BanPersonResponse>(msg).data; let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
this.state.posts this.state.posts
.filter(p => p.creator.id == data.person_view.person.id) .filter(p => p.creator.id == data.person_view.person.id)
.forEach(p => (p.creator.banned = data.banned)); .forEach(p => (p.creator.banned = data.banned));
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetComments) { } else if (op == UserOperation.GetComments) {
let data = wsJsonToRes<GetCommentsResponse>(msg).data; let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse);
this.state.comments = data.comments; this.state.comments = data.comments;
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
@ -760,18 +816,20 @@ export class Home extends Component<any, HomeState> {
op == UserOperation.DeleteComment || op == UserOperation.DeleteComment ||
op == UserOperation.RemoveComment op == UserOperation.RemoveComment
) { ) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
editCommentRes(data.comment_view, this.state.comments); editCommentRes(data.comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
// Necessary since it might be a user reply // Necessary since it might be a user reply
if (data.form_id) { if (data.form_id) {
// 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 (
UserService.Instance.myUserInfo.follows UserService.Instance.myUserInfo
.map(m => m.follows)
.unwrapOr([])
.map(c => c.community.id) .map(c => c.community.id)
.includes(data.comment_view.community.id) .includes(data.comment_view.community.id)
) { ) {
@ -783,23 +841,23 @@ export class Home extends Component<any, HomeState> {
this.setState(this.state); this.setState(this.state);
} }
} else if (op == UserOperation.SaveComment) { } else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
saveCommentRes(data.comment_view, this.state.comments); saveCommentRes(data.comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
createCommentLikeRes(data.comment_view, this.state.comments); createCommentLikeRes(data.comment_view, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data; let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
updatePersonBlock(data); updatePersonBlock(data);
} else if (op == UserOperation.CreatePostReport) { } else if (op == UserOperation.CreatePostReport) {
let data = wsJsonToRes<PostReportResponse>(msg).data; let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
} else if (op == UserOperation.CreateCommentReport) { } else if (op == UserOperation.CreateCommentReport) {
let data = wsJsonToRes<CommentReportResponse>(msg).data; let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }

View file

@ -1,3 +1,4 @@
import { None } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { GetSiteResponse } from "lemmy-js-client"; import { GetSiteResponse } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
@ -20,42 +21,56 @@ export class Instances extends Component<any, InstancesState> {
} }
get documentTitle(): string { get documentTitle(): string {
return `${i18n.t("instances")} - ${this.state.siteRes.site_view.site.name}`; return this.state.siteRes.site_view.match({
some: siteView => `${i18n.t("instances")} - ${siteView.site.name}`,
none: "",
});
} }
render() { render() {
let federated_instances = this.state.siteRes?.federated_instances; return this.state.siteRes.federated_instances.match({
return ( some: federated_instances => (
federated_instances && (
<div class="container"> <div class="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}
image={None}
/> />
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="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?.length > 0 && ( {federated_instances.allowed.match({
<div class="col-md-6"> some: allowed =>
<h5>{i18n.t("allowed_instances")}</h5> allowed.length > 0 && (
{this.itemList(federated_instances.allowed)} <div class="col-md-6">
</div> <h5>{i18n.t("allowed_instances")}</h5>
)} {this.itemList(allowed)}
{federated_instances.blocked?.length > 0 && ( </div>
<div class="col-md-6"> ),
<h5>{i18n.t("blocked_instances")}</h5> none: <></>,
{this.itemList(federated_instances.blocked)} })}
</div> {federated_instances.blocked.match({
)} some: blocked =>
blocked.length > 0 && (
<div class="col-md-6">
<h5>{i18n.t("blocked_instances")}</h5>
{this.itemList(blocked)}
</div>
),
none: <></>,
})}
</div> </div>
</div> </div>
) ),
); none: <></>,
});
} }
itemList(items: string[]) { itemList(items: string[]) {
let noneFound = <div>{i18n.t("none_found")}</div>;
return items.length > 0 ? ( return items.length > 0 ? (
<ul> <ul>
{items.map(i => ( {items.map(i => (
@ -67,7 +82,7 @@ export class Instances extends Component<any, InstancesState> {
))} ))}
</ul> </ul>
) : ( ) : (
<div>{i18n.t("none_found")}</div> noneFound
); );
} }
} }

View file

@ -1,3 +1,4 @@
import { None } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { GetSiteResponse } from "lemmy-js-client"; import { GetSiteResponse } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
@ -29,13 +30,22 @@ export class Legal extends Component<any, LegalState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<div {this.state.siteRes.site_view.match({
className="md-div" some: siteView =>
dangerouslySetInnerHTML={mdToHtml( siteView.site.legal_information.match({
this.state.siteRes.site_view.site.legal_information some: legal => (
)} <div
/> className="md-div"
dangerouslySetInnerHTML={mdToHtml(legal)}
/>
),
none: <></>,
}),
none: <></>,
})}
</div> </div>
); );
} }

View file

@ -1,25 +1,25 @@
import { None } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
GetSiteResponse, GetSiteResponse,
Login as LoginForm, Login as LoginForm,
LoginResponse, LoginResponse,
PasswordReset, PasswordReset,
SiteView,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, auth,
isBrowser, isBrowser,
setIsoData, setIsoData,
toast, toast,
validEmail, validEmail,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
@ -27,7 +27,7 @@ import { Spinner } from "../common/icon";
interface State { interface State {
loginForm: LoginForm; loginForm: LoginForm;
loginLoading: boolean; loginLoading: boolean;
site_view: SiteView; siteRes: GetSiteResponse;
} }
export class Login extends Component<any, State> { export class Login extends Component<any, State> {
@ -35,12 +35,12 @@ export class Login extends Component<any, State> {
private subscription: Subscription; private subscription: Subscription;
emptyState: State = { emptyState: State = {
loginForm: { loginForm: new LoginForm({
username_or_email: undefined, username_or_email: undefined,
password: undefined, password: undefined,
}, }),
loginLoading: false, loginLoading: false,
site_view: this.isoData.site_res.site_view, siteRes: this.isoData.site_res,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -58,7 +58,7 @@ export class Login extends Component<any, State> {
componentDidMount() { componentDidMount() {
// Navigate to home if already logged in // Navigate to home if already logged in
if (UserService.Instance.myUserInfo) { if (UserService.Instance.myUserInfo.isSome()) {
this.context.router.history.push("/"); this.context.router.history.push("/");
} }
} }
@ -70,7 +70,10 @@ export class Login extends Component<any, State> {
} }
get documentTitle(): string { get documentTitle(): string {
return `${i18n.t("login")} - ${this.state.site_view.site.name}`; return this.state.siteRes.site_view.match({
some: siteView => `${i18n.t("login")} - ${siteView.site.name}`,
none: "",
});
} }
get isLemmyMl(): boolean { get isLemmyMl(): boolean {
@ -83,6 +86,8 @@ export class Login extends Component<any, State> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</div> <div class="col-12 col-lg-6 offset-lg-3">{this.loginForm()}</div>
@ -173,9 +178,9 @@ export class Login extends Component<any, State> {
handlePasswordReset(i: Login, event: any) { handlePasswordReset(i: Login, event: any) {
event.preventDefault(); event.preventDefault();
let resetForm: PasswordReset = { let resetForm = new PasswordReset({
email: i.state.loginForm.username_or_email, email: i.state.loginForm.username_or_email,
}; });
WebSocketService.Instance.send(wsClient.passwordReset(resetForm)); WebSocketService.Instance.send(wsClient.passwordReset(resetForm));
} }
@ -189,13 +194,13 @@ export class Login extends Component<any, State> {
return; return;
} else { } else {
if (op == UserOperation.Login) { if (op == UserOperation.Login) {
let data = wsJsonToRes<LoginResponse>(msg).data; let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
this.state = this.emptyState; this.state = this.emptyState;
this.setState(this.state); this.setState(this.state);
UserService.Instance.login(data); UserService.Instance.login(data);
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.userJoin({ wsClient.userJoin({
auth: authField(), auth: auth().unwrap(),
}) })
); );
toast(i18n.t("logged_in")); toast(i18n.t("logged_in"));
@ -203,8 +208,8 @@ export class Login extends Component<any, State> {
} 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).data; let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
this.state.site_view = data.site_view; this.state.siteRes = data;
this.setState(this.state); this.setState(this.state);
} }
} }

View file

@ -1,11 +1,19 @@
import { None, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Helmet } from "inferno-helmet"; import { Helmet } from "inferno-helmet";
import { LoginResponse, Register, UserOperation } from "lemmy-js-client"; import {
LoginResponse,
Register,
toUndefined,
UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { delay, retryWhen, take } from "rxjs/operators"; import { delay, retryWhen, take } from "rxjs/operators";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { toast, wsClient, wsJsonToRes, wsUserOp } from "../../utils"; import { toast, wsClient } from "../../utils";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import { SiteForm } from "./site-form"; import { SiteForm } from "./site-form";
@ -19,15 +27,18 @@ export class Setup extends Component<any, State> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: State = { private emptyState: State = {
userForm: { userForm: new Register({
username: undefined, username: undefined,
password: undefined, password: undefined,
password_verify: undefined, password_verify: undefined,
show_nsfw: true, show_nsfw: true,
// The first admin signup doesn't need a captcha // The first admin signup doesn't need a captcha
captcha_uuid: "", captcha_uuid: None,
captcha_answer: "", captcha_answer: None,
}, email: None,
honeypot: None,
answer: None,
}),
doneRegisteringUser: false, doneRegisteringUser: false,
userLoading: false, userLoading: false,
}; };
@ -64,7 +75,7 @@ export class Setup extends Component<any, State> {
{!this.state.doneRegisteringUser ? ( {!this.state.doneRegisteringUser ? (
this.registerUser() this.registerUser()
) : ( ) : (
<SiteForm showLocal /> <SiteForm site={None} showLocal />
)} )}
</div> </div>
</div> </div>
@ -104,7 +115,7 @@ export class Setup extends Component<any, State> {
id="email" id="email"
class="form-control" class="form-control"
placeholder={i18n.t("optional")} placeholder={i18n.t("optional")}
value={this.state.userForm.email} value={toUndefined(this.state.userForm.email)}
onInput={linkEvent(this, this.handleRegisterEmailChange)} onInput={linkEvent(this, this.handleRegisterEmailChange)}
minLength={3} minLength={3}
/> />
@ -171,7 +182,7 @@ export class Setup extends Component<any, State> {
} }
handleRegisterEmailChange(i: Setup, event: any) { handleRegisterEmailChange(i: Setup, event: any) {
i.state.userForm.email = event.target.value; i.state.userForm.email = Some(event.target.value);
i.setState(i.state); i.setState(i.state);
} }
@ -193,7 +204,7 @@ export class Setup extends Component<any, State> {
this.setState(this.state); this.setState(this.state);
return; return;
} else if (op == UserOperation.Register) { } else if (op == UserOperation.Register) {
let data = wsJsonToRes<LoginResponse>(msg).data; let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
this.state.userLoading = false; this.state.userLoading = false;
this.state.doneRegisteringUser = true; this.state.doneRegisteringUser = true;
UserService.Instance.login(data); UserService.Instance.login(data);

View file

@ -1,20 +1,25 @@
import { None, Option, Some } from "@sniptt/monads";
import { Options, passwordStrength } from "check-password-strength"; import { Options, passwordStrength } from "check-password-strength";
import { I18nKeys } from "i18next"; import { I18nKeys } from "i18next";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { import {
CaptchaResponse,
GetCaptchaResponse, GetCaptchaResponse,
GetSiteResponse, GetSiteResponse,
LoginResponse, LoginResponse,
Register, Register,
SiteView, SiteView,
toUndefined,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, auth,
isBrowser, isBrowser,
joinLemmyUrl, joinLemmyUrl,
mdToHtml, mdToHtml,
@ -22,9 +27,7 @@ import {
toast, toast,
validEmail, validEmail,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
@ -60,9 +63,9 @@ const passwordStrengthOptions: Options<string> = [
interface State { interface State {
registerForm: Register; registerForm: Register;
registerLoading: boolean; registerLoading: boolean;
captcha: GetCaptchaResponse; captcha: Option<GetCaptchaResponse>;
captchaPlaying: boolean; captchaPlaying: boolean;
site_view: SiteView; siteRes: GetSiteResponse;
} }
export class Signup extends Component<any, State> { export class Signup extends Component<any, State> {
@ -71,20 +74,21 @@ export class Signup extends Component<any, State> {
private audio: HTMLAudioElement; private audio: HTMLAudioElement;
emptyState: State = { emptyState: State = {
registerForm: { registerForm: new Register({
username: undefined, username: undefined,
password: undefined, password: undefined,
password_verify: undefined, password_verify: undefined,
show_nsfw: false, show_nsfw: false,
captcha_uuid: undefined, captcha_uuid: None,
captcha_answer: undefined, captcha_answer: None,
honeypot: undefined, honeypot: None,
answer: undefined, answer: None,
}, email: None,
}),
registerLoading: false, registerLoading: false,
captcha: undefined, captcha: None,
captchaPlaying: false, captchaPlaying: false,
site_view: this.isoData.site_res.site_view, siteRes: this.isoData.site_res,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -108,13 +112,14 @@ export class Signup extends Component<any, State> {
} }
get documentTitle(): string { get documentTitle(): string {
return `${this.titleName} - ${this.state.site_view.site.name}`; return this.state.siteRes.site_view.match({
some: siteView => `${this.titleName(siteView)} - ${siteView.site.name}`,
none: "",
});
} }
get titleName(): string { titleName(siteView: SiteView): string {
return `${i18n.t( return i18n.t(siteView.site.private_instance ? "apply_to_join" : "sign_up");
this.state.site_view.site.private_instance ? "apply_to_join" : "sign_up"
)}`;
} }
get isLemmyMl(): boolean { get isLemmyMl(): boolean {
@ -127,6 +132,8 @@ export class Signup extends Component<any, State> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 offset-lg-3">{this.registerForm()}</div> <div class="col-12 col-lg-6 offset-lg-3">{this.registerForm()}</div>
@ -136,244 +143,272 @@ export class Signup extends Component<any, State> {
} }
registerForm() { registerForm() {
return ( return this.state.siteRes.site_view.match({
<form onSubmit={linkEvent(this, this.handleRegisterSubmit)}> some: siteView => (
<h5>{this.titleName}</h5> <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
<h5>{this.titleName(siteView)}</h5>
{this.isLemmyMl && ( {this.isLemmyMl && (
<div class="form-group row">
<div class="mt-2 mb-0 alert alert-warning" role="alert">
<T i18nKey="lemmy_ml_registration_message">
#<a href={joinLemmyUrl}>#</a>
</T>
</div>
</div>
)}
<div class="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-username">
{i18n.t("username")}
</label>
<div class="col-sm-10">
<input
type="text"
id="register-username"
class="form-control"
value={this.state.registerForm.username}
onInput={linkEvent(this, this.handleRegisterUsernameChange)}
required
minLength={3}
pattern="[a-zA-Z0-9_]+"
title={i18n.t("community_reqs")}
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-email">
{i18n.t("email")}
</label>
<div class="col-sm-10">
<input
type="email"
id="register-email"
class="form-control"
placeholder={
this.state.site_view.site.require_email_verification
? i18n.t("required")
: i18n.t("optional")
}
value={this.state.registerForm.email}
autoComplete="email"
onInput={linkEvent(this, this.handleRegisterEmailChange)}
required={this.state.site_view.site.require_email_verification}
minLength={3}
/>
{!this.state.site_view.site.require_email_verification &&
!validEmail(this.state.registerForm.email) && (
<div class="mt-2 mb-0 alert alert-warning" role="alert">
<Icon icon="alert-triangle" classes="icon-inline mr-2" />
{i18n.t("no_password_reset")}
</div>
)}
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-password">
{i18n.t("password")}
</label>
<div class="col-sm-10">
<input
type="password"
id="register-password"
value={this.state.registerForm.password}
autoComplete="new-password"
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
minLength={10}
maxLength={60}
class="form-control"
required
/>
{this.state.registerForm.password && (
<div class={this.passwordColorClass}>
{i18n.t(this.passwordStrength as I18nKeys)}
</div>
)}
</div>
</div>
<div class="form-group row">
<label
class="col-sm-2 col-form-label"
htmlFor="register-verify-password"
>
{i18n.t("verify_password")}
</label>
<div class="col-sm-10">
<input
type="password"
id="register-verify-password"
value={this.state.registerForm.password_verify}
autoComplete="new-password"
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
maxLength={60}
class="form-control"
required
/>
</div>
</div>
{this.state.site_view.site.require_application && (
<>
<div class="form-group row"> <div class="form-group row">
<div class="offset-sm-2 col-sm-10"> <div class="mt-2 mb-0 alert alert-warning" role="alert">
<div class="mt-2 alert alert-warning" role="alert"> <T i18nKey="lemmy_ml_registration_message">
<Icon icon="alert-triangle" classes="icon-inline mr-2" /> #<a href={joinLemmyUrl}>#</a>
{i18n.t("fill_out_application")} </T>
</div>
<div
className="md-div"
dangerouslySetInnerHTML={mdToHtml(
this.state.site_view.site.application_question || ""
)}
/>
</div> </div>
</div> </div>
)}
<div class="form-group row">
<label
class="col-sm-2 col-form-label"
htmlFor="application_answer"
>
{i18n.t("answer")}
</label>
<div class="col-sm-10">
<MarkdownTextArea
onContentChange={this.handleAnswerChange}
hideNavigationWarnings
/>
</div>
</div>
</>
)}
{this.state.captcha && (
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2" htmlFor="register-captcha"> <label class="col-sm-2 col-form-label" htmlFor="register-username">
<span class="mr-2">{i18n.t("enter_code")}</span> {i18n.t("username")}
<button
type="button"
class="btn btn-secondary"
onClick={linkEvent(this, this.handleRegenCaptcha)}
aria-label={i18n.t("captcha")}
>
<Icon icon="refresh-cw" classes="icon-refresh-cw" />
</button>
</label> </label>
{this.showCaptcha()}
<div class="col-sm-6"> <div class="col-sm-10">
<input <input
type="text" type="text"
id="register-username"
class="form-control" class="form-control"
id="register-captcha" value={this.state.registerForm.username}
value={this.state.registerForm.captcha_answer} onInput={linkEvent(this, this.handleRegisterUsernameChange)}
required
minLength={3}
pattern="[a-zA-Z0-9_]+"
title={i18n.t("community_reqs")}
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-email">
{i18n.t("email")}
</label>
<div class="col-sm-10">
<input
type="email"
id="register-email"
class="form-control"
placeholder={
siteView.site.require_email_verification
? i18n.t("required")
: i18n.t("optional")
}
value={toUndefined(this.state.registerForm.email)}
autoComplete="email"
onInput={linkEvent(this, this.handleRegisterEmailChange)}
required={siteView.site.require_email_verification}
minLength={3}
/>
{!siteView.site.require_email_verification &&
!this.state.registerForm.email
.map(validEmail)
.unwrapOr(true) && (
<div class="mt-2 mb-0 alert alert-warning" role="alert">
<Icon icon="alert-triangle" classes="icon-inline mr-2" />
{i18n.t("no_password_reset")}
</div>
)}
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="register-password">
{i18n.t("password")}
</label>
<div class="col-sm-10">
<input
type="password"
id="register-password"
value={this.state.registerForm.password}
autoComplete="new-password"
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
minLength={10}
maxLength={60}
class="form-control"
required
/>
{this.state.registerForm.password && (
<div class={this.passwordColorClass}>
{i18n.t(this.passwordStrength as I18nKeys)}
</div>
)}
</div>
</div>
<div class="form-group row">
<label
class="col-sm-2 col-form-label"
htmlFor="register-verify-password"
>
{i18n.t("verify_password")}
</label>
<div class="col-sm-10">
<input
type="password"
id="register-verify-password"
value={this.state.registerForm.password_verify}
autoComplete="new-password"
onInput={linkEvent( onInput={linkEvent(
this, this,
this.handleRegisterCaptchaAnswerChange this.handleRegisterPasswordVerifyChange
)} )}
maxLength={60}
class="form-control"
required required
/> />
</div> </div>
</div> </div>
)}
{this.state.site_view.site.enable_nsfw && ( {siteView.site.require_application && (
<div class="form-group row"> <>
<div class="col-sm-10"> <div class="form-group row">
<div class="form-check"> <div class="offset-sm-2 col-sm-10">
<input <div class="mt-2 alert alert-warning" role="alert">
class="form-check-input" <Icon icon="alert-triangle" classes="icon-inline mr-2" />
id="register-show-nsfw" {i18n.t("fill_out_application")}
type="checkbox" </div>
checked={this.state.registerForm.show_nsfw} {siteView.site.application_question.match({
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)} some: question => (
/> <div
<label class="form-check-label" htmlFor="register-show-nsfw"> className="md-div"
{i18n.t("show_nsfw")} dangerouslySetInnerHTML={mdToHtml(question)}
/>
),
none: <></>,
})}
</div>
</div>
<div class="form-group row">
<label
class="col-sm-2 col-form-label"
htmlFor="application_answer"
>
{i18n.t("answer")}
</label> </label>
<div class="col-sm-10">
<MarkdownTextArea
initialContent={None}
placeholder={None}
buttonTitle={None}
maxLength={None}
onContentChange={this.handleAnswerChange}
hideNavigationWarnings
/>
</div>
</div>
</>
)}
{this.state.captcha.isSome() && (
<div class="form-group row">
<label class="col-sm-2" htmlFor="register-captcha">
<span class="mr-2">{i18n.t("enter_code")}</span>
<button
type="button"
class="btn btn-secondary"
onClick={linkEvent(this, this.handleRegenCaptcha)}
aria-label={i18n.t("captcha")}
>
<Icon icon="refresh-cw" classes="icon-refresh-cw" />
</button>
</label>
{this.showCaptcha()}
<div class="col-sm-6">
<input
type="text"
class="form-control"
id="register-captcha"
value={toUndefined(this.state.registerForm.captcha_answer)}
onInput={linkEvent(
this,
this.handleRegisterCaptchaAnswerChange
)}
required
/>
</div> </div>
</div> </div>
)}
{siteView.site.enable_nsfw && (
<div class="form-group row">
<div class="col-sm-10">
<div class="form-check">
<input
class="form-check-input"
id="register-show-nsfw"
type="checkbox"
checked={this.state.registerForm.show_nsfw}
onChange={linkEvent(
this,
this.handleRegisterShowNsfwChange
)}
/>
<label class="form-check-label" htmlFor="register-show-nsfw">
{i18n.t("show_nsfw")}
</label>
</div>
</div>
</div>
)}
<input
tabIndex={-1}
autoComplete="false"
name="a_password"
type="text"
class="form-control honeypot"
id="register-honey"
value={toUndefined(this.state.registerForm.honeypot)}
onInput={linkEvent(this, this.handleHoneyPotChange)}
/>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary">
{this.state.registerLoading ? (
<Spinner />
) : (
this.titleName(siteView)
)}
</button>
</div>
</div> </div>
)} </form>
<input ),
tabIndex={-1} none: <></>,
autoComplete="false" });
name="a_password"
type="text"
class="form-control honeypot"
id="register-honey"
value={this.state.registerForm.honeypot}
onInput={linkEvent(this, this.handleHoneyPotChange)}
/>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary">
{this.state.registerLoading ? <Spinner /> : this.titleName}
</button>
</div>
</div>
</form>
);
} }
showCaptcha() { showCaptcha() {
return ( return this.state.captcha.match({
<div class="col-sm-4"> some: captcha => (
{this.state.captcha.ok && ( <div class="col-sm-4">
<> {captcha.ok.match({
<img some: res => (
class="rounded-top img-fluid" <>
src={this.captchaPngSrc()} <img
style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;" class="rounded-top img-fluid"
alt={i18n.t("captcha")} src={this.captchaPngSrc(res)}
/> style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
{this.state.captcha.ok.wav && ( alt={i18n.t("captcha")}
<button />
class="rounded-bottom btn btn-sm btn-secondary btn-block" {res.wav.isSome() && (
style="border-top-right-radius: 0; border-top-left-radius: 0;" <button
title={i18n.t("play_captcha_audio")} class="rounded-bottom btn btn-sm btn-secondary btn-block"
onClick={linkEvent(this, this.handleCaptchaPlay)} style="border-top-right-radius: 0; border-top-left-radius: 0;"
type="button" title={i18n.t("play_captcha_audio")}
disabled={this.state.captchaPlaying} onClick={linkEvent(this, this.handleCaptchaPlay)}
> type="button"
<Icon icon="play" classes="icon-play" /> disabled={this.state.captchaPlaying}
</button> >
)} <Icon icon="play" classes="icon-play" />
</> </button>
)} )}
</div> </>
); ),
none: <></>,
})}
</div>
),
none: <></>,
});
} }
get passwordStrength() { get passwordStrength() {
@ -408,9 +443,9 @@ export class Signup extends Component<any, State> {
} }
handleRegisterEmailChange(i: Signup, event: any) { handleRegisterEmailChange(i: Signup, event: any) {
i.state.registerForm.email = event.target.value; i.state.registerForm.email = Some(event.target.value);
if (i.state.registerForm.email == "") { if (i.state.registerForm.email.unwrap() == "") {
i.state.registerForm.email = undefined; i.state.registerForm.email = None;
} }
i.setState(i.state); i.setState(i.state);
} }
@ -431,17 +466,17 @@ export class Signup extends Component<any, State> {
} }
handleRegisterCaptchaAnswerChange(i: Signup, event: any) { handleRegisterCaptchaAnswerChange(i: Signup, event: any) {
i.state.registerForm.captcha_answer = event.target.value; i.state.registerForm.captcha_answer = Some(event.target.value);
i.setState(i.state); i.setState(i.state);
} }
handleAnswerChange(val: string) { handleAnswerChange(val: string) {
this.state.registerForm.answer = val; this.state.registerForm.answer = Some(val);
this.setState(this.state); this.setState(this.state);
} }
handleHoneyPotChange(i: Signup, event: any) { handleHoneyPotChange(i: Signup, event: any) {
i.state.registerForm.honeypot = event.target.value; i.state.registerForm.honeypot = Some(event.target.value);
i.setState(i.state); i.setState(i.state);
} }
@ -455,25 +490,34 @@ export class Signup extends Component<any, State> {
handleCaptchaPlay(i: Signup) { handleCaptchaPlay(i: Signup) {
// This was a bad bug, it should only build the new audio on a new file. // This was a bad bug, it should only build the new audio on a new file.
// Replays would stop prematurely if this was rebuilt every time. // Replays would stop prematurely if this was rebuilt every time.
if (i.audio == null) { i.state.captcha.match({
let base64 = `data:audio/wav;base64,${i.state.captcha.ok.wav}`; some: captcha =>
i.audio = new Audio(base64); captcha.ok.match({
} some: res => {
if (i.audio == null) {
let base64 = `data:audio/wav;base64,${res.wav}`;
i.audio = new Audio(base64);
}
i.audio.play(); i.audio.play();
i.state.captchaPlaying = true; i.state.captchaPlaying = true;
i.setState(i.state); 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.state.captchaPlaying = false;
i.setState(i.state); i.setState(i.state);
});
},
none: void 0,
}),
none: void 0,
}); });
} }
captchaPngSrc() { captchaPngSrc(captcha: CaptchaResponse) {
return `data:image/png;base64,${this.state.captcha.ok.png}`; return `data:image/png;base64,${captcha.png}`;
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -489,7 +533,7 @@ export class Signup extends Component<any, State> {
return; return;
} else { } else {
if (op == UserOperation.Register) { if (op == UserOperation.Register) {
let data = wsJsonToRes<LoginResponse>(msg).data; let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
this.state = this.emptyState; this.state = this.emptyState;
this.setState(this.state); this.setState(this.state);
// Only log them in if a jwt was set // Only log them in if a jwt was set
@ -497,7 +541,7 @@ export class Signup extends Component<any, State> {
UserService.Instance.login(data); UserService.Instance.login(data);
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.userJoin({ wsClient.userJoin({
auth: authField(), auth: auth().unwrap(),
}) })
); );
this.props.history.push("/communities"); this.props.history.push("/communities");
@ -511,17 +555,20 @@ export class Signup extends Component<any, State> {
this.props.history.push("/"); this.props.history.push("/");
} }
} else if (op == UserOperation.GetCaptcha) { } else if (op == UserOperation.GetCaptcha) {
let data = wsJsonToRes<GetCaptchaResponse>(msg).data; let data = wsJsonToRes<GetCaptchaResponse>(msg, GetCaptchaResponse);
if (data.ok) { data.ok.match({
this.state.captcha = data; some: res => {
this.state.registerForm.captcha_uuid = data.ok.uuid; this.state.captcha = Some(data);
this.setState(this.state); this.state.registerForm.captcha_uuid = Some(res.uuid);
} this.setState(this.state);
},
none: void 0,
});
} 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).data; let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
this.state.site_view = data.site_view; this.state.siteRes = data;
this.setState(this.state); this.setState(this.state);
} }
} }

View file

@ -1,10 +1,17 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Prompt } from "inferno-router"; import { Prompt } from "inferno-router";
import { CreateSite, EditSite, ListingType, Site } from "lemmy-js-client"; import {
CreateSite,
EditSite,
ListingType,
Site,
toUndefined,
} from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { import {
authField, auth,
capitalizeFirstLetter, capitalizeFirstLetter,
fetchThemeList, fetchThemeList,
wsClient, wsClient,
@ -15,38 +22,41 @@ import { ListingTypeSelect } from "../common/listing-type-select";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
interface SiteFormProps { interface SiteFormProps {
site?: Site; // If a site is given, that means this is an edit site: Option<Site>; // If a site is given, that means this is an edit
showLocal: boolean; showLocal?: boolean;
onCancel?(): any; onCancel?(): void;
onEdit?(): any; onEdit?(): void;
} }
interface SiteFormState { interface SiteFormState {
siteForm: EditSite; siteForm: EditSite;
loading: boolean; loading: boolean;
themeList: string[]; themeList: Option<string[]>;
} }
export class SiteForm extends Component<SiteFormProps, SiteFormState> { export class SiteForm extends Component<SiteFormProps, SiteFormState> {
private emptyState: SiteFormState = { private emptyState: SiteFormState = {
siteForm: { siteForm: new EditSite({
enable_downvotes: true, enable_downvotes: Some(true),
open_registration: true, open_registration: Some(true),
enable_nsfw: true, enable_nsfw: Some(true),
name: null, name: None,
icon: null, icon: None,
banner: null, banner: None,
require_email_verification: null, require_email_verification: None,
require_application: null, require_application: None,
application_question: null, application_question: None,
private_instance: null, private_instance: None,
default_theme: null, default_theme: None,
default_post_listing_type: null, sidebar: None,
legal_information: null, default_post_listing_type: None,
auth: authField(false), legal_information: None,
}, description: None,
community_creation_admin_only: None,
auth: undefined,
}),
loading: false, loading: false,
themeList: [], themeList: None,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -67,32 +77,36 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
this.handleDefaultPostListingTypeChange = this.handleDefaultPostListingTypeChange =
this.handleDefaultPostListingTypeChange.bind(this); this.handleDefaultPostListingTypeChange.bind(this);
if (this.props.site) { this.props.site.match({
let site = this.props.site; some: site => {
this.state.siteForm = { this.state.siteForm = new EditSite({
name: site.name, name: Some(site.name),
sidebar: site.sidebar, sidebar: site.sidebar,
description: site.description, description: site.description,
enable_downvotes: site.enable_downvotes, enable_downvotes: Some(site.enable_downvotes),
open_registration: site.open_registration, open_registration: Some(site.open_registration),
enable_nsfw: site.enable_nsfw, enable_nsfw: Some(site.enable_nsfw),
community_creation_admin_only: site.community_creation_admin_only, community_creation_admin_only: Some(
icon: site.icon, site.community_creation_admin_only
banner: site.banner, ),
require_email_verification: site.require_email_verification, icon: site.icon,
require_application: site.require_application, banner: site.banner,
application_question: site.application_question, require_email_verification: Some(site.require_email_verification),
private_instance: site.private_instance, require_application: Some(site.require_application),
default_theme: site.default_theme, application_question: site.application_question,
default_post_listing_type: site.default_post_listing_type, private_instance: Some(site.private_instance),
legal_information: site.legal_information, default_theme: Some(site.default_theme),
auth: authField(false), default_post_listing_type: Some(site.default_post_listing_type),
}; legal_information: site.legal_information,
} auth: auth(false).unwrap(),
});
},
none: void 0,
});
} }
async componentDidMount() { async componentDidMount() {
this.state.themeList = await fetchThemeList(); this.state.themeList = Some(await fetchThemeList());
this.setState(this.state); this.setState(this.state);
} }
@ -105,7 +119,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
componentDidUpdate() { componentDidUpdate() {
if ( if (
!this.state.loading && !this.state.loading &&
!this.props.site && this.props.site.isNone() &&
(this.state.siteForm.name || (this.state.siteForm.name ||
this.state.siteForm.sidebar || this.state.siteForm.sidebar ||
this.state.siteForm.application_question || this.state.siteForm.application_question ||
@ -127,7 +141,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
<Prompt <Prompt
when={ when={
!this.state.loading && !this.state.loading &&
!this.props.site && this.props.site.isNone() &&
(this.state.siteForm.name || (this.state.siteForm.name ||
this.state.siteForm.sidebar || this.state.siteForm.sidebar ||
this.state.siteForm.application_question || this.state.siteForm.application_question ||
@ -137,7 +151,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
/> />
<form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}> <form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
<h5>{`${ <h5>{`${
this.props.site this.props.site.isSome()
? 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>
@ -150,7 +164,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
type="text" type="text"
id="create-site-name" id="create-site-name"
class="form-control" class="form-control"
value={this.state.siteForm.name} value={toUndefined(this.state.siteForm.name)}
onInput={linkEvent(this, this.handleSiteNameChange)} onInput={linkEvent(this, this.handleSiteNameChange)}
required required
minLength={3} minLength={3}
@ -186,7 +200,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
type="text" type="text"
class="form-control" class="form-control"
id="site-desc" id="site-desc"
value={this.state.siteForm.description} value={toUndefined(this.state.siteForm.description)}
onInput={linkEvent(this, this.handleSiteDescChange)} onInput={linkEvent(this, this.handleSiteDescChange)}
maxLength={150} maxLength={150}
/> />
@ -197,6 +211,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
<div class="col-12"> <div class="col-12">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.siteForm.sidebar} initialContent={this.state.siteForm.sidebar}
placeholder={None}
buttonTitle={None}
maxLength={None}
onContentChange={this.handleSiteSidebarChange} onContentChange={this.handleSiteSidebarChange}
hideNavigationWarnings hideNavigationWarnings
/> />
@ -209,12 +226,15 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
<div class="col-12"> <div class="col-12">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.siteForm.legal_information} initialContent={this.state.siteForm.legal_information}
placeholder={None}
buttonTitle={None}
maxLength={None}
onContentChange={this.handleSiteLegalInfoChange} onContentChange={this.handleSiteLegalInfoChange}
hideNavigationWarnings hideNavigationWarnings
/> />
</div> </div>
</div> </div>
{this.state.siteForm.require_application && ( {this.state.siteForm.require_application.unwrapOr(false) && (
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label"> <label class="col-12 col-form-label">
{i18n.t("application_questionnaire")} {i18n.t("application_questionnaire")}
@ -222,6 +242,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
<div class="col-12"> <div class="col-12">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.siteForm.application_question} initialContent={this.state.siteForm.application_question}
placeholder={None}
buttonTitle={None}
maxLength={None}
onContentChange={this.handleSiteApplicationQuestionChange} onContentChange={this.handleSiteApplicationQuestionChange}
hideNavigationWarnings hideNavigationWarnings
/> />
@ -235,7 +258,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
class="form-check-input" class="form-check-input"
id="create-site-downvotes" id="create-site-downvotes"
type="checkbox" type="checkbox"
checked={this.state.siteForm.enable_downvotes} checked={toUndefined(this.state.siteForm.enable_downvotes)}
onChange={linkEvent( onChange={linkEvent(
this, this,
this.handleSiteEnableDownvotesChange this.handleSiteEnableDownvotesChange
@ -254,7 +277,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
class="form-check-input" class="form-check-input"
id="create-site-enable-nsfw" id="create-site-enable-nsfw"
type="checkbox" type="checkbox"
checked={this.state.siteForm.enable_nsfw} checked={toUndefined(this.state.siteForm.enable_nsfw)}
onChange={linkEvent(this, this.handleSiteEnableNsfwChange)} onChange={linkEvent(this, this.handleSiteEnableNsfwChange)}
/> />
<label <label
@ -273,7 +296,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
class="form-check-input" class="form-check-input"
id="create-site-open-registration" id="create-site-open-registration"
type="checkbox" type="checkbox"
checked={this.state.siteForm.open_registration} checked={toUndefined(this.state.siteForm.open_registration)}
onChange={linkEvent( onChange={linkEvent(
this, this,
this.handleSiteOpenRegistrationChange this.handleSiteOpenRegistrationChange
@ -295,7 +318,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
class="form-check-input" class="form-check-input"
id="create-site-community-creation-admin-only" id="create-site-community-creation-admin-only"
type="checkbox" type="checkbox"
checked={this.state.siteForm.community_creation_admin_only} checked={toUndefined(
this.state.siteForm.community_creation_admin_only
)}
onChange={linkEvent( onChange={linkEvent(
this, this,
this.handleSiteCommunityCreationAdminOnly this.handleSiteCommunityCreationAdminOnly
@ -317,7 +342,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
class="form-check-input" class="form-check-input"
id="create-site-require-email-verification" id="create-site-require-email-verification"
type="checkbox" type="checkbox"
checked={this.state.siteForm.require_email_verification} checked={toUndefined(
this.state.siteForm.require_email_verification
)}
onChange={linkEvent( onChange={linkEvent(
this, this,
this.handleSiteRequireEmailVerification this.handleSiteRequireEmailVerification
@ -339,7 +366,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
class="form-check-input" class="form-check-input"
id="create-site-require-application" id="create-site-require-application"
type="checkbox" type="checkbox"
checked={this.state.siteForm.require_application} checked={toUndefined(this.state.siteForm.require_application)}
onChange={linkEvent(this, this.handleSiteRequireApplication)} onChange={linkEvent(this, this.handleSiteRequireApplication)}
/> />
<label <label
@ -361,12 +388,12 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</label> </label>
<select <select
id="create-site-default-theme" id="create-site-default-theme"
value={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" class="custom-select w-auto"
> >
<option value="browser">{i18n.t("browser_default")}</option> <option value="browser">{i18n.t("browser_default")}</option>
{this.state.themeList.map(theme => ( {this.state.themeList.unwrapOr([]).map(theme => (
<option value={theme}>{theme}</option> <option value={theme}>{theme}</option>
))} ))}
</select> </select>
@ -378,7 +405,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
<div class="col-sm-9"> <div class="col-sm-9">
<ListingTypeSelect <ListingTypeSelect
type_={ type_={
ListingType[this.state.siteForm.default_post_listing_type] ListingType[
this.state.siteForm.default_post_listing_type.unwrapOr(
"Local"
)
]
} }
showLocal showLocal
showSubscribed={false} showSubscribed={false}
@ -394,7 +425,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
class="form-check-input" class="form-check-input"
id="create-site-private-instance" id="create-site-private-instance"
type="checkbox" type="checkbox"
value={this.state.siteForm.default_theme} value={toUndefined(this.state.siteForm.default_theme)}
onChange={linkEvent(this, this.handleSitePrivateInstance)} onChange={linkEvent(this, this.handleSitePrivateInstance)}
/> />
<label <label
@ -415,13 +446,13 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
> >
{this.state.loading ? ( {this.state.loading ? (
<Spinner /> <Spinner />
) : this.props.site ? ( ) : this.props.site.isSome() ? (
capitalizeFirstLetter(i18n.t("save")) capitalizeFirstLetter(i18n.t("save"))
) : ( ) : (
capitalizeFirstLetter(i18n.t("create")) capitalizeFirstLetter(i18n.t("create"))
)} )}
</button> </button>
{this.props.site && ( {this.props.site.isSome() && (
<button <button
type="button" type="button"
class="btn btn-secondary" class="btn btn-secondary"
@ -440,81 +471,98 @@ 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.state.loading = true;
if (i.props.site) { i.state.siteForm.auth = auth().unwrap();
if (i.props.site.isSome()) {
WebSocketService.Instance.send(wsClient.editSite(i.state.siteForm)); WebSocketService.Instance.send(wsClient.editSite(i.state.siteForm));
i.props.onEdit(); i.props.onEdit();
} else { } else {
let form: CreateSite = { let sForm = i.state.siteForm;
name: i.state.siteForm.name || "My site", let form = new CreateSite({
...i.state.siteForm, name: sForm.name.unwrapOr("My site"),
}; sidebar: sForm.sidebar,
description: sForm.description,
icon: sForm.icon,
banner: sForm.banner,
community_creation_admin_only: sForm.community_creation_admin_only,
enable_nsfw: sForm.enable_nsfw,
enable_downvotes: sForm.enable_downvotes,
require_application: sForm.require_application,
application_question: sForm.application_question,
open_registration: sForm.open_registration,
require_email_verification: sForm.require_email_verification,
private_instance: sForm.private_instance,
default_theme: sForm.default_theme,
default_post_listing_type: sForm.default_post_listing_type,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.createSite(form)); WebSocketService.Instance.send(wsClient.createSite(form));
} }
i.setState(i.state); i.setState(i.state);
} }
handleSiteNameChange(i: SiteForm, event: any) { handleSiteNameChange(i: SiteForm, event: any) {
i.state.siteForm.name = event.target.value; i.state.siteForm.name = Some(event.target.value);
i.setState(i.state); i.setState(i.state);
} }
handleSiteSidebarChange(val: string) { handleSiteSidebarChange(val: string) {
this.state.siteForm.sidebar = val; this.state.siteForm.sidebar = Some(val);
this.setState(this.state); this.setState(this.state);
} }
handleSiteLegalInfoChange(val: string) { handleSiteLegalInfoChange(val: string) {
this.state.siteForm.legal_information = val; this.state.siteForm.legal_information = Some(val);
this.setState(this.state); this.setState(this.state);
} }
handleSiteApplicationQuestionChange(val: string) { handleSiteApplicationQuestionChange(val: string) {
this.state.siteForm.application_question = val; this.state.siteForm.application_question = Some(val);
this.setState(this.state); this.setState(this.state);
} }
handleSiteDescChange(i: SiteForm, event: any) { handleSiteDescChange(i: SiteForm, event: any) {
i.state.siteForm.description = event.target.value; i.state.siteForm.description = Some(event.target.value);
i.setState(i.state); i.setState(i.state);
} }
handleSiteEnableNsfwChange(i: SiteForm, event: any) { handleSiteEnableNsfwChange(i: SiteForm, event: any) {
i.state.siteForm.enable_nsfw = event.target.checked; i.state.siteForm.enable_nsfw = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
} }
handleSiteOpenRegistrationChange(i: SiteForm, event: any) { handleSiteOpenRegistrationChange(i: SiteForm, event: any) {
i.state.siteForm.open_registration = event.target.checked; i.state.siteForm.open_registration = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
} }
handleSiteCommunityCreationAdminOnly(i: SiteForm, event: any) { handleSiteCommunityCreationAdminOnly(i: SiteForm, event: any) {
i.state.siteForm.community_creation_admin_only = event.target.checked; i.state.siteForm.community_creation_admin_only = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
} }
handleSiteEnableDownvotesChange(i: SiteForm, event: any) { handleSiteEnableDownvotesChange(i: SiteForm, event: any) {
i.state.siteForm.enable_downvotes = event.target.checked; i.state.siteForm.enable_downvotes = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
} }
handleSiteRequireApplication(i: SiteForm, event: any) { handleSiteRequireApplication(i: SiteForm, event: any) {
i.state.siteForm.require_application = event.target.checked; i.state.siteForm.require_application = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
} }
handleSiteRequireEmailVerification(i: SiteForm, event: any) { handleSiteRequireEmailVerification(i: SiteForm, event: any) {
i.state.siteForm.require_email_verification = event.target.checked; i.state.siteForm.require_email_verification = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
} }
handleSitePrivateInstance(i: SiteForm, event: any) { handleSitePrivateInstance(i: SiteForm, event: any) {
i.state.siteForm.private_instance = event.target.checked; i.state.siteForm.private_instance = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
} }
handleSiteDefaultTheme(i: SiteForm, event: any) { handleSiteDefaultTheme(i: SiteForm, event: any) {
i.state.siteForm.default_theme = event.target.value; i.state.siteForm.default_theme = Some(event.target.value);
i.setState(i.state); i.setState(i.state);
} }
@ -523,28 +571,29 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
} }
handleIconUpload(url: string) { handleIconUpload(url: string) {
this.state.siteForm.icon = url; this.state.siteForm.icon = Some(url);
this.setState(this.state); this.setState(this.state);
} }
handleIconRemove() { handleIconRemove() {
this.state.siteForm.icon = ""; this.state.siteForm.icon = Some("");
this.setState(this.state); this.setState(this.state);
} }
handleBannerUpload(url: string) { handleBannerUpload(url: string) {
this.state.siteForm.banner = url; this.state.siteForm.banner = Some(url);
this.setState(this.state); this.setState(this.state);
} }
handleBannerRemove() { handleBannerRemove() {
this.state.siteForm.banner = ""; this.state.siteForm.banner = Some("");
this.setState(this.state); this.setState(this.state);
} }
handleDefaultPostListingTypeChange(val: ListingType) { handleDefaultPostListingTypeChange(val: ListingType) {
this.state.siteForm.default_post_listing_type = this.state.siteForm.default_post_listing_type = Some(
ListingType[ListingType[val]]; ListingType[ListingType[val]]
);
this.setState(this.state); this.setState(this.state);
} }
} }

View file

@ -1,9 +1,9 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { PersonViewSafe, Site, SiteAggregates } from "lemmy-js-client"; import { PersonViewSafe, Site, SiteAggregates } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService } from "../../services"; import { amAdmin, mdToHtml, numToSI } from "../../utils";
import { mdToHtml, numToSI } from "../../utils";
import { BannerIconHeader } from "../common/banner-icon-header"; import { BannerIconHeader } from "../common/banner-icon-header";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
@ -12,9 +12,9 @@ import { SiteForm } from "./site-form";
interface SiteSidebarProps { interface SiteSidebarProps {
site: Site; site: Site;
showLocal: boolean; showLocal: boolean;
counts?: SiteAggregates; counts: Option<SiteAggregates>;
admins?: PersonViewSafe[]; admins: Option<PersonViewSafe[]>;
online?: number; online: Option<number>;
} }
interface SiteSidebarState { interface SiteSidebarState {
@ -44,18 +44,18 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
<div> <div>
<div class="mb-2"> <div class="mb-2">
{this.siteName()} {this.siteName()}
{this.props.admins && this.adminButtons()} {this.props.admins.isSome() && this.adminButtons()}
</div> </div>
{!this.state.collapsed && ( {!this.state.collapsed && (
<> <>
<BannerIconHeader banner={site.banner} /> <BannerIconHeader banner={site.banner} icon={None} />
{this.siteInfo()} {this.siteInfo()}
</> </>
)} )}
</div> </div>
) : ( ) : (
<SiteForm <SiteForm
site={site} site={Some(site)}
showLocal={this.props.showLocal} showLocal={this.props.showLocal}
onEdit={this.handleEditSite} onEdit={this.handleEditSite}
onCancel={this.handleEditCancel} onCancel={this.handleEditCancel}
@ -69,23 +69,21 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
siteName() { siteName() {
let site = this.props.site; let site = this.props.site;
return ( return (
site.name && ( <h5 class="mb-0 d-inline">
<h5 class="mb-0 d-inline"> {site.name}
{site.name} <button
<button class="btn btn-sm text-muted"
class="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")} >
> {this.state.collapsed ? (
{this.state.collapsed ? ( <Icon icon="plus-square" classes="icon-inline" />
<Icon icon="plus-square" classes="icon-inline" /> ) : (
) : ( <Icon icon="minus-square" classes="icon-inline" />
<Icon icon="minus-square" classes="icon-inline" /> )}
)} </button>
</button> </h5>
</h5>
)
); );
} }
@ -93,17 +91,29 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
let site = this.props.site; let site = this.props.site;
return ( return (
<div> <div>
{site.description && <h6>{site.description}</h6>} {site.description.match({
{site.sidebar && this.siteSidebar()} some: description => <h6>{description}</h6>,
{this.props.counts && this.badges()} none: <></>,
{this.props.admins && this.admins()} })}
{site.sidebar.match({
some: sidebar => this.siteSidebar(sidebar),
none: <></>,
})}
{this.props.counts.match({
some: counts => this.badges(counts),
none: <></>,
})}
{this.props.admins.match({
some: admins => this.admins(admins),
none: <></>,
})}
</div> </div>
); );
} }
adminButtons() { adminButtons() {
return ( return (
this.canAdmin && ( amAdmin(this.props.admins) && (
<ul class="list-inline mb-1 text-muted font-weight-bold"> <ul class="list-inline mb-1 text-muted font-weight-bold">
<li className="list-inline-item-action"> <li className="list-inline-item-action">
<button <button
@ -120,20 +130,17 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
); );
} }
siteSidebar() { siteSidebar(sidebar: string) {
return ( return (
<div <div className="md-div" dangerouslySetInnerHTML={mdToHtml(sidebar)} />
className="md-div"
dangerouslySetInnerHTML={mdToHtml(this.props.site.sidebar)}
/>
); );
} }
admins() { admins(admins: PersonViewSafe[]) {
return ( return (
<ul class="mt-1 list-inline small mb-0"> <ul class="mt-1 list-inline small mb-0">
<li class="list-inline-item">{i18n.t("admins")}:</li> <li class="list-inline-item">{i18n.t("admins")}:</li>
{this.props.admins?.map(av => ( {admins.map(av => (
<li class="list-inline-item"> <li class="list-inline-item">
<PersonListing person={av.person} /> <PersonListing person={av.person} />
</li> </li>
@ -142,9 +149,9 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
); );
} }
badges() { badges(siteAggregates: SiteAggregates) {
let counts = this.props.counts; let counts = siteAggregates;
let online = this.props.online; let online = this.props.online.unwrapOr(1);
return ( return (
<ul class="my-2 list-inline"> <ul class="my-2 list-inline">
<li className="list-inline-item badge badge-secondary"> <li className="list-inline-item badge badge-secondary">
@ -238,15 +245,6 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
); );
} }
get canAdmin(): boolean {
return (
UserService.Instance.myUserInfo &&
this.props.admins
.map(a => a.person.id)
.includes(UserService.Instance.myUserInfo.local_user_view.person.id)
);
}
handleCollapseSidebar(i: SiteSidebar) { handleCollapseSidebar(i: SiteSidebar) {
i.state.collapsed = !i.state.collapsed; i.state.collapsed = !i.state.collapsed;
i.setState(i.state); i.setState(i.state);

View file

@ -1,3 +1,4 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { Link } from "inferno-router"; import { Link } from "inferno-router";
import { import {
@ -6,6 +7,7 @@ import {
GetCommunityResponse, GetCommunityResponse,
GetModlog, GetModlog,
GetModlogResponse, GetModlogResponse,
GetSiteResponse,
ModAddCommunityView, ModAddCommunityView,
ModAddView, ModAddView,
ModBanFromCommunityView, ModBanFromCommunityView,
@ -17,25 +19,25 @@ import {
ModStickyPostView, ModStickyPostView,
ModTransferCommunityView, ModTransferCommunityView,
PersonSafe, PersonSafe,
SiteView,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import moment from "moment"; import moment from "moment";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../i18next"; import { i18n } from "../i18next";
import { InitialFetchRequest } from "../interfaces"; import { InitialFetchRequest } from "../interfaces";
import { UserService, WebSocketService } from "../services"; import { WebSocketService } from "../services";
import { import {
authField, amAdmin,
amMod,
auth,
fetchLimit, fetchLimit,
isBrowser, isBrowser,
setIsoData, setIsoData,
setOptionalAuth,
toast, toast,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../utils"; } from "../utils";
import { HtmlTags } from "./common/html-tags"; import { HtmlTags } from "./common/html-tags";
import { Spinner } from "./common/icon"; import { Spinner } from "./common/icon";
@ -75,34 +77,28 @@ type ModlogType = {
}; };
interface ModlogState { interface ModlogState {
res: GetModlogResponse; res: Option<GetModlogResponse>;
communityId?: number; communityId: Option<number>;
communityName?: string; communityMods: Option<CommunityModeratorView[]>;
communityMods?: CommunityModeratorView[];
page: number; page: number;
site_view: SiteView; siteRes: GetSiteResponse;
loading: boolean; loading: boolean;
} }
export class Modlog extends Component<any, ModlogState> { export class Modlog extends Component<any, ModlogState> {
private isoData = setIsoData(this.context); private isoData = setIsoData(
this.context,
GetModlogResponse,
GetCommunityResponse
);
private subscription: Subscription; private subscription: Subscription;
private emptyState: ModlogState = { private emptyState: ModlogState = {
res: { res: None,
removed_posts: [], communityId: None,
locked_posts: [], communityMods: None,
stickied_posts: [],
removed_comments: [],
removed_communities: [],
banned_from_community: [],
banned: [],
added_to_community: [],
transferred_to_community: [],
added: [],
},
page: 1, page: 1,
loading: true, loading: true,
site_view: this.isoData.site_res.site_view, siteRes: this.isoData.site_res,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -112,22 +108,23 @@ export class Modlog extends Component<any, ModlogState> {
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
this.state.communityId = this.props.match.params.community_id this.state.communityId = this.props.match.params.community_id
? Number(this.props.match.params.community_id) ? Some(Number(this.props.match.params.community_id))
: undefined; : None;
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); 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) {
let data = this.isoData.routeData[0]; this.state.res = Some(this.isoData.routeData[0] as GetModlogResponse);
this.state.res = data;
this.state.loading = false;
// Getting the moderators // Getting the moderators
if (this.isoData.routeData[1]) { let communityRes = Some(
this.state.communityMods = this.isoData.routeData[1].moderators; this.isoData.routeData[1] as GetCommunityResponse
} );
this.state.communityMods = communityRes.map(c => c.moderators);
this.state.loading = false;
} else { } else {
this.refetch(); this.refetch();
} }
@ -226,12 +223,6 @@ export class Modlog extends Component<any, ModlogState> {
combined.push(...added); combined.push(...added);
combined.push(...banned); combined.push(...banned);
if (this.state.communityId && combined.length > 0) {
this.state.communityName = (
combined[0].view as ModRemovePostView
).community.name;
}
// Sort them by time // Sort them by time
combined.sort((a, b) => b.when_.localeCompare(a.when_)); combined.sort((a, b) => b.when_.localeCompare(a.when_));
@ -294,11 +285,11 @@ export class Modlog extends Component<any, ModlogState> {
<span> <span>
Community <CommunityLink community={mrco.community} /> Community <CommunityLink community={mrco.community} />
</span>, </span>,
mrco.mod_remove_community.reason && mrco.mod_remove_community.reason.isSome() &&
` reason: ${mrco.mod_remove_community.reason}`, ` reason: ${mrco.mod_remove_community.reason.unwrap()}`,
mrco.mod_remove_community.expires && mrco.mod_remove_community.expires.isSome() &&
` expires: ${moment ` expires: ${moment
.utc(mrco.mod_remove_community.expires) .utc(mrco.mod_remove_community.expires.unwrap())
.fromNow()}`, .fromNow()}`,
]; ];
} }
@ -316,13 +307,13 @@ export class Modlog extends Component<any, ModlogState> {
<CommunityLink community={mbfc.community} /> <CommunityLink community={mbfc.community} />
</span>, </span>,
<div> <div>
{mbfc.mod_ban_from_community.reason && {mbfc.mod_ban_from_community.reason.isSome() &&
` reason: ${mbfc.mod_ban_from_community.reason}`} ` reason: ${mbfc.mod_ban_from_community.reason.unwrap()}`}
</div>, </div>,
<div> <div>
{mbfc.mod_ban_from_community.expires && {mbfc.mod_ban_from_community.expires.isSome() &&
` expires: ${moment ` expires: ${moment
.utc(mbfc.mod_ban_from_community.expires) .utc(mbfc.mod_ban_from_community.expires.unwrap())
.fromNow()}`} .fromNow()}`}
</div>, </div>,
]; ];
@ -364,17 +355,24 @@ export class Modlog extends Component<any, ModlogState> {
<span> <span>
<PersonListing person={mb.banned_person} /> <PersonListing person={mb.banned_person} />
</span>, </span>,
<div>{mb.mod_ban.reason && ` reason: ${mb.mod_ban.reason}`}</div>,
<div> <div>
{mb.mod_ban.expires && {mb.mod_ban.reason.isSome() &&
` expires: ${moment.utc(mb.mod_ban.expires).fromNow()}`} ` reason: ${mb.mod_ban.reason.unwrap()}`}
</div>,
<div>
{mb.mod_ban.expires.isSome() &&
` expires: ${moment.utc(mb.mod_ban.expires.unwrap()).fromNow()}`}
</div>, </div>,
]; ];
} }
case ModlogEnum.ModAdd: { case ModlogEnum.ModAdd: {
let ma = i.view as ModAddView; let ma = i.view as ModAddView;
return [ return [
<span>{ma.mod_add.removed ? "Removed " : "Appointed "} </span>, <span>
{ma.mod_add.removed.isSome() && ma.mod_add.removed.unwrap()
? "Removed "
: "Appointed "}{" "}
</span>,
<span> <span>
<PersonListing person={ma.modded_person} /> <PersonListing person={ma.modded_person} />
</span>, </span>,
@ -387,17 +385,17 @@ export class Modlog extends Component<any, ModlogState> {
} }
combined() { combined() {
let combined = this.buildCombined(this.state.res); let combined = this.state.res.map(this.buildCombined).unwrapOr([]);
return ( return (
<tbody> <tbody>
{combined.map(i => ( {combined.map(i => (
<tr> <tr>
<td> <td>
<MomentTime data={i} /> <MomentTime published={i.when_} updated={None} />
</td> </td>
<td> <td>
{this.isAdminOrMod ? ( {this.amAdminOrMod ? (
<PersonListing person={i.view.moderator} /> <PersonListing person={i.view.moderator} />
) : ( ) : (
<div>{this.modOrAdminText(i.view.moderator)}</div> <div>{this.modOrAdminText(i.view.moderator)}</div>
@ -410,19 +408,11 @@ export class Modlog extends Component<any, ModlogState> {
); );
} }
get isAdminOrMod(): boolean { get amAdminOrMod(): boolean {
let isAdmin = return (
UserService.Instance.myUserInfo && amAdmin(Some(this.state.siteRes.admins)) ||
this.isoData.site_res.admins amMod(this.state.communityMods)
.map(a => a.person.id) );
.includes(UserService.Instance.myUserInfo.local_user_view.person.id);
let isMod =
UserService.Instance.myUserInfo &&
this.state.communityMods &&
this.state.communityMods
.map(m => m.moderator.id)
.includes(UserService.Instance.myUserInfo.local_user_view.person.id);
return isAdmin || isMod;
} }
modOrAdminText(person: PersonSafe): Text { modOrAdminText(person: PersonSafe): Text {
@ -436,7 +426,10 @@ export class Modlog extends Component<any, ModlogState> {
} }
get documentTitle(): string { get documentTitle(): string {
return `Modlog - ${this.state.site_view.site.name}`; return this.state.siteRes.site_view.match({
some: siteView => `Modlog - ${siteView.site.name}`,
none: "",
});
} }
render() { render() {
@ -445,6 +438,8 @@ export class Modlog extends Component<any, ModlogState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
@ -452,17 +447,6 @@ export class Modlog extends Component<any, ModlogState> {
</h5> </h5>
) : ( ) : (
<div> <div>
<h5>
{this.state.communityName && (
<Link
className="text-body"
to={`/c/${this.state.communityName}`}
>
/c/{this.state.communityName}{" "}
</Link>
)}
<span>{i18n.t("modlog")}</span>
</h5>
<div class="table-responsive"> <div class="table-responsive">
<table id="modlog_table" class="table table-sm table-hover"> <table id="modlog_table" class="table table-sm table-hover">
<thead class="pointer"> <thead class="pointer">
@ -491,46 +475,52 @@ export class Modlog extends Component<any, ModlogState> {
} }
refetch() { refetch() {
let modlogForm: GetModlog = { let modlogForm = new GetModlog({
community_id: this.state.communityId, community_id: this.state.communityId,
page: this.state.page, mod_person_id: None,
limit: fetchLimit, page: Some(this.state.page),
auth: authField(false), limit: Some(fetchLimit),
}; auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.getModlog(modlogForm)); WebSocketService.Instance.send(wsClient.getModlog(modlogForm));
if (this.state.communityId) { this.state.communityId.match({
let communityForm: GetCommunity = { some: id => {
id: this.state.communityId, let communityForm = new GetCommunity({
name: this.state.communityName, id: Some(id),
}; name: None,
WebSocketService.Instance.send(wsClient.getCommunity(communityForm)); auth: auth(false).ok(),
} });
WebSocketService.Instance.send(wsClient.getCommunity(communityForm));
},
none: void 0,
});
} }
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let pathSplit = req.path.split("/"); let pathSplit = req.path.split("/");
let communityId = pathSplit[3]; let communityId = Some(pathSplit[3]).map(Number);
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let modlogForm: GetModlog = { let modlogForm = new GetModlog({
page: 1, page: Some(1),
limit: fetchLimit, limit: Some(fetchLimit),
}; community_id: communityId,
mod_person_id: None,
if (communityId) { auth: req.auth,
modlogForm.community_id = Number(communityId); });
}
setOptionalAuth(modlogForm, req.auth);
promises.push(req.client.getModlog(modlogForm)); promises.push(req.client.getModlog(modlogForm));
if (communityId) { if (communityId.isSome()) {
let communityForm: GetCommunity = { let communityForm = new GetCommunity({
id: Number(communityId), id: communityId,
}; name: None,
setOptionalAuth(communityForm, req.auth); auth: req.auth,
});
promises.push(req.client.getCommunity(communityForm)); promises.push(req.client.getCommunity(communityForm));
} else {
promises.push(Promise.resolve());
} }
return promises; return promises;
} }
@ -542,14 +532,14 @@ export class Modlog extends Component<any, ModlogState> {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
return; return;
} else if (op == UserOperation.GetModlog) { } else if (op == UserOperation.GetModlog) {
let data = wsJsonToRes<GetModlogResponse>(msg).data; let data = wsJsonToRes<GetModlogResponse>(msg, GetModlogResponse);
this.state.loading = false; this.state.loading = false;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.state.res = data; this.state.res = Some(data);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetCommunity) { } else if (op == UserOperation.GetCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg).data; let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
this.state.communityMods = data.moderators; this.state.communityMods = Some(data.moderators);
} }
} }
} }

View file

@ -1,3 +1,4 @@
import { None, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
BlockPersonResponse, BlockPersonResponse,
@ -9,25 +10,28 @@ import {
GetPrivateMessages, GetPrivateMessages,
GetReplies, GetReplies,
GetRepliesResponse, GetRepliesResponse,
GetSiteResponse,
PersonMentionResponse, PersonMentionResponse,
PersonMentionView, PersonMentionView,
PostReportResponse, PostReportResponse,
PrivateMessageResponse, PrivateMessageResponse,
PrivateMessagesResponse, PrivateMessagesResponse,
PrivateMessageView, PrivateMessageView,
SiteView,
SortType, SortType,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, auth,
commentsToFlatNodes, commentsToFlatNodes,
createCommentLikeRes, createCommentLikeRes,
editCommentRes, editCommentRes,
enableDownvotes,
fetchLimit, fetchLimit,
isBrowser, isBrowser,
relTags, relTags,
@ -37,9 +41,7 @@ import {
toast, toast,
updatePersonBlock, updatePersonBlock,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { CommentNodes } from "../comment/comment-nodes"; import { CommentNodes } from "../comment/comment-nodes";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
@ -81,12 +83,17 @@ interface InboxState {
combined: ReplyType[]; combined: ReplyType[];
sort: SortType; sort: SortType;
page: number; page: number;
site_view: SiteView; siteRes: GetSiteResponse;
loading: boolean; loading: boolean;
} }
export class Inbox extends Component<any, InboxState> { export class Inbox extends Component<any, InboxState> {
private isoData = setIsoData(this.context); private isoData = setIsoData(
this.context,
GetRepliesResponse,
GetPersonMentionsResponse,
PrivateMessagesResponse
);
private subscription: Subscription; private subscription: Subscription;
private emptyState: InboxState = { private emptyState: InboxState = {
unreadOrAll: UnreadOrAll.Unread, unreadOrAll: UnreadOrAll.Unread,
@ -97,7 +104,7 @@ export class Inbox extends Component<any, InboxState> {
combined: [], combined: [],
sort: SortType.New, sort: SortType.New,
page: 1, page: 1,
site_view: this.isoData.site_res.site_view, siteRes: this.isoData.site_res,
loading: true, loading: true,
}; };
@ -108,7 +115,7 @@ export class Inbox extends Component<any, InboxState> {
this.handleSortChange = this.handleSortChange.bind(this); this.handleSortChange = this.handleSortChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
if (!UserService.Instance.myUserInfo && 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`);
} }
@ -118,9 +125,13 @@ 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.isoData.routeData[0].replies || []; this.state.replies =
this.state.mentions = this.isoData.routeData[1].mentions || []; (this.isoData.routeData[0] as GetRepliesResponse).replies || [];
this.state.messages = this.isoData.routeData[2].messages || []; this.state.mentions =
(this.isoData.routeData[1] as GetPersonMentionsResponse).mentions || [];
this.state.messages =
(this.isoData.routeData[2] as PrivateMessagesResponse)
.private_messages || [];
this.state.combined = this.buildCombined(); this.state.combined = this.buildCombined();
this.state.loading = false; this.state.loading = false;
} else { } else {
@ -135,13 +146,23 @@ export class Inbox extends Component<any, InboxState> {
} }
get documentTitle(): string { get documentTitle(): string {
return `@${ return this.state.siteRes.site_view.match({
UserService.Instance.myUserInfo.local_user_view.person.name some: siteView =>
} ${i18n.t("inbox")} - ${this.state.site_view.site.name}`; UserService.Instance.myUserInfo.match({
some: mui =>
`@${mui.local_user_view.person.name} ${i18n.t("inbox")} - ${
siteView.site.name
}`,
none: "",
}),
none: "",
});
} }
render() { render() {
let inboxRss = `/feeds/inbox/${UserService.Instance.auth}.xml`; let inboxRss = auth()
.ok()
.map(a => `/feeds/inbox/${a}.xml`);
return ( return (
<div class="container"> <div class="container">
{this.state.loading ? ( {this.state.loading ? (
@ -154,19 +175,26 @@ export class Inbox extends Component<any, InboxState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<h5 class="mb-2"> <h5 class="mb-2">
{i18n.t("inbox")} {i18n.t("inbox")}
<small> {inboxRss.match({
<a href={inboxRss} title="RSS" rel={relTags}> some: rss => (
<Icon icon="rss" classes="ml-2 text-muted small" /> <small>
</a> <a href={rss} title="RSS" rel={relTags}>
<link <Icon icon="rss" classes="ml-2 text-muted small" />
rel="alternate" </a>
type="application/atom+xml" <link
href={inboxRss} rel="alternate"
/> type="application/atom+xml"
</small> href={rss}
/>
</small>
),
none: <></>,
})}
</h5> </h5>
{this.state.replies.length + {this.state.replies.length +
this.state.mentions.length + this.state.mentions.length +
@ -355,11 +383,14 @@ export class Inbox extends Component<any, InboxState> {
<CommentNodes <CommentNodes
key={i.id} key={i.id}
nodes={[{ comment_view: i.view as CommentView }]} nodes={[{ comment_view: i.view as CommentView }]}
moderators={None}
admins={None}
maxCommentsShown={None}
noIndent noIndent
markable markable
showCommunity showCommunity
showContext showContext
enableDownvotes={this.state.site_view.site.enable_downvotes} enableDownvotes={enableDownvotes(this.state.siteRes)}
/> />
); );
case ReplyEnum.Mention: case ReplyEnum.Mention:
@ -367,11 +398,14 @@ export class Inbox extends Component<any, InboxState> {
<CommentNodes <CommentNodes
key={i.id} key={i.id}
nodes={[{ comment_view: i.view as PersonMentionView }]} nodes={[{ comment_view: i.view as PersonMentionView }]}
moderators={None}
admins={None}
maxCommentsShown={None}
noIndent noIndent
markable markable
showCommunity showCommunity
showContext showContext
enableDownvotes={this.state.site_view.site.enable_downvotes} enableDownvotes={enableDownvotes(this.state.siteRes)}
/> />
); );
case ReplyEnum.Message: case ReplyEnum.Message:
@ -395,11 +429,14 @@ export class Inbox extends Component<any, InboxState> {
<div> <div>
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(this.state.replies)} nodes={commentsToFlatNodes(this.state.replies)}
moderators={None}
admins={None}
maxCommentsShown={None}
noIndent noIndent
markable markable
showCommunity showCommunity
showContext showContext
enableDownvotes={this.state.site_view.site.enable_downvotes} enableDownvotes={enableDownvotes(this.state.siteRes)}
/> />
</div> </div>
); );
@ -412,11 +449,14 @@ export class Inbox extends Component<any, InboxState> {
<CommentNodes <CommentNodes
key={umv.person_mention.id} key={umv.person_mention.id}
nodes={[{ comment_view: umv }]} nodes={[{ comment_view: umv }]}
moderators={None}
admins={None}
maxCommentsShown={None}
noIndent noIndent
markable markable
showCommunity showCommunity
showContext showContext
enableDownvotes={this.state.site_view.site.enable_downvotes} enableDownvotes={enableDownvotes(this.state.siteRes)}
/> />
))} ))}
</div> </div>
@ -459,62 +499,67 @@ export class Inbox extends Component<any, InboxState> {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
// It can be /u/me, or /username/1 // It can be /u/me, or /username/1
let repliesForm: GetReplies = { let repliesForm = new GetReplies({
sort: SortType.New, sort: Some(SortType.New),
unread_only: true, unread_only: Some(true),
page: 1, page: Some(1),
limit: fetchLimit, limit: Some(fetchLimit),
auth: req.auth, auth: req.auth.unwrap(),
}; });
promises.push(req.client.getReplies(repliesForm)); promises.push(req.client.getReplies(repliesForm));
let personMentionsForm: GetPersonMentions = { let personMentionsForm = new GetPersonMentions({
sort: SortType.New, sort: Some(SortType.New),
unread_only: true, unread_only: Some(true),
page: 1, page: Some(1),
limit: fetchLimit, limit: Some(fetchLimit),
auth: req.auth, auth: req.auth.unwrap(),
}; });
promises.push(req.client.getPersonMentions(personMentionsForm)); promises.push(req.client.getPersonMentions(personMentionsForm));
let privateMessagesForm: GetPrivateMessages = { let privateMessagesForm = new GetPrivateMessages({
unread_only: true, unread_only: Some(true),
page: 1, page: Some(1),
limit: fetchLimit, limit: Some(fetchLimit),
auth: req.auth, auth: req.auth.unwrap(),
}; });
promises.push(req.client.getPrivateMessages(privateMessagesForm)); promises.push(req.client.getPrivateMessages(privateMessagesForm));
return promises; return promises;
} }
refetch() { refetch() {
let repliesForm: GetReplies = { let sort = Some(this.state.sort);
sort: this.state.sort, let unread_only = Some(this.state.unreadOrAll == UnreadOrAll.Unread);
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread, let page = Some(this.state.page);
page: this.state.page, let limit = Some(fetchLimit);
limit: fetchLimit,
auth: authField(), let repliesForm = new GetReplies({
}; sort,
unread_only,
page,
limit,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.getReplies(repliesForm)); WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
let personMentionsForm: GetPersonMentions = { let personMentionsForm = new GetPersonMentions({
sort: this.state.sort, sort,
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread, unread_only,
page: this.state.page, page,
limit: fetchLimit, limit,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getPersonMentions(personMentionsForm) wsClient.getPersonMentions(personMentionsForm)
); );
let privateMessagesForm: GetPrivateMessages = { let privateMessagesForm = new GetPrivateMessages({
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread, unread_only,
page: this.state.page, page,
limit: fetchLimit, limit,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getPrivateMessages(privateMessagesForm) wsClient.getPrivateMessages(privateMessagesForm)
); );
@ -530,7 +575,7 @@ export class Inbox extends Component<any, InboxState> {
markAllAsRead(i: Inbox) { markAllAsRead(i: Inbox) {
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.markAllAsRead({ wsClient.markAllAsRead({
auth: authField(), auth: auth().unwrap(),
}) })
); );
i.state.replies = []; i.state.replies = [];
@ -559,7 +604,7 @@ export class Inbox extends Component<any, InboxState> {
} else if (msg.reconnect) { } else if (msg.reconnect) {
this.refetch(); this.refetch();
} else if (op == UserOperation.GetReplies) { } else if (op == UserOperation.GetReplies) {
let data = wsJsonToRes<GetRepliesResponse>(msg).data; let data = wsJsonToRes<GetRepliesResponse>(msg, GetRepliesResponse);
this.state.replies = data.replies; this.state.replies = data.replies;
this.state.combined = this.buildCombined(); this.state.combined = this.buildCombined();
this.state.loading = false; this.state.loading = false;
@ -567,21 +612,30 @@ export class Inbox extends Component<any, InboxState> {
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.GetPersonMentions) { } else if (op == UserOperation.GetPersonMentions) {
let data = wsJsonToRes<GetPersonMentionsResponse>(msg).data; let data = wsJsonToRes<GetPersonMentionsResponse>(
msg,
GetPersonMentionsResponse
);
this.state.mentions = data.mentions; this.state.mentions = data.mentions;
this.state.combined = this.buildCombined(); this.state.combined = this.buildCombined();
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.GetPrivateMessages) { } else if (op == UserOperation.GetPrivateMessages) {
let data = wsJsonToRes<PrivateMessagesResponse>(msg).data; let data = wsJsonToRes<PrivateMessagesResponse>(
msg,
PrivateMessagesResponse
);
this.state.messages = data.private_messages; this.state.messages = data.private_messages;
this.state.combined = this.buildCombined(); this.state.combined = this.buildCombined();
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.EditPrivateMessage) { } else if (op == UserOperation.EditPrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>(msg).data; let data = wsJsonToRes<PrivateMessageResponse>(
msg,
PrivateMessageResponse
);
let found: PrivateMessageView = this.state.messages.find( let found: PrivateMessageView = this.state.messages.find(
m => m =>
m.private_message.id === data.private_message_view.private_message.id m.private_message.id === data.private_message_view.private_message.id
@ -597,7 +651,10 @@ export class Inbox extends Component<any, InboxState> {
} }
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.DeletePrivateMessage) { } else if (op == UserOperation.DeletePrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>(msg).data; let data = wsJsonToRes<PrivateMessageResponse>(
msg,
PrivateMessageResponse
);
let found: PrivateMessageView = this.state.messages.find( let found: PrivateMessageView = this.state.messages.find(
m => m =>
m.private_message.id === data.private_message_view.private_message.id m.private_message.id === data.private_message_view.private_message.id
@ -613,7 +670,10 @@ export class Inbox extends Component<any, InboxState> {
} }
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.MarkPrivateMessageAsRead) { } else if (op == UserOperation.MarkPrivateMessageAsRead) {
let data = wsJsonToRes<PrivateMessageResponse>(msg).data; let data = wsJsonToRes<PrivateMessageResponse>(
msg,
PrivateMessageResponse
);
let found: PrivateMessageView = this.state.messages.find( let found: PrivateMessageView = this.state.messages.find(
m => m =>
m.private_message.id === data.private_message_view.private_message.id m.private_message.id === data.private_message_view.private_message.id
@ -653,11 +713,11 @@ export class Inbox extends Component<any, InboxState> {
op == UserOperation.DeleteComment || op == UserOperation.DeleteComment ||
op == UserOperation.RemoveComment op == UserOperation.RemoveComment
) { ) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
editCommentRes(data.comment_view, this.state.replies); editCommentRes(data.comment_view, this.state.replies);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.MarkCommentAsRead) { } else if (op == UserOperation.MarkCommentAsRead) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
// If youre in the unread view, just remove it from the list // If youre in the unread view, just remove it from the list
if ( if (
@ -685,7 +745,7 @@ export class Inbox extends Component<any, InboxState> {
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.MarkPersonMentionAsRead) { } else if (op == UserOperation.MarkPersonMentionAsRead) {
let data = wsJsonToRes<PersonMentionResponse>(msg).data; let data = wsJsonToRes<PersonMentionResponse>(msg, PersonMentionResponse);
// TODO this might not be correct, it might need to use the comment id // TODO this might not be correct, it might need to use the comment id
let found = this.state.mentions.find( let found = this.state.mentions.find(
@ -732,85 +792,109 @@ export class Inbox extends Component<any, InboxState> {
this.sendUnreadCount(data.person_mention_view.person_mention.read); this.sendUnreadCount(data.person_mention_view.person_mention.read);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
if ( UserService.Instance.myUserInfo.match({
data.recipient_ids.includes( some: mui => {
UserService.Instance.myUserInfo.local_user_view.local_user.id if (data.recipient_ids.includes(mui.local_user_view.local_user.id)) {
) this.state.replies.unshift(data.comment_view);
) { this.state.combined.unshift(
this.state.replies.unshift(data.comment_view); this.replyToReplyType(data.comment_view)
this.state.combined.unshift(this.replyToReplyType(data.comment_view)); );
this.setState(this.state); this.setState(this.state);
} else if ( } else if (
data.comment_view.creator.id == data.comment_view.creator.id == mui.local_user_view.person.id
UserService.Instance.myUserInfo.local_user_view.person.id ) {
) { // If youre in the unread view, just remove it from the list
// If youre in the unread view, just remove it from the list if (this.state.unreadOrAll == UnreadOrAll.Unread) {
if (this.state.unreadOrAll == UnreadOrAll.Unread) { this.state.replies = this.state.replies.filter(
this.state.replies = this.state.replies.filter( r =>
r => r.comment.id !== data.comment_view.comment.parent_id r.comment.id !==
); data.comment_view.comment.parent_id.unwrapOr(0)
this.state.mentions = this.state.mentions.filter( );
m => m.comment.id !== data.comment_view.comment.parent_id this.state.mentions = this.state.mentions.filter(
); m =>
this.state.combined = this.state.combined.filter(r => { m.comment.id !==
if (this.isMention(r.view)) data.comment_view.comment.parent_id.unwrapOr(0)
return r.view.comment.id !== data.comment_view.comment.parent_id; );
else return r.id !== data.comment_view.comment.parent_id; this.state.combined = this.state.combined.filter(r => {
}); if (this.isMention(r.view))
} else { return (
let mention_found = this.state.mentions.find( r.view.comment.id !==
i => i.comment.id == data.comment_view.comment.parent_id data.comment_view.comment.parent_id.unwrapOr(0)
); );
if (mention_found) { else
mention_found.person_mention.read = true; return (
r.id !== data.comment_view.comment.parent_id.unwrapOr(0)
);
});
} else {
let mention_found = this.state.mentions.find(
i =>
i.comment.id ==
data.comment_view.comment.parent_id.unwrapOr(0)
);
if (mention_found) {
mention_found.person_mention.read = true;
}
let reply_found = this.state.replies.find(
i =>
i.comment.id ==
data.comment_view.comment.parent_id.unwrapOr(0)
);
if (reply_found) {
reply_found.comment.read = true;
}
this.state.combined = this.buildCombined();
}
this.sendUnreadCount(true);
this.setState(this.state);
setupTippy();
// TODO this seems wrong, you should be using form_id
toast(i18n.t("reply_sent"));
} }
let reply_found = this.state.replies.find( },
i => i.comment.id == data.comment_view.comment.parent_id none: void 0,
); });
if (reply_found) {
reply_found.comment.read = true;
}
this.state.combined = this.buildCombined();
}
this.sendUnreadCount(true);
this.setState(this.state);
setupTippy();
// TODO this seems wrong, you should be using form_id
toast(i18n.t("reply_sent"));
}
} else if (op == UserOperation.CreatePrivateMessage) { } else if (op == UserOperation.CreatePrivateMessage) {
let data = wsJsonToRes<PrivateMessageResponse>(msg).data; let data = wsJsonToRes<PrivateMessageResponse>(
if ( msg,
data.private_message_view.recipient.id == PrivateMessageResponse
UserService.Instance.myUserInfo.local_user_view.person.id );
) { UserService.Instance.myUserInfo.match({
this.state.messages.unshift(data.private_message_view); some: mui => {
this.state.combined.unshift( if (
this.messageToReplyType(data.private_message_view) data.private_message_view.recipient.id ==
); mui.local_user_view.person.id
this.setState(this.state); ) {
} this.state.messages.unshift(data.private_message_view);
this.state.combined.unshift(
this.messageToReplyType(data.private_message_view)
);
this.setState(this.state);
}
},
none: void 0,
});
} else if (op == UserOperation.SaveComment) { } else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
saveCommentRes(data.comment_view, this.state.replies); saveCommentRes(data.comment_view, this.state.replies);
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
createCommentLikeRes(data.comment_view, this.state.replies); createCommentLikeRes(data.comment_view, this.state.replies);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data; let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
updatePersonBlock(data); updatePersonBlock(data);
} else if (op == UserOperation.CreatePostReport) { } else if (op == UserOperation.CreatePostReport) {
let data = wsJsonToRes<PostReportResponse>(msg).data; let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
} else if (op == UserOperation.CreateCommentReport) { } else if (op == UserOperation.CreateCommentReport) {
let data = wsJsonToRes<CommentReportResponse>(msg).data; let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }

View file

@ -1,9 +1,12 @@
import { None } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
GetSiteResponse,
LoginResponse, LoginResponse,
PasswordChange as PasswordChangeForm, PasswordChange as PasswordChangeForm,
SiteView,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
@ -14,9 +17,7 @@ import {
setIsoData, setIsoData,
toast, toast,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
@ -24,7 +25,7 @@ import { Spinner } from "../common/icon";
interface State { interface State {
passwordChangeForm: PasswordChangeForm; passwordChangeForm: PasswordChangeForm;
loading: boolean; loading: boolean;
site_view: SiteView; siteRes: GetSiteResponse;
} }
export class PasswordChange extends Component<any, State> { export class PasswordChange extends Component<any, State> {
@ -32,13 +33,13 @@ export class PasswordChange extends Component<any, State> {
private subscription: Subscription; private subscription: Subscription;
emptyState: State = { emptyState: State = {
passwordChangeForm: { passwordChangeForm: new PasswordChangeForm({
token: this.props.match.params.token, token: this.props.match.params.token,
password: undefined, password: undefined,
password_verify: undefined, password_verify: undefined,
}, }),
loading: false, loading: false,
site_view: this.isoData.site_res.site_view, siteRes: this.isoData.site_res,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -57,7 +58,10 @@ export class PasswordChange extends Component<any, State> {
} }
get documentTitle(): string { get documentTitle(): string {
return `${i18n.t("password_change")} - ${this.state.site_view.site.name}`; return this.state.siteRes.site_view.match({
some: siteView => `${i18n.t("password_change")} - ${siteView.site.name}`,
none: "",
});
} }
render() { render() {
@ -66,6 +70,8 @@ export class PasswordChange extends Component<any, State> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div class="col-12 col-lg-6 offset-lg-3 mb-4">
@ -156,7 +162,7 @@ export class PasswordChange extends Component<any, State> {
this.setState(this.state); this.setState(this.state);
return; return;
} else if (op == UserOperation.PasswordChange) { } else if (op == UserOperation.PasswordChange) {
let data = wsJsonToRes<LoginResponse>(msg).data; let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
this.state = this.emptyState; this.state = this.emptyState;
this.setState(this.state); this.setState(this.state);
UserService.Instance.login(data); UserService.Instance.login(data);

View file

@ -1,3 +1,4 @@
import { None, Some } from "@sniptt/monads/build";
import { Component } from "inferno"; import { Component } from "inferno";
import { import {
CommentView, CommentView,
@ -89,7 +90,9 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
<CommentNodes <CommentNodes
key={i.id} key={i.id}
nodes={[{ comment_view: c }]} nodes={[{ comment_view: c }]}
admins={this.props.admins} admins={Some(this.props.admins)}
moderators={None}
maxCommentsShown={None}
noBorder noBorder
noIndent noIndent
showCommunity showCommunity
@ -104,7 +107,9 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
<PostListing <PostListing
key={i.id} key={i.id}
post_view={p} post_view={p}
admins={this.props.admins} admins={Some(this.props.admins)}
duplicates={None}
moderators={None}
showCommunity showCommunity
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
@ -154,7 +159,9 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
<div> <div>
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(this.props.personRes.comments)} nodes={commentsToFlatNodes(this.props.personRes.comments)}
admins={this.props.admins} admins={Some(this.props.admins)}
moderators={None}
maxCommentsShown={None}
noIndent noIndent
showCommunity showCommunity
showContext showContext
@ -171,8 +178,10 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
<> <>
<PostListing <PostListing
post_view={post} post_view={post}
admins={this.props.admins} admins={Some(this.props.admins)}
showCommunity showCommunity
duplicates={None}
moderators={None}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
/> />

View file

@ -21,7 +21,7 @@ export class PersonListing extends Component<PersonListingProps, any> {
render() { render() {
let person = this.props.person; let person = this.props.person;
let local = person.local == null ? true : person.local; let local = person.local;
let apubName: string, link: string; let apubName: string, link: string;
if (local) { if (local) {
@ -37,11 +37,9 @@ export class PersonListing extends Component<PersonListingProps, any> {
let displayName = this.props.useApubName let displayName = this.props.useApubName
? apubName ? apubName
: person.display_name : person.display_name.unwrapOr(apubName);
? person.display_name
: apubName;
if (this.props.showApubName && !local && person.display_name) { if (this.props.showApubName && !local && person.display_name.isSome()) {
displayName = `${displayName} (${apubName})`; displayName = `${displayName} (${apubName})`;
} }
@ -72,12 +70,14 @@ export class PersonListing extends Component<PersonListingProps, any> {
} }
avatarAndName(displayName: string) { avatarAndName(displayName: string) {
let person = this.props.person;
return ( return (
<> <>
{!this.props.hideAvatar && person.avatar && showAvatars() && ( {this.props.person.avatar.match({
<PictrsImage src={person.avatar} icon /> some: avatar =>
)} !this.props.hideAvatar &&
showAvatars() && <PictrsImage src={avatar} icon />,
none: <></>,
})}
<span>{displayName}</span> <span>{displayName}</span>
</> </>
); );

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,20 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
GetSiteResponse,
ListRegistrationApplications, ListRegistrationApplications,
ListRegistrationApplicationsResponse, ListRegistrationApplicationsResponse,
RegistrationApplicationResponse, RegistrationApplicationResponse,
RegistrationApplicationView,
SiteView,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, auth,
fetchLimit, fetchLimit,
isBrowser, isBrowser,
setIsoData, setIsoData,
@ -20,9 +22,7 @@ import {
toast, toast,
updateRegistrationApplicationRes, updateRegistrationApplicationRes,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
@ -35,10 +35,10 @@ enum UnreadOrAll {
} }
interface RegistrationApplicationsState { interface RegistrationApplicationsState {
applications: RegistrationApplicationView[]; listRegistrationApplicationsResponse: Option<ListRegistrationApplicationsResponse>;
page: number; siteRes: GetSiteResponse;
site_view: SiteView;
unreadOrAll: UnreadOrAll; unreadOrAll: UnreadOrAll;
page: number;
loading: boolean; loading: boolean;
} }
@ -46,13 +46,16 @@ export class RegistrationApplications extends Component<
any, any,
RegistrationApplicationsState RegistrationApplicationsState
> { > {
private isoData = setIsoData(this.context); private isoData = setIsoData(
this.context,
ListRegistrationApplicationsResponse
);
private subscription: Subscription; private subscription: Subscription;
private emptyState: RegistrationApplicationsState = { private emptyState: RegistrationApplicationsState = {
listRegistrationApplicationsResponse: None,
siteRes: this.isoData.site_res,
unreadOrAll: UnreadOrAll.Unread, unreadOrAll: UnreadOrAll.Unread,
applications: [],
page: 1, page: 1,
site_view: this.isoData.site_res.site_view,
loading: true, loading: true,
}; };
@ -62,7 +65,7 @@ export class RegistrationApplications extends Component<
this.state = this.emptyState; this.state = this.emptyState;
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
if (!UserService.Instance.myUserInfo && 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`);
} }
@ -72,8 +75,9 @@ 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.applications = this.state.listRegistrationApplicationsResponse = Some(
this.isoData.routeData[0].registration_applications || []; // TODO test this.isoData.routeData[0] as ListRegistrationApplicationsResponse
);
this.state.loading = false; this.state.loading = false;
} else { } else {
this.refetch(); this.refetch();
@ -91,11 +95,17 @@ export class RegistrationApplications extends Component<
} }
get documentTitle(): string { get documentTitle(): string {
return `@${ return this.state.siteRes.site_view.match({
UserService.Instance.myUserInfo.local_user_view.person.name some: siteView =>
} ${i18n.t("registration_applications")} - ${ UserService.Instance.myUserInfo.match({
this.state.site_view.site.name some: mui =>
}`; `@${mui.local_user_view.person.name} ${i18n.t(
"registration_applications"
)} - ${siteView.site.name}`,
none: "",
}),
none: "",
});
} }
render() { render() {
@ -111,6 +121,8 @@ export class RegistrationApplications extends Component<
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<h5 class="mb-2">{i18n.t("registration_applications")}</h5> <h5 class="mb-2">{i18n.t("registration_applications")}</h5>
{this.selects()} {this.selects()}
@ -168,19 +180,22 @@ export class RegistrationApplications extends Component<
} }
applicationList() { applicationList() {
return ( return this.state.listRegistrationApplicationsResponse.match({
<div> some: res => (
{this.state.applications.map(ra => ( <div>
<> {res.registration_applications.map(ra => (
<hr /> <>
<RegistrationApplication <hr />
key={ra.registration_application.id} <RegistrationApplication
application={ra} key={ra.registration_application.id}
/> application={ra}
</> />
))} </>
</div> ))}
); </div>
),
none: <></>,
});
} }
handleUnreadOrAllChange(i: RegistrationApplications, event: any) { handleUnreadOrAllChange(i: RegistrationApplications, event: any) {
@ -198,12 +213,12 @@ export class RegistrationApplications extends Component<
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let form: ListRegistrationApplications = { let form = new ListRegistrationApplications({
unread_only: true, unread_only: Some(true),
page: 1, page: Some(1),
limit: fetchLimit, limit: Some(fetchLimit),
auth: req.auth, auth: req.auth.unwrap(),
}; });
promises.push(req.client.listRegistrationApplications(form)); promises.push(req.client.listRegistrationApplications(form));
return promises; return promises;
@ -211,12 +226,12 @@ export class RegistrationApplications extends Component<
refetch() { refetch() {
let unread_only = this.state.unreadOrAll == UnreadOrAll.Unread; let unread_only = this.state.unreadOrAll == UnreadOrAll.Unread;
let form: ListRegistrationApplications = { let form = new ListRegistrationApplications({
unread_only: unread_only, unread_only: Some(unread_only),
page: this.state.page, page: Some(this.state.page),
limit: fetchLimit, limit: Some(fetchLimit),
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.listRegistrationApplications(form)); WebSocketService.Instance.send(wsClient.listRegistrationApplications(form));
} }
@ -229,16 +244,24 @@ export class RegistrationApplications extends Component<
} else if (msg.reconnect) { } else if (msg.reconnect) {
this.refetch(); this.refetch();
} else if (op == UserOperation.ListRegistrationApplications) { } else if (op == UserOperation.ListRegistrationApplications) {
let data = wsJsonToRes<ListRegistrationApplicationsResponse>(msg).data; let data = wsJsonToRes<ListRegistrationApplicationsResponse>(
this.state.applications = data.registration_applications; msg,
ListRegistrationApplicationsResponse
);
this.state.listRegistrationApplicationsResponse = Some(data);
this.state.loading = false; this.state.loading = false;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.ApproveRegistrationApplication) { } else if (op == UserOperation.ApproveRegistrationApplication) {
let data = wsJsonToRes<RegistrationApplicationResponse>(msg).data; let data = wsJsonToRes<RegistrationApplicationResponse>(
msg,
RegistrationApplicationResponse
);
updateRegistrationApplicationRes( updateRegistrationApplicationRes(
data.registration_application, data.registration_application,
this.state.applications this.state.listRegistrationApplicationsResponse
.map(r => r.registration_applications)
.unwrapOr([])
); );
let uacs = UserService.Instance.unreadApplicationCountSub; let uacs = UserService.Instance.unreadApplicationCountSub;
// Minor bug, where if the application switches from deny to approve, the count will still go down // Minor bug, where if the application switches from deny to approve, the count will still go down

View file

@ -1,22 +1,25 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
CommentReportResponse, CommentReportResponse,
CommentReportView, CommentReportView,
GetSiteResponse,
ListCommentReports, ListCommentReports,
ListCommentReportsResponse, ListCommentReportsResponse,
ListPostReports, ListPostReports,
ListPostReportsResponse, ListPostReportsResponse,
PostReportResponse, PostReportResponse,
PostReportView, PostReportView,
SiteView,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, auth,
fetchLimit, fetchLimit,
isBrowser, isBrowser,
setIsoData, setIsoData,
@ -25,9 +28,7 @@ import {
updateCommentReportRes, updateCommentReportRes,
updatePostReportRes, updatePostReportRes,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { CommentReport } from "../comment/comment-report"; import { CommentReport } from "../comment/comment-report";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
@ -59,27 +60,31 @@ type ItemType = {
}; };
interface ReportsState { interface ReportsState {
listCommentReportsResponse: Option<ListCommentReportsResponse>;
listPostReportsResponse: Option<ListPostReportsResponse>;
unreadOrAll: UnreadOrAll; unreadOrAll: UnreadOrAll;
messageType: MessageType; messageType: MessageType;
commentReports: CommentReportView[];
postReports: PostReportView[];
combined: ItemType[]; combined: ItemType[];
siteRes: GetSiteResponse;
page: number; page: number;
site_view: SiteView;
loading: boolean; loading: boolean;
} }
export class Reports extends Component<any, ReportsState> { export class Reports extends Component<any, ReportsState> {
private isoData = setIsoData(this.context); private isoData = setIsoData(
this.context,
ListCommentReportsResponse,
ListPostReportsResponse
);
private subscription: Subscription; private subscription: Subscription;
private emptyState: ReportsState = { private emptyState: ReportsState = {
listCommentReportsResponse: None,
listPostReportsResponse: None,
unreadOrAll: UnreadOrAll.Unread, unreadOrAll: UnreadOrAll.Unread,
messageType: MessageType.All, messageType: MessageType.All,
commentReports: [],
postReports: [],
combined: [], combined: [],
page: 1, page: 1,
site_view: this.isoData.site_res.site_view, siteRes: this.isoData.site_res,
loading: true, loading: true,
}; };
@ -89,7 +94,7 @@ export class Reports extends Component<any, ReportsState> {
this.state = this.emptyState; this.state = this.emptyState;
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
if (!UserService.Instance.myUserInfo && 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`);
} }
@ -99,9 +104,12 @@ 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.commentReports = this.state.listCommentReportsResponse = Some(
this.isoData.routeData[0].comment_reports || []; this.isoData.routeData[0] as ListCommentReportsResponse
this.state.postReports = this.isoData.routeData[1].post_reports || []; );
this.state.listPostReportsResponse = Some(
this.isoData.routeData[1] as ListPostReportsResponse
);
this.state.combined = this.buildCombined(); this.state.combined = this.buildCombined();
this.state.loading = false; this.state.loading = false;
} else { } else {
@ -116,9 +124,17 @@ export class Reports extends Component<any, ReportsState> {
} }
get documentTitle(): string { get documentTitle(): string {
return `@${ return this.state.siteRes.site_view.match({
UserService.Instance.myUserInfo.local_user_view.person.name some: siteView =>
} ${i18n.t("reports")} - ${this.state.site_view.site.name}`; UserService.Instance.myUserInfo.match({
some: mui =>
`@${mui.local_user_view.person.name} ${i18n.t("reports")} - ${
siteView.site.name
}`,
none: "",
}),
none: "",
});
} }
render() { render() {
@ -134,6 +150,8 @@ export class Reports extends Component<any, ReportsState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<h5 class="mb-2">{i18n.t("reports")}</h5> <h5 class="mb-2">{i18n.t("reports")}</h5>
{this.selects()} {this.selects()}
@ -260,12 +278,14 @@ export class Reports extends Component<any, ReportsState> {
} }
buildCombined(): ItemType[] { buildCombined(): ItemType[] {
let comments: ItemType[] = this.state.commentReports.map(r => let comments: ItemType[] = this.state.listCommentReportsResponse
this.replyToReplyType(r) .map(r => r.comment_reports)
); .unwrapOr([])
let posts: ItemType[] = this.state.postReports.map(r => .map(r => this.replyToReplyType(r));
this.mentionToReplyType(r) let posts: ItemType[] = this.state.listPostReportsResponse
); .map(r => r.post_reports)
.unwrapOr([])
.map(r => this.mentionToReplyType(r));
return [...comments, ...posts].sort((a, b) => return [...comments, ...posts].sort((a, b) =>
b.published.localeCompare(a.published) b.published.localeCompare(a.published)
@ -299,29 +319,35 @@ export class Reports extends Component<any, ReportsState> {
} }
commentReports() { commentReports() {
return ( return this.state.listCommentReportsResponse.match({
<div> some: res => (
{this.state.commentReports.map(cr => ( <div>
<> {res.comment_reports.map(cr => (
<hr /> <>
<CommentReport key={cr.comment_report.id} report={cr} /> <hr />
</> <CommentReport key={cr.comment_report.id} report={cr} />
))} </>
</div> ))}
); </div>
),
none: <></>,
});
} }
postReports() { postReports() {
return ( return this.state.listPostReportsResponse.match({
<div> some: res => (
{this.state.postReports.map(pr => ( <div>
<> {res.post_reports.map(pr => (
<hr /> <>
<PostReport key={pr.post_report.id} report={pr} /> <hr />
</> <PostReport key={pr.post_report.id} report={pr} />
))} </>
</div> ))}
); </div>
),
none: <></>,
});
} }
handlePageChange(page: number) { handlePageChange(page: number) {
@ -346,47 +372,61 @@ export class Reports extends Component<any, ReportsState> {
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let commentReportsForm: ListCommentReports = { let unresolved_only = Some(true);
let page = Some(1);
let limit = Some(fetchLimit);
let community_id = None;
let auth = req.auth.unwrap();
let commentReportsForm = new ListCommentReports({
// TODO community_id // TODO community_id
unresolved_only: true, unresolved_only,
page: 1, community_id,
limit: fetchLimit, page,
auth: req.auth, limit,
}; auth,
});
promises.push(req.client.listCommentReports(commentReportsForm)); promises.push(req.client.listCommentReports(commentReportsForm));
let postReportsForm: ListPostReports = { let postReportsForm = new ListPostReports({
// TODO community_id // TODO community_id
unresolved_only: true, unresolved_only,
page: 1, community_id,
limit: fetchLimit, page,
auth: req.auth, limit,
}; auth,
});
promises.push(req.client.listPostReports(postReportsForm)); promises.push(req.client.listPostReports(postReportsForm));
return promises; return promises;
} }
refetch() { refetch() {
let unresolved_only = this.state.unreadOrAll == UnreadOrAll.Unread; let unresolved_only = Some(this.state.unreadOrAll == UnreadOrAll.Unread);
let commentReportsForm: ListCommentReports = { let community_id = None;
// TODO community_id let page = Some(this.state.page);
let limit = Some(fetchLimit);
let commentReportsForm = new ListCommentReports({
unresolved_only, unresolved_only,
page: this.state.page, // TODO community_id
limit: fetchLimit, community_id,
auth: authField(), page,
}; limit,
auth: auth().unwrap(),
});
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.listCommentReports(commentReportsForm) wsClient.listCommentReports(commentReportsForm)
); );
let postReportsForm: ListPostReports = { let postReportsForm = new ListPostReports({
// TODO community_id
unresolved_only, unresolved_only,
page: this.state.page, // TODO community_id
limit: fetchLimit, community_id,
auth: authField(), page,
}; limit,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.listPostReports(postReportsForm)); WebSocketService.Instance.send(wsClient.listPostReports(postReportsForm));
} }
@ -399,8 +439,11 @@ export class Reports extends Component<any, ReportsState> {
} else if (msg.reconnect) { } else if (msg.reconnect) {
this.refetch(); this.refetch();
} else if (op == UserOperation.ListCommentReports) { } else if (op == UserOperation.ListCommentReports) {
let data = wsJsonToRes<ListCommentReportsResponse>(msg).data; let data = wsJsonToRes<ListCommentReportsResponse>(
this.state.commentReports = data.comment_reports; msg,
ListCommentReportsResponse
);
this.state.listCommentReportsResponse = Some(data);
this.state.combined = this.buildCombined(); this.state.combined = this.buildCombined();
this.state.loading = false; this.state.loading = false;
// this.sendUnreadCount(); // this.sendUnreadCount();
@ -408,8 +451,11 @@ export class Reports extends Component<any, ReportsState> {
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.ListPostReports) { } else if (op == UserOperation.ListPostReports) {
let data = wsJsonToRes<ListPostReportsResponse>(msg).data; let data = wsJsonToRes<ListPostReportsResponse>(
this.state.postReports = data.post_reports; msg,
ListPostReportsResponse
);
this.state.listPostReportsResponse = Some(data);
this.state.combined = this.buildCombined(); this.state.combined = this.buildCombined();
this.state.loading = false; this.state.loading = false;
// this.sendUnreadCount(); // this.sendUnreadCount();
@ -417,8 +463,11 @@ export class Reports extends Component<any, ReportsState> {
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.ResolvePostReport) { } else if (op == UserOperation.ResolvePostReport) {
let data = wsJsonToRes<PostReportResponse>(msg).data; let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
updatePostReportRes(data.post_report_view, this.state.postReports); updatePostReportRes(
data.post_report_view,
this.state.listPostReportsResponse.map(r => r.post_reports).unwrapOr([])
);
let urcs = UserService.Instance.unreadReportCountSub; let urcs = UserService.Instance.unreadReportCountSub;
if (data.post_report_view.post_report.resolved) { if (data.post_report_view.post_report.resolved) {
urcs.next(urcs.getValue() - 1); urcs.next(urcs.getValue() - 1);
@ -427,10 +476,12 @@ export class Reports extends Component<any, ReportsState> {
} }
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.ResolveCommentReport) { } else if (op == UserOperation.ResolveCommentReport) {
let data = wsJsonToRes<CommentReportResponse>(msg).data; let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
updateCommentReportRes( updateCommentReportRes(
data.comment_report_view, data.comment_report_view,
this.state.commentReports this.state.listCommentReportsResponse
.map(r => r.comment_reports)
.unwrapOr([])
); );
let urcs = UserService.Instance.unreadReportCountSub; let urcs = UserService.Instance.unreadReportCountSub;
if (data.comment_report_view.comment_report.resolved) { if (data.comment_report_view.comment_report.resolved) {

View file

@ -1,3 +1,4 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
BlockCommunity, BlockCommunity,
@ -15,19 +16,23 @@ import {
PersonViewSafe, PersonViewSafe,
SaveUserSettings, SaveUserSettings,
SortType, SortType,
toUndefined,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n, languages } from "../../i18next"; import { i18n, languages } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, auth,
capitalizeFirstLetter, capitalizeFirstLetter,
choicesConfig, choicesConfig,
communitySelectName, communitySelectName,
communityToChoice, communityToChoice,
debounce, debounce,
elementUrl, elementUrl,
enableNsfw,
fetchCommunities, fetchCommunities,
fetchThemeList, fetchThemeList,
fetchUsers, fetchUsers,
@ -44,9 +49,7 @@ import {
updateCommunityBlock, updateCommunityBlock,
updatePersonBlock, updatePersonBlock,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
@ -65,20 +68,19 @@ if (isBrowser()) {
interface SettingsState { interface SettingsState {
saveUserSettingsForm: SaveUserSettings; saveUserSettingsForm: SaveUserSettings;
changePasswordForm: ChangePassword; changePasswordForm: ChangePassword;
saveUserSettingsLoading: boolean;
changePasswordLoading: boolean;
deleteAccountLoading: boolean;
deleteAccountShowConfirm: boolean;
deleteAccountForm: DeleteAccount; deleteAccountForm: DeleteAccount;
personBlocks: PersonBlockView[]; personBlocks: PersonBlockView[];
blockPersonId: number; blockPerson: Option<PersonViewSafe>;
blockPerson?: PersonViewSafe;
communityBlocks: CommunityBlockView[]; communityBlocks: CommunityBlockView[];
blockCommunityId: number; blockCommunityId: number;
blockCommunity?: CommunityView; blockCommunity?: CommunityView;
currentTab: string; currentTab: string;
siteRes: GetSiteResponse;
themeList: string[]; themeList: string[];
saveUserSettingsLoading: boolean;
changePasswordLoading: boolean;
deleteAccountLoading: boolean;
deleteAccountShowConfirm: boolean;
siteRes: GetSiteResponse;
} }
export class Settings extends Component<any, SettingsState> { export class Settings extends Component<any, SettingsState> {
@ -87,25 +89,43 @@ export class Settings extends Component<any, SettingsState> {
private blockCommunityChoices: any; private blockCommunityChoices: any;
private subscription: Subscription; private subscription: Subscription;
private emptyState: SettingsState = { private emptyState: SettingsState = {
saveUserSettingsForm: { saveUserSettingsForm: new SaveUserSettings({
auth: authField(false), show_nsfw: None,
}, show_scores: None,
changePasswordForm: { show_avatars: None,
new_password: null, show_read_posts: None,
new_password_verify: null, show_bot_accounts: None,
old_password: null, show_new_post_notifs: None,
auth: authField(false), default_sort_type: None,
}, default_listing_type: None,
saveUserSettingsLoading: null, theme: None,
lang: None,
avatar: None,
banner: None,
display_name: None,
email: None,
bio: None,
matrix_user_id: None,
send_notifications_to_email: None,
bot_account: None,
auth: undefined,
}),
changePasswordForm: new ChangePassword({
new_password: undefined,
new_password_verify: undefined,
old_password: undefined,
auth: undefined,
}),
saveUserSettingsLoading: false,
changePasswordLoading: false, changePasswordLoading: false,
deleteAccountLoading: null, deleteAccountLoading: false,
deleteAccountShowConfirm: false, deleteAccountShowConfirm: false,
deleteAccountForm: { deleteAccountForm: new DeleteAccount({
password: null, password: undefined,
auth: authField(false), auth: undefined,
}, }),
personBlocks: [], personBlocks: [],
blockPersonId: 0, blockPerson: None,
communityBlocks: [], communityBlocks: [],
blockCommunityId: 0, blockCommunityId: 0,
currentTab: "settings", currentTab: "settings",
@ -154,7 +174,7 @@ export class Settings extends Component<any, SettingsState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={this.documentTitle} description={Some(this.documentTitle)}
image={this.state.saveUserSettingsForm.avatar} image={this.state.saveUserSettingsForm.avatar}
/> />
<ul class="nav nav-tabs mb-2"> <ul class="nav nav-tabs mb-2">
@ -342,14 +362,17 @@ export class Settings extends Component<any, SettingsState> {
<select <select
class="form-control" class="form-control"
id="block-person-filter" id="block-person-filter"
value={this.state.blockPersonId} value={this.state.blockPerson.map(p => p.person.id).unwrapOr(0)}
> >
<option value="0"></option> <option value="0"></option>
{this.state.blockPerson && ( {this.state.blockPerson.match({
<option value={this.state.blockPerson.person.id}> some: personView => (
{personSelectName(this.state.blockPerson)} <option value={personView.person.id}>
</option> {personSelectName(personView)}
)} </option>
),
none: <></>,
})}
</select> </select>
</div> </div>
</div> </div>
@ -431,7 +454,9 @@ export class Settings extends Component<any, SettingsState> {
type="text" type="text"
class="form-control" class="form-control"
placeholder={i18n.t("optional")} placeholder={i18n.t("optional")}
value={this.state.saveUserSettingsForm.display_name} value={toUndefined(
this.state.saveUserSettingsForm.display_name
)}
onInput={linkEvent(this, this.handleDisplayNameChange)} onInput={linkEvent(this, this.handleDisplayNameChange)}
pattern="^(?!@)(.+)$" pattern="^(?!@)(.+)$"
minLength={3} minLength={3}
@ -446,7 +471,9 @@ export class Settings extends Component<any, SettingsState> {
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.saveUserSettingsForm.bio} initialContent={this.state.saveUserSettingsForm.bio}
onContentChange={this.handleBioChange} onContentChange={this.handleBioChange}
maxLength={300} maxLength={Some(300)}
placeholder={None}
buttonTitle={None}
hideNavigationWarnings hideNavigationWarnings
/> />
</div> </div>
@ -461,7 +488,7 @@ export class Settings extends Component<any, SettingsState> {
id="user-email" id="user-email"
class="form-control" class="form-control"
placeholder={i18n.t("optional")} placeholder={i18n.t("optional")}
value={this.state.saveUserSettingsForm.email} value={toUndefined(this.state.saveUserSettingsForm.email)}
onInput={linkEvent(this, this.handleEmailChange)} onInput={linkEvent(this, this.handleEmailChange)}
minLength={3} minLength={3}
/> />
@ -479,7 +506,9 @@ export class Settings extends Component<any, SettingsState> {
type="text" type="text"
class="form-control" class="form-control"
placeholder="@user:example.com" placeholder="@user:example.com"
value={this.state.saveUserSettingsForm.matrix_user_id} value={toUndefined(
this.state.saveUserSettingsForm.matrix_user_id
)}
onInput={linkEvent(this, this.handleMatrixUserIdChange)} onInput={linkEvent(this, this.handleMatrixUserIdChange)}
pattern="^@[A-Za-z0-9._=-]+:[A-Za-z0-9.-]+\.[A-Za-z]{2,}$" pattern="^@[A-Za-z0-9._=-]+:[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
/> />
@ -515,7 +544,7 @@ export class Settings extends Component<any, SettingsState> {
<div class="col-sm-9"> <div class="col-sm-9">
<select <select
id="user-language" id="user-language"
value={this.state.saveUserSettingsForm.lang} value={toUndefined(this.state.saveUserSettingsForm.lang)}
onChange={linkEvent(this, this.handleLangChange)} onChange={linkEvent(this, this.handleLangChange)}
class="custom-select w-auto" class="custom-select w-auto"
> >
@ -541,7 +570,7 @@ export class Settings extends Component<any, SettingsState> {
<div class="col-sm-9"> <div class="col-sm-9">
<select <select
id="user-theme" id="user-theme"
value={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" class="custom-select w-auto"
> >
@ -561,7 +590,9 @@ export class Settings extends Component<any, SettingsState> {
<ListingTypeSelect <ListingTypeSelect
type_={ type_={
Object.values(ListingType)[ Object.values(ListingType)[
this.state.saveUserSettingsForm.default_listing_type this.state.saveUserSettingsForm.default_listing_type.unwrapOr(
1
)
] ]
} }
showLocal={showLocal(this.isoData)} showLocal={showLocal(this.isoData)}
@ -576,21 +607,25 @@ export class Settings extends Component<any, SettingsState> {
<SortSelect <SortSelect
sort={ sort={
Object.values(SortType)[ Object.values(SortType)[
this.state.saveUserSettingsForm.default_sort_type this.state.saveUserSettingsForm.default_sort_type.unwrapOr(
0
)
] ]
} }
onChange={this.handleSortTypeChange} onChange={this.handleSortTypeChange}
/> />
</div> </div>
</form> </form>
{this.state.siteRes.site_view.site.enable_nsfw && ( {enableNsfw(this.state.siteRes) && (
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input
class="form-check-input" class="form-check-input"
id="user-show-nsfw" id="user-show-nsfw"
type="checkbox" type="checkbox"
checked={this.state.saveUserSettingsForm.show_nsfw} checked={toUndefined(
this.state.saveUserSettingsForm.show_nsfw
)}
onChange={linkEvent(this, this.handleShowNsfwChange)} onChange={linkEvent(this, this.handleShowNsfwChange)}
/> />
<label class="form-check-label" htmlFor="user-show-nsfw"> <label class="form-check-label" htmlFor="user-show-nsfw">
@ -605,7 +640,9 @@ export class Settings extends Component<any, SettingsState> {
class="form-check-input" class="form-check-input"
id="user-show-scores" id="user-show-scores"
type="checkbox" type="checkbox"
checked={this.state.saveUserSettingsForm.show_scores} checked={toUndefined(
this.state.saveUserSettingsForm.show_scores
)}
onChange={linkEvent(this, this.handleShowScoresChange)} onChange={linkEvent(this, this.handleShowScoresChange)}
/> />
<label class="form-check-label" htmlFor="user-show-scores"> <label class="form-check-label" htmlFor="user-show-scores">
@ -619,7 +656,9 @@ export class Settings extends Component<any, SettingsState> {
class="form-check-input" class="form-check-input"
id="user-show-avatars" id="user-show-avatars"
type="checkbox" type="checkbox"
checked={this.state.saveUserSettingsForm.show_avatars} checked={toUndefined(
this.state.saveUserSettingsForm.show_avatars
)}
onChange={linkEvent(this, this.handleShowAvatarsChange)} onChange={linkEvent(this, this.handleShowAvatarsChange)}
/> />
<label class="form-check-label" htmlFor="user-show-avatars"> <label class="form-check-label" htmlFor="user-show-avatars">
@ -633,7 +672,9 @@ export class Settings extends Component<any, SettingsState> {
class="form-check-input" class="form-check-input"
id="user-bot-account" id="user-bot-account"
type="checkbox" type="checkbox"
checked={this.state.saveUserSettingsForm.bot_account} checked={toUndefined(
this.state.saveUserSettingsForm.bot_account
)}
onChange={linkEvent(this, this.handleBotAccount)} onChange={linkEvent(this, this.handleBotAccount)}
/> />
<label class="form-check-label" htmlFor="user-bot-account"> <label class="form-check-label" htmlFor="user-bot-account">
@ -647,7 +688,9 @@ export class Settings extends Component<any, SettingsState> {
class="form-check-input" class="form-check-input"
id="user-show-bot-accounts" id="user-show-bot-accounts"
type="checkbox" type="checkbox"
checked={this.state.saveUserSettingsForm.show_bot_accounts} checked={toUndefined(
this.state.saveUserSettingsForm.show_bot_accounts
)}
onChange={linkEvent(this, this.handleShowBotAccounts)} onChange={linkEvent(this, this.handleShowBotAccounts)}
/> />
<label class="form-check-label" htmlFor="user-show-bot-accounts"> <label class="form-check-label" htmlFor="user-show-bot-accounts">
@ -661,7 +704,9 @@ export class Settings extends Component<any, SettingsState> {
class="form-check-input" class="form-check-input"
id="user-show-read-posts" id="user-show-read-posts"
type="checkbox" type="checkbox"
checked={this.state.saveUserSettingsForm.show_read_posts} checked={toUndefined(
this.state.saveUserSettingsForm.show_read_posts
)}
onChange={linkEvent(this, this.handleReadPosts)} onChange={linkEvent(this, this.handleReadPosts)}
/> />
<label class="form-check-label" htmlFor="user-show-read-posts"> <label class="form-check-label" htmlFor="user-show-read-posts">
@ -675,7 +720,9 @@ export class Settings extends Component<any, SettingsState> {
class="form-check-input" class="form-check-input"
id="user-show-new-post-notifs" id="user-show-new-post-notifs"
type="checkbox" type="checkbox"
checked={this.state.saveUserSettingsForm.show_new_post_notifs} checked={toUndefined(
this.state.saveUserSettingsForm.show_new_post_notifs
)}
onChange={linkEvent(this, this.handleShowNewPostNotifs)} onChange={linkEvent(this, this.handleShowNewPostNotifs)}
/> />
<label <label
@ -693,9 +740,9 @@ export class Settings extends Component<any, SettingsState> {
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}
checked={ checked={toUndefined(
this.state.saveUserSettingsForm.send_notifications_to_email this.state.saveUserSettingsForm.send_notifications_to_email
} )}
onChange={linkEvent( onChange={linkEvent(
this, this,
this.handleSendNotificationsToEmailChange this.handleSendNotificationsToEmailChange
@ -844,31 +891,31 @@ export class Settings extends Component<any, SettingsState> {
handleBlockPerson(personId: number) { handleBlockPerson(personId: number) {
if (personId != 0) { if (personId != 0) {
let blockUserForm: BlockPerson = { let blockUserForm = new BlockPerson({
person_id: personId, person_id: personId,
block: true, block: true,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm)); WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
} }
} }
handleUnblockPerson(i: { ctx: Settings; recipientId: number }) { handleUnblockPerson(i: { ctx: Settings; recipientId: number }) {
let blockUserForm: BlockPerson = { let blockUserForm = new BlockPerson({
person_id: i.recipientId, person_id: i.recipientId,
block: false, block: false,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm)); WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
} }
handleBlockCommunity(community_id: number) { handleBlockCommunity(community_id: number) {
if (community_id != 0) { if (community_id != 0) {
let blockCommunityForm: BlockCommunity = { let blockCommunityForm = new BlockCommunity({
community_id, community_id,
block: true, block: true,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.blockCommunity(blockCommunityForm) wsClient.blockCommunity(blockCommunityForm)
); );
@ -876,126 +923,133 @@ export class Settings extends Component<any, SettingsState> {
} }
handleUnblockCommunity(i: { ctx: Settings; communityId: number }) { handleUnblockCommunity(i: { ctx: Settings; communityId: number }) {
let blockCommunityForm: BlockCommunity = { let blockCommunityForm = new BlockCommunity({
community_id: i.communityId, community_id: i.communityId,
block: false, block: false,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm)); WebSocketService.Instance.send(wsClient.blockCommunity(blockCommunityForm));
} }
handleShowNsfwChange(i: Settings, event: any) { handleShowNsfwChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_nsfw = event.target.checked; i.state.saveUserSettingsForm.show_nsfw = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
} }
handleShowAvatarsChange(i: Settings, event: any) { handleShowAvatarsChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_avatars = event.target.checked; i.state.saveUserSettingsForm.show_avatars = Some(event.target.checked);
UserService.Instance.myUserInfo.local_user_view.local_user.show_avatars = UserService.Instance.myUserInfo.match({
event.target.checked; // Just for instant updates some: mui =>
(mui.local_user_view.local_user.show_avatars = event.target.checked),
none: void 0,
});
i.setState(i.state); i.setState(i.state);
} }
handleBotAccount(i: Settings, event: any) { handleBotAccount(i: Settings, event: any) {
i.state.saveUserSettingsForm.bot_account = event.target.checked; i.state.saveUserSettingsForm.bot_account = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
} }
handleShowBotAccounts(i: Settings, event: any) { handleShowBotAccounts(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_bot_accounts = event.target.checked; i.state.saveUserSettingsForm.show_bot_accounts = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
} }
handleReadPosts(i: Settings, event: any) { handleReadPosts(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_read_posts = event.target.checked; i.state.saveUserSettingsForm.show_read_posts = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
} }
handleShowNewPostNotifs(i: Settings, event: any) { handleShowNewPostNotifs(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_new_post_notifs = event.target.checked; i.state.saveUserSettingsForm.show_new_post_notifs = Some(
event.target.checked
);
i.setState(i.state); i.setState(i.state);
} }
handleShowScoresChange(i: Settings, event: any) { handleShowScoresChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.show_scores = event.target.checked; i.state.saveUserSettingsForm.show_scores = Some(event.target.checked);
UserService.Instance.myUserInfo.local_user_view.local_user.show_scores = UserService.Instance.myUserInfo.match({
event.target.checked; // Just for instant updates some: mui =>
(mui.local_user_view.local_user.show_scores = event.target.checked),
none: void 0,
});
i.setState(i.state); i.setState(i.state);
} }
handleSendNotificationsToEmailChange(i: Settings, event: any) { handleSendNotificationsToEmailChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.send_notifications_to_email = i.state.saveUserSettingsForm.send_notifications_to_email = Some(
event.target.checked; event.target.checked
);
i.setState(i.state); i.setState(i.state);
} }
handleThemeChange(i: Settings, event: any) { handleThemeChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.theme = event.target.value; i.state.saveUserSettingsForm.theme = Some(event.target.value);
setTheme(event.target.value, true); setTheme(event.target.value, true);
i.setState(i.state); i.setState(i.state);
} }
handleLangChange(i: Settings, event: any) { handleLangChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.lang = event.target.value; i.state.saveUserSettingsForm.lang = Some(event.target.value);
i18n.changeLanguage(getLanguages(i.state.saveUserSettingsForm.lang)[0]); i18n.changeLanguage(
getLanguages(i.state.saveUserSettingsForm.lang.unwrap())[0]
);
i.setState(i.state); i.setState(i.state);
} }
handleSortTypeChange(val: SortType) { handleSortTypeChange(val: SortType) {
this.state.saveUserSettingsForm.default_sort_type = this.state.saveUserSettingsForm.default_sort_type = Some(
Object.keys(SortType).indexOf(val); Object.keys(SortType).indexOf(val)
);
this.setState(this.state); this.setState(this.state);
} }
handleListingTypeChange(val: ListingType) { handleListingTypeChange(val: ListingType) {
this.state.saveUserSettingsForm.default_listing_type = this.state.saveUserSettingsForm.default_listing_type = Some(
Object.keys(ListingType).indexOf(val); Object.keys(ListingType).indexOf(val)
);
this.setState(this.state); this.setState(this.state);
} }
handleEmailChange(i: Settings, event: any) { handleEmailChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.email = event.target.value; i.state.saveUserSettingsForm.email = Some(event.target.value);
i.setState(i.state); i.setState(i.state);
} }
handleBioChange(val: string) { handleBioChange(val: string) {
this.state.saveUserSettingsForm.bio = val; this.state.saveUserSettingsForm.bio = Some(val);
this.setState(this.state); this.setState(this.state);
} }
handleAvatarUpload(url: string) { handleAvatarUpload(url: string) {
this.state.saveUserSettingsForm.avatar = url; this.state.saveUserSettingsForm.avatar = Some(url);
this.setState(this.state); this.setState(this.state);
} }
handleAvatarRemove() { handleAvatarRemove() {
this.state.saveUserSettingsForm.avatar = ""; this.state.saveUserSettingsForm.avatar = Some("");
this.setState(this.state); this.setState(this.state);
} }
handleBannerUpload(url: string) { handleBannerUpload(url: string) {
this.state.saveUserSettingsForm.banner = url; this.state.saveUserSettingsForm.banner = Some(url);
this.setState(this.state); this.setState(this.state);
} }
handleBannerRemove() { handleBannerRemove() {
this.state.saveUserSettingsForm.banner = ""; this.state.saveUserSettingsForm.banner = Some("");
this.setState(this.state); this.setState(this.state);
} }
handleDisplayNameChange(i: Settings, event: any) { handleDisplayNameChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.display_name = event.target.value; i.state.saveUserSettingsForm.display_name = Some(event.target.value);
i.setState(i.state); i.setState(i.state);
} }
handleMatrixUserIdChange(i: Settings, event: any) { handleMatrixUserIdChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.matrix_user_id = event.target.value; i.state.saveUserSettingsForm.matrix_user_id = Some(event.target.value);
if (
i.state.saveUserSettingsForm.matrix_user_id == "" &&
!UserService.Instance.myUserInfo.local_user_view.person.matrix_user_id
) {
i.state.saveUserSettingsForm.matrix_user_id = undefined;
}
i.setState(i.state); i.setState(i.state);
} }
@ -1026,6 +1080,7 @@ 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.state.saveUserSettingsLoading = true;
i.state.saveUserSettingsForm.auth = auth().unwrap();
i.setState(i.state); i.setState(i.state);
WebSocketService.Instance.send( WebSocketService.Instance.send(
@ -1036,6 +1091,7 @@ export class Settings extends Component<any, SettingsState> {
handleChangePasswordSubmit(i: Settings, event: any) { handleChangePasswordSubmit(i: Settings, event: any) {
event.preventDefault(); event.preventDefault();
i.state.changePasswordLoading = true; i.state.changePasswordLoading = true;
i.state.changePasswordForm.auth = auth().unwrap();
i.setState(i.state); i.setState(i.state);
WebSocketService.Instance.send( WebSocketService.Instance.send(
@ -1057,6 +1113,7 @@ 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.state.deleteAccountLoading = true;
i.state.deleteAccountForm.auth = auth().unwrap();
i.setState(i.state); i.setState(i.state);
WebSocketService.Instance.send( WebSocketService.Instance.send(
@ -1074,36 +1131,55 @@ export class Settings extends Component<any, SettingsState> {
} }
setUserInfo() { setUserInfo() {
let luv = UserService.Instance.myUserInfo.local_user_view; UserService.Instance.myUserInfo.match({
this.state.saveUserSettingsForm.show_nsfw = luv.local_user.show_nsfw; some: mui => {
this.state.saveUserSettingsForm.theme = luv.local_user.theme let luv = mui.local_user_view;
? luv.local_user.theme this.state.saveUserSettingsForm.show_nsfw = Some(
: "browser"; luv.local_user.show_nsfw
this.state.saveUserSettingsForm.default_sort_type = );
luv.local_user.default_sort_type; this.state.saveUserSettingsForm.theme = Some(
this.state.saveUserSettingsForm.default_listing_type = luv.local_user.theme ? luv.local_user.theme : "browser"
luv.local_user.default_listing_type; );
this.state.saveUserSettingsForm.lang = luv.local_user.lang; this.state.saveUserSettingsForm.default_sort_type = Some(
this.state.saveUserSettingsForm.avatar = luv.person.avatar; luv.local_user.default_sort_type
this.state.saveUserSettingsForm.banner = luv.person.banner; );
this.state.saveUserSettingsForm.display_name = luv.person.display_name; this.state.saveUserSettingsForm.default_listing_type = Some(
this.state.saveUserSettingsForm.show_avatars = luv.local_user.show_avatars; luv.local_user.default_listing_type
this.state.saveUserSettingsForm.bot_account = luv.person.bot_account; );
this.state.saveUserSettingsForm.show_bot_accounts = this.state.saveUserSettingsForm.lang = Some(luv.local_user.lang);
luv.local_user.show_bot_accounts; this.state.saveUserSettingsForm.avatar = luv.person.avatar;
this.state.saveUserSettingsForm.show_scores = luv.local_user.show_scores; this.state.saveUserSettingsForm.banner = luv.person.banner;
this.state.saveUserSettingsForm.show_read_posts = this.state.saveUserSettingsForm.display_name = luv.person.display_name;
luv.local_user.show_read_posts; this.state.saveUserSettingsForm.show_avatars = Some(
this.state.saveUserSettingsForm.show_new_post_notifs = luv.local_user.show_avatars
luv.local_user.show_new_post_notifs; );
this.state.saveUserSettingsForm.email = luv.local_user.email; this.state.saveUserSettingsForm.bot_account = Some(
this.state.saveUserSettingsForm.bio = luv.person.bio; luv.person.bot_account
this.state.saveUserSettingsForm.send_notifications_to_email = );
luv.local_user.send_notifications_to_email; this.state.saveUserSettingsForm.show_bot_accounts = Some(
this.state.saveUserSettingsForm.matrix_user_id = luv.person.matrix_user_id; luv.local_user.show_bot_accounts
this.state.personBlocks = UserService.Instance.myUserInfo.person_blocks; );
this.state.communityBlocks = this.state.saveUserSettingsForm.show_scores = Some(
UserService.Instance.myUserInfo.community_blocks; 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) {
@ -1118,14 +1194,14 @@ export class Settings extends Component<any, SettingsState> {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
return; return;
} else if (op == UserOperation.SaveUserSettings) { } else if (op == UserOperation.SaveUserSettings) {
let data = wsJsonToRes<LoginResponse>(msg).data; let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
UserService.Instance.login(data); UserService.Instance.login(data);
this.state.saveUserSettingsLoading = false; this.state.saveUserSettingsLoading = false;
this.setState(this.state); this.setState(this.state);
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).data; let data = wsJsonToRes<LoginResponse>(msg, LoginResponse);
UserService.Instance.login(data); UserService.Instance.login(data);
this.state.changePasswordLoading = false; this.state.changePasswordLoading = false;
this.setState(this.state); this.setState(this.state);
@ -1138,13 +1214,21 @@ export class Settings extends Component<any, SettingsState> {
}); });
UserService.Instance.logout(); UserService.Instance.logout();
window.location.href = "/"; window.location.href = "/";
location.reload();
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data; let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
this.setState({ personBlocks: updatePersonBlock(data) }); updatePersonBlock(data).match({
some: blocks => this.setState({ personBlocks: blocks }),
none: void 0,
});
} else if (op == UserOperation.BlockCommunity) { } else if (op == UserOperation.BlockCommunity) {
let data = wsJsonToRes<BlockCommunityResponse>(msg).data; let data = wsJsonToRes<BlockCommunityResponse>(
this.setState({ communityBlocks: updateCommunityBlock(data) }); msg,
BlockCommunityResponse
);
updateCommunityBlock(data).match({
some: blocks => this.setState({ communityBlocks: blocks }),
none: void 0,
});
} }
} }
} }

View file

@ -1,9 +1,12 @@
import { None } from "@sniptt/monads/build";
import { Component } from "inferno"; import { Component } from "inferno";
import { import {
SiteView, GetSiteResponse,
UserOperation, UserOperation,
VerifyEmail as VerifyEmailForm, VerifyEmail as VerifyEmailForm,
VerifyEmailResponse, VerifyEmailResponse,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
@ -13,15 +16,13 @@ import {
setIsoData, setIsoData,
toast, toast,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
interface State { interface State {
verifyEmailForm: VerifyEmailForm; verifyEmailForm: VerifyEmailForm;
site_view: SiteView; siteRes: GetSiteResponse;
} }
export class VerifyEmail extends Component<any, State> { export class VerifyEmail extends Component<any, State> {
@ -29,10 +30,10 @@ export class VerifyEmail extends Component<any, State> {
private subscription: Subscription; private subscription: Subscription;
emptyState: State = { emptyState: State = {
verifyEmailForm: { verifyEmailForm: new VerifyEmailForm({
token: this.props.match.params.token, token: this.props.match.params.token,
}, }),
site_view: this.isoData.site_res.site_view, siteRes: this.isoData.site_res,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -57,7 +58,10 @@ export class VerifyEmail extends Component<any, State> {
} }
get documentTitle(): string { get documentTitle(): string {
return `${i18n.t("verify_email")} - ${this.state.site_view.site.name}`; return this.state.siteRes.site_view.match({
some: siteView => `${i18n.t("verify_email")} - ${siteView.site.name}`,
none: "",
});
} }
render() { render() {
@ -66,6 +70,8 @@ export class VerifyEmail extends Component<any, State> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div class="col-12 col-lg-6 offset-lg-3 mb-4">
@ -85,7 +91,7 @@ export class VerifyEmail extends Component<any, State> {
this.props.history.push("/"); this.props.history.push("/");
return; return;
} else if (op == UserOperation.VerifyEmail) { } else if (op == UserOperation.VerifyEmail) {
let data = wsJsonToRes<VerifyEmailResponse>(msg).data; 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.state = this.emptyState;

View file

@ -1,48 +1,50 @@
import { Either, Left, None, Option, Right, Some } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { import {
CommunityView,
GetCommunity, GetCommunity,
GetCommunityResponse, GetCommunityResponse,
GetSiteResponse,
ListCommunities, ListCommunities,
ListCommunitiesResponse, ListCommunitiesResponse,
ListingType, ListingType,
PostView, PostView,
SiteView,
SortType, SortType,
toOption,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { InitialFetchRequest, PostFormParams } from "shared/interfaces"; import { InitialFetchRequest, PostFormParams } from "shared/interfaces";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, auth,
enableDownvotes,
enableNsfw,
fetchLimit, fetchLimit,
isBrowser, isBrowser,
setIsoData, setIsoData,
setOptionalAuth,
toast, toast,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import { PostForm } from "./post-form"; import { PostForm } from "./post-form";
interface CreatePostState { interface CreatePostState {
site_view: SiteView; listCommunitiesResponse: Option<ListCommunitiesResponse>;
communities: CommunityView[]; siteRes: GetSiteResponse;
loading: boolean; loading: boolean;
} }
export class CreatePost extends Component<any, CreatePostState> { export class CreatePost extends Component<any, CreatePostState> {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context, ListCommunitiesResponse);
private subscription: Subscription; private subscription: Subscription;
private emptyState: CreatePostState = { private emptyState: CreatePostState = {
site_view: this.isoData.site_res.site_view, siteRes: this.isoData.site_res,
communities: [], listCommunitiesResponse: None,
loading: true, loading: true,
}; };
@ -51,7 +53,7 @@ export class CreatePost extends Component<any, CreatePostState> {
this.handlePostCreate = this.handlePostCreate.bind(this); this.handlePostCreate = this.handlePostCreate.bind(this);
this.state = this.emptyState; this.state = this.emptyState;
if (!UserService.Instance.myUserInfo && 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`);
} }
@ -61,7 +63,9 @@ export class CreatePost extends Component<any, CreatePostState> {
// 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.communities = this.isoData.routeData[0].communities; this.state.listCommunitiesResponse = Some(
this.isoData.routeData[0] as ListCommunitiesResponse
);
this.state.loading = false; this.state.loading = false;
} else { } else {
this.refetch(); this.refetch();
@ -69,27 +73,39 @@ export class CreatePost extends Component<any, CreatePostState> {
} }
refetch() { refetch() {
if (this.params.community_id) { this.params.nameOrId.match({
let form: GetCommunity = { some: opt =>
id: this.params.community_id, opt.match({
}; left: name => {
WebSocketService.Instance.send(wsClient.getCommunity(form)); let form = new GetCommunity({
} else if (this.params.community_name) { name: Some(name),
let form: GetCommunity = { id: None,
name: this.params.community_name, auth: auth(false).ok(),
}; });
WebSocketService.Instance.send(wsClient.getCommunity(form)); WebSocketService.Instance.send(wsClient.getCommunity(form));
} else { },
let listCommunitiesForm: ListCommunities = { right: id => {
type_: ListingType.All, let form = new GetCommunity({
sort: SortType.TopAll, id: Some(id),
limit: fetchLimit, name: None,
auth: authField(false), auth: auth(false).ok(),
}; });
WebSocketService.Instance.send( WebSocketService.Instance.send(wsClient.getCommunity(form));
wsClient.listCommunities(listCommunitiesForm) },
); }),
} none: () => {
let listCommunitiesForm = new ListCommunities({
type_: Some(ListingType.All),
sort: Some(SortType.TopAll),
limit: Some(fetchLimit),
page: None,
auth: auth(false).ok(),
});
WebSocketService.Instance.send(
wsClient.listCommunities(listCommunitiesForm)
);
},
});
} }
componentWillUnmount() { componentWillUnmount() {
@ -99,7 +115,10 @@ export class CreatePost extends Component<any, CreatePostState> {
} }
get documentTitle(): string { get documentTitle(): string {
return `${i18n.t("create_post")} - ${this.state.site_view.site.name}`; return this.state.siteRes.site_view.match({
some: siteView => `${i18n.t("create_post")} - ${siteView.site.name}`,
none: "",
});
} }
render() { render() {
@ -108,24 +127,32 @@ export class CreatePost extends Component<any, CreatePostState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
<div class="row"> this.state.listCommunitiesResponse.match({
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> some: res => (
<h5>{i18n.t("create_post")}</h5> <div class="row">
<PostForm <div class="col-12 col-lg-6 offset-lg-3 mb-4">
communities={this.state.communities} <h5>{i18n.t("create_post")}</h5>
onCreate={this.handlePostCreate} <PostForm
params={this.params} post_view={None}
enableDownvotes={this.state.site_view.site.enable_downvotes} communities={Some(res.communities)}
enableNsfw={this.state.site_view.site.enable_nsfw} onCreate={this.handlePostCreate}
/> params={Some(this.params)}
</div> enableDownvotes={enableDownvotes(this.state.siteRes)}
</div> enableNsfw={enableNsfw(this.state.siteRes)}
/>
</div>
</div>
),
none: <></>,
})
)} )}
</div> </div>
); );
@ -133,36 +160,48 @@ export class CreatePost extends Component<any, CreatePostState> {
get params(): PostFormParams { get params(): PostFormParams {
let urlParams = new URLSearchParams(this.props.location.search); let urlParams = new URLSearchParams(this.props.location.search);
let name = toOption(urlParams.get("community_name")).or(
this.prevCommunityName
);
let id = toOption(urlParams.get("community_id"))
.map(Number)
.or(this.prevCommunityId);
let nameOrId: Option<Either<string, number>>;
if (name.isSome()) {
nameOrId = Some(Left(name.unwrap()));
} else if (id.isSome()) {
nameOrId = Some(Right(id.unwrap()));
} else {
nameOrId = None;
}
let params: PostFormParams = { let params: PostFormParams = {
name: urlParams.get("title"), name: toOption(urlParams.get("title")),
community_name: urlParams.get("community_name") || this.prevCommunityName, nameOrId,
community_id: urlParams.get("community_id") body: toOption(urlParams.get("body")),
? Number(urlParams.get("community_id")) || this.prevCommunityId url: toOption(urlParams.get("url")),
: null,
body: urlParams.get("body"),
url: urlParams.get("url"),
}; };
return params; return params;
} }
get prevCommunityName(): string { get prevCommunityName(): Option<string> {
if (this.props.match.params.name) { if (this.props.match.params.name) {
return this.props.match.params.name; return toOption(this.props.match.params.name);
} else if (this.props.location.state) { } else if (this.props.location.state) {
let lastLocation = this.props.location.state.prevPath; let lastLocation = this.props.location.state.prevPath;
if (lastLocation.includes("/c/")) { if (lastLocation.includes("/c/")) {
return lastLocation.split("/c/")[1]; return toOption(lastLocation.split("/c/")[1]);
} }
} }
return null; return None;
} }
get prevCommunityId(): number { get prevCommunityId(): Option<number> {
if (this.props.match.params.id) { if (this.props.match.params.id) {
return this.props.match.params.id; return toOption(this.props.match.params.id);
} }
return null; return None;
} }
handlePostCreate(post_view: PostView) { handlePostCreate(post_view: PostView) {
@ -170,12 +209,13 @@ export class CreatePost extends Component<any, CreatePostState> {
} }
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let listCommunitiesForm: ListCommunities = { let listCommunitiesForm = new ListCommunities({
type_: ListingType.All, type_: Some(ListingType.All),
sort: SortType.TopAll, sort: Some(SortType.TopAll),
limit: fetchLimit, limit: Some(fetchLimit),
}; page: None,
setOptionalAuth(listCommunitiesForm, req.auth); auth: req.auth,
});
return [req.client.listCommunities(listCommunitiesForm)]; return [req.client.listCommunities(listCommunitiesForm)];
} }
@ -186,13 +226,18 @@ export class CreatePost extends Component<any, CreatePostState> {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
return; return;
} else if (op == UserOperation.ListCommunities) { } else if (op == UserOperation.ListCommunities) {
let data = wsJsonToRes<ListCommunitiesResponse>(msg).data; let data = wsJsonToRes<ListCommunitiesResponse>(
this.state.communities = data.communities; msg,
ListCommunitiesResponse
);
this.state.listCommunitiesResponse = Some(data);
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.GetCommunity) { } else if (op == UserOperation.GetCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg).data; let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
this.state.communities = [data.community_view]; this.state.listCommunitiesResponse = Some({
communities: [data.community_view],
});
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
} }

View file

@ -29,56 +29,71 @@ export class MetadataCard extends Component<
let post = this.props.post; let post = this.props.post;
return ( return (
<> <>
{post.embed_title && !this.state.expanded && ( {!this.state.expanded &&
<div class="card border-secondary mt-3 mb-2"> post.embed_title.match({
<div class="row"> some: embedTitle =>
<div class="col-12"> post.url.match({
<div class="card-body"> some: url => (
{post.name !== post.embed_title && [ <div class="card border-secondary mt-3 mb-2">
<h5 class="card-title d-inline"> <div class="row">
<a class="text-body" href={post.url} rel={relTags}> <div class="col-12">
{post.embed_title} <div class="card-body">
</a> {post.name !== embedTitle && [
</h5>, <h5 class="card-title d-inline">
<span class="d-inline-block ml-2 mb-2 small text-muted"> <a class="text-body" href={url} rel={relTags}>
<a {embedTitle}
class="text-muted font-italic" </a>
href={post.url} </h5>,
rel={relTags} <span class="d-inline-block ml-2 mb-2 small text-muted">
> <a
{new URL(post.url).hostname} class="text-muted font-italic"
<Icon icon="external-link" classes="ml-1" /> href={url}
</a> rel={relTags}
</span>, >
]} {new URL(url).hostname}
{post.embed_description && ( <Icon icon="external-link" classes="ml-1" />
<div </a>
className="card-text small text-muted md-div" </span>,
dangerouslySetInnerHTML={{ ]}
__html: post.embed_description, {post.embed_description.match({
}} some: desc => (
/> <div
)} className="card-text small text-muted md-div"
{post.embed_html && ( dangerouslySetInnerHTML={{
<button __html: desc,
class="mt-2 btn btn-secondary text-monospace" }}
onClick={linkEvent(this, this.handleIframeExpand)} />
data-tippy-content={i18n.t("expand_here")} ),
> none: <></>,
{this.state.expanded ? "-" : "+"} })}
</button> {post.embed_html.isSome() && (
)} <button
</div> class="mt-2 btn btn-secondary text-monospace"
</div> onClick={linkEvent(this, this.handleIframeExpand)}
</div> data-tippy-content={i18n.t("expand_here")}
</div> >
)} {this.state.expanded ? "-" : "+"}
{this.state.expanded && ( </button>
<div )}
class="mt-3 mb-2" </div>
dangerouslySetInnerHTML={{ __html: post.embed_html }} </div>
/> </div>
)} </div>
),
none: <></>,
}),
none: <></>,
})}
{this.state.expanded &&
post.embed_html.match({
some: html => (
<div
class="mt-3 mb-2"
dangerouslySetInnerHTML={{ __html: html }}
/>
),
none: <></>,
})}
</> </>
); );
} }

View file

@ -1,3 +1,4 @@
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";
@ -12,7 +13,10 @@ import {
SearchResponse, SearchResponse,
SearchType, SearchType,
SortType, SortType,
toUndefined,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { pictrsUri } from "../../env"; import { pictrsUri } from "../../env";
@ -21,7 +25,7 @@ import { PostFormParams } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
archiveTodayUrl, archiveTodayUrl,
authField, auth,
capitalizeFirstLetter, capitalizeFirstLetter,
choicesConfig, choicesConfig,
communitySelectName, communitySelectName,
@ -36,13 +40,12 @@ import {
relTags, relTags,
setupTippy, setupTippy,
toast, toast,
trendingFetchLimit,
validTitle, validTitle,
validURL, validURL,
webArchiveUrl, webArchiveUrl,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
@ -56,42 +59,45 @@ if (isBrowser()) {
const MAX_POST_TITLE_LENGTH = 200; const MAX_POST_TITLE_LENGTH = 200;
interface PostFormProps { interface PostFormProps {
post_view?: 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
communities?: CommunityView[]; communities: Option<CommunityView[]>;
params?: PostFormParams; params: Option<PostFormParams>;
onCancel?(): any; onCancel?(): any;
onCreate?(post: PostView): any; onCreate?(post: PostView): any;
onEdit?(post: PostView): any; onEdit?(post: PostView): any;
enableNsfw: boolean; enableNsfw?: boolean;
enableDownvotes: boolean; enableDownvotes?: boolean;
} }
interface PostFormState { interface PostFormState {
postForm: CreatePost; postForm: CreatePost;
suggestedTitle: Option<string>;
suggestedPosts: Option<PostView[]>;
crossPosts: Option<PostView[]>;
loading: boolean; loading: boolean;
imageLoading: boolean; imageLoading: boolean;
previewMode: boolean; previewMode: boolean;
suggestedTitle: string;
suggestedPosts: PostView[];
crossPosts: PostView[];
} }
export class PostForm extends Component<PostFormProps, PostFormState> { export class PostForm extends Component<PostFormProps, PostFormState> {
private subscription: Subscription; private subscription: Subscription;
private choices: any; private choices: any;
private emptyState: PostFormState = { private emptyState: PostFormState = {
postForm: { postForm: new CreatePost({
community_id: null, community_id: undefined,
name: null, name: undefined,
nsfw: false, nsfw: Some(false),
auth: authField(false), url: None,
}, body: None,
honeypot: None,
auth: undefined,
}),
loading: false, loading: false,
imageLoading: false, imageLoading: false,
previewMode: false, previewMode: false,
suggestedTitle: undefined, suggestedTitle: None,
suggestedPosts: [], suggestedPosts: None,
crossPosts: [], crossPosts: None,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -103,26 +109,28 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.state = this.emptyState; this.state = this.emptyState;
// Means its an edit // Means its an edit
if (this.props.post_view) { this.props.post_view.match({
this.state.postForm = { some: pv =>
body: this.props.post_view.post.body, (this.state.postForm = new CreatePost({
name: this.props.post_view.post.name, body: pv.post.body,
community_id: this.props.post_view.community.id, name: pv.post.name,
url: this.props.post_view.post.url, community_id: pv.community.id,
nsfw: this.props.post_view.post.nsfw, url: pv.post.url,
auth: authField(), nsfw: Some(pv.post.nsfw),
}; honeypot: None,
} auth: auth().unwrap(),
})),
none: void 0,
});
if (this.props.params) { this.props.params.match({
this.state.postForm.name = this.props.params.name; some: params => {
if (this.props.params.url) { this.state.postForm.name = toUndefined(params.name);
this.state.postForm.url = this.props.params.url; this.state.postForm.url = params.url;
} this.state.postForm.body = params.body;
if (this.props.params.body) { },
this.state.postForm.body = this.props.params.body; none: void 0,
} });
}
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
@ -141,8 +149,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
if ( if (
!this.state.loading && !this.state.loading &&
(this.state.postForm.name || (this.state.postForm.name ||
this.state.postForm.url || this.state.postForm.url.isSome() ||
this.state.postForm.body) this.state.postForm.body.isSome())
) { ) {
window.onbeforeunload = () => true; window.onbeforeunload = () => true;
} else { } else {
@ -163,8 +171,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
when={ when={
!this.state.loading && !this.state.loading &&
(this.state.postForm.name || (this.state.postForm.name ||
this.state.postForm.url || this.state.postForm.url.isSome() ||
this.state.postForm.body) this.state.postForm.body.isSome())
} }
message={i18n.t("block_leaving")} message={i18n.t("block_leaving")}
/> />
@ -178,26 +186,29 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
type="url" type="url"
id="post-url" id="post-url"
class="form-control" class="form-control"
value={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)}
/> />
{this.state.suggestedTitle && ( {this.state.suggestedTitle.match({
<div some: title => (
class="mt-1 text-muted small font-weight-bold pointer" <div
role="button" class="mt-1 text-muted small font-weight-bold pointer"
onClick={linkEvent(this, this.copySuggestedTitle)} role="button"
> onClick={linkEvent(this, this.copySuggestedTitle)}
{i18n.t("copy_suggested_title", { >
title: this.state.suggestedTitle, {i18n.t("copy_suggested_title", {
})} title,
</div> })}
)} </div>
),
none: <></>,
})}
<form> <form>
<label <label
htmlFor="file-upload" htmlFor="file-upload"
className={`${ className={`${
UserService.Instance.myUserInfo && "pointer" UserService.Instance.myUserInfo.isSome() && "pointer"
} d-inline-block float-right text-muted font-weight-bold`} } d-inline-block float-right text-muted font-weight-bold`}
data-tippy-content={i18n.t("upload_image")} data-tippy-content={i18n.t("upload_image")}
> >
@ -209,58 +220,68 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
accept="image/*,video/*" accept="image/*,video/*"
name="file" name="file"
class="d-none" class="d-none"
disabled={!UserService.Instance.myUserInfo} disabled={UserService.Instance.myUserInfo.isNone()}
onChange={linkEvent(this, this.handleImageUpload)} onChange={linkEvent(this, this.handleImageUpload)}
/> />
</form> </form>
{this.state.postForm.url && validURL(this.state.postForm.url) && ( {this.state.postForm.url.match({
<div> some: url =>
<a validURL(url) && (
href={`${webArchiveUrl}/save/${encodeURIComponent( <div>
this.state.postForm.url <a
)}`} href={`${webArchiveUrl}/save/${encodeURIComponent(
class="mr-2 d-inline-block float-right text-muted small font-weight-bold" url
rel={relTags} )}`}
> class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
archive.org {i18n.t("archive_link")} rel={relTags}
</a> >
<a archive.org {i18n.t("archive_link")}
href={`${ghostArchiveUrl}/search?term=${encodeURIComponent( </a>
this.state.postForm.url <a
)}`} href={`${ghostArchiveUrl}/search?term=${encodeURIComponent(
class="mr-2 d-inline-block float-right text-muted small font-weight-bold" url
rel={relTags} )}`}
> class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
ghostarchive.org {i18n.t("archive_link")} rel={relTags}
</a> >
<a ghostarchive.org {i18n.t("archive_link")}
href={`${archiveTodayUrl}/?run=1&url=${encodeURIComponent( </a>
this.state.postForm.url <a
)}`} href={`${archiveTodayUrl}/?run=1&url=${encodeURIComponent(
class="mr-2 d-inline-block float-right text-muted small font-weight-bold" url
rel={relTags} )}`}
> class="mr-2 d-inline-block float-right text-muted small font-weight-bold"
archive.today {i18n.t("archive_link")} rel={relTags}
</a> >
</div> archive.today {i18n.t("archive_link")}
)} </a>
</div>
),
none: <></>,
})}
{this.state.imageLoading && <Spinner />} {this.state.imageLoading && <Spinner />}
{isImage(this.state.postForm.url) && ( {this.state.postForm.url.match({
<img src={this.state.postForm.url} class="img-fluid" alt="" /> some: url =>
)} isImage(url) && <img src={url} class="img-fluid" alt="" />,
{this.state.crossPosts.length > 0 && ( none: <></>,
<> })}
<div class="my-1 text-muted small font-weight-bold"> {this.state.crossPosts.match({
{i18n.t("cross_posts")} some: xPosts =>
</div> xPosts.length > 0 && (
<PostListings <>
showCommunity <div class="my-1 text-muted small font-weight-bold">
posts={this.state.crossPosts} {i18n.t("cross_posts")}
enableDownvotes={this.props.enableDownvotes} </div>
enableNsfw={this.props.enableNsfw} <PostListings
/> showCommunity
</> posts={xPosts}
)} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/>
</>
),
none: <></>,
})}
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -285,18 +306,22 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
{i18n.t("invalid_post_title")} {i18n.t("invalid_post_title")}
</div> </div>
)} )}
{this.state.suggestedPosts.length > 0 && ( {this.state.suggestedPosts.match({
<> some: sPosts =>
<div class="my-1 text-muted small font-weight-bold"> sPosts.length > 0 && (
{i18n.t("related_posts")} <>
</div> <div class="my-1 text-muted small font-weight-bold">
<PostListings {i18n.t("related_posts")}
posts={this.state.suggestedPosts} </div>
enableDownvotes={this.props.enableDownvotes} <PostListings
enableNsfw={this.props.enableNsfw} posts={sPosts}
/> enableDownvotes={this.props.enableDownvotes}
</> enableNsfw={this.props.enableNsfw}
)} />
</>
),
none: <></>,
})}
</div> </div>
</div> </div>
@ -306,10 +331,13 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.postForm.body} initialContent={this.state.postForm.body}
onContentChange={this.handlePostBodyChange} onContentChange={this.handlePostBodyChange}
placeholder={None}
buttonTitle={None}
maxLength={None}
/> />
</div> </div>
</div> </div>
{!this.props.post_view && ( {this.props.post_view.isNone() && (
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label" htmlFor="post-community"> <label class="col-sm-2 col-form-label" htmlFor="post-community">
{i18n.t("community")} {i18n.t("community")}
@ -322,7 +350,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
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.map(cv => ( {this.props.communities.unwrapOr([]).map(cv => (
<option value={cv.community.id}> <option value={cv.community.id}>
{communitySelectName(cv)} {communitySelectName(cv)}
</option> </option>
@ -342,7 +370,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
class="form-check-input position-static" class="form-check-input position-static"
id="post-nsfw" id="post-nsfw"
type="checkbox" type="checkbox"
checked={this.state.postForm.nsfw} checked={toUndefined(this.state.postForm.nsfw)}
onChange={linkEvent(this, this.handlePostNsfwChange)} onChange={linkEvent(this, this.handlePostNsfwChange)}
/> />
</div> </div>
@ -356,7 +384,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
type="text" type="text"
class="form-control honeypot" class="form-control honeypot"
id="register-honey" id="register-honey"
value={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 class="form-group row">
@ -370,13 +398,13 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
> >
{this.state.loading ? ( {this.state.loading ? (
<Spinner /> <Spinner />
) : this.props.post_view ? ( ) : this.props.post_view.isSome() ? (
capitalizeFirstLetter(i18n.t("save")) capitalizeFirstLetter(i18n.t("save"))
) : ( ) : (
capitalizeFirstLetter(i18n.t("create")) capitalizeFirstLetter(i18n.t("create"))
)} )}
</button> </button>
{this.props.post_view && ( {this.props.post_view.isSome() && (
<button <button
type="button" type="button"
class="btn btn-secondary" class="btn btn-secondary"
@ -396,65 +424,87 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
event.preventDefault(); event.preventDefault();
// Coerce empty url string to undefined // Coerce empty url string to undefined
if (i.state.postForm.url !== undefined && i.state.postForm.url === "") { if (
i.state.postForm.url = undefined; i.state.postForm.url.isSome() &&
i.state.postForm.url.unwrapOr("blank") === ""
) {
i.state.postForm.url = None;
} }
if (i.props.post_view) { let pForm = i.state.postForm;
let form: EditPost = { i.props.post_view.match({
...i.state.postForm, some: pv => {
post_id: i.props.post_view.post.id, let form = new EditPost({
}; name: Some(pForm.name),
WebSocketService.Instance.send(wsClient.editPost(form)); url: pForm.url,
} else { body: pForm.body,
WebSocketService.Instance.send(wsClient.createPost(i.state.postForm)); nsfw: pForm.nsfw,
} post_id: pv.post.id,
auth: auth().unwrap(),
});
WebSocketService.Instance.send(wsClient.editPost(form));
},
none: () => {
i.state.postForm.auth = auth().unwrap();
WebSocketService.Instance.send(wsClient.createPost(i.state.postForm));
},
});
i.state.loading = true; i.state.loading = true;
i.setState(i.state); i.setState(i.state);
} }
copySuggestedTitle(i: PostForm) { copySuggestedTitle(i: PostForm) {
i.state.postForm.name = i.state.suggestedTitle.substring( i.state.suggestedTitle.match({
0, some: sTitle => {
MAX_POST_TITLE_LENGTH i.state.postForm.name = sTitle.substring(0, MAX_POST_TITLE_LENGTH);
); i.state.suggestedTitle = None;
i.state.suggestedTitle = undefined; 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);
i.setState(i.state); },
none: void 0,
});
} }
handlePostUrlChange(i: PostForm, event: any) { handlePostUrlChange(i: PostForm, event: any) {
i.state.postForm.url = event.target.value; i.state.postForm.url = Some(event.target.value);
i.setState(i.state); i.setState(i.state);
i.fetchPageTitle(); i.fetchPageTitle();
} }
fetchPageTitle() { fetchPageTitle() {
if (validURL(this.state.postForm.url)) { this.state.postForm.url.match({
let form: Search = { some: url => {
q: this.state.postForm.url, if (validURL(url)) {
type_: SearchType.Url, let form = new Search({
sort: SortType.TopAll, q: url,
listing_type: ListingType.All, community_id: None,
page: 1, community_name: None,
limit: 6, creator_id: None,
auth: authField(false), type_: Some(SearchType.Url),
}; sort: Some(SortType.TopAll),
listing_type: Some(ListingType.All),
page: Some(1),
limit: Some(trendingFetchLimit),
auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.search(form)); WebSocketService.Instance.send(wsClient.search(form));
// Fetch the page title // Fetch the page title
getSiteMetadata(this.state.postForm.url).then(d => { getSiteMetadata(url).then(d => {
this.state.suggestedTitle = d.metadata.title; this.state.suggestedTitle = d.metadata.title;
this.setState(this.state); this.setState(this.state);
}); });
} else { } else {
this.state.suggestedTitle = undefined; this.state.suggestedTitle = None;
this.state.crossPosts = []; this.state.crossPosts = None;
} }
},
none: void 0,
});
} }
handlePostNameChange(i: PostForm, event: any) { handlePostNameChange(i: PostForm, event: any) {
@ -464,28 +514,30 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} }
fetchSimilarPosts() { fetchSimilarPosts() {
let form: Search = { let form = new Search({
q: this.state.postForm.name, q: this.state.postForm.name,
type_: SearchType.Posts, type_: Some(SearchType.Posts),
sort: SortType.TopAll, sort: Some(SortType.TopAll),
listing_type: ListingType.All, listing_type: Some(ListingType.All),
community_id: this.state.postForm.community_id, community_id: Some(this.state.postForm.community_id),
page: 1, community_name: None,
limit: 6, creator_id: None,
auth: authField(false), page: Some(1),
}; limit: Some(trendingFetchLimit),
auth: auth(false).ok(),
});
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 = []; this.state.suggestedPosts = None;
} }
this.setState(this.state); this.setState(this.state);
} }
handlePostBodyChange(val: string) { handlePostBodyChange(val: string) {
this.state.postForm.body = val; this.state.postForm.body = Some(val);
this.setState(this.state); this.setState(this.state);
} }
@ -495,12 +547,12 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} }
handlePostNsfwChange(i: PostForm, event: any) { handlePostNsfwChange(i: PostForm, event: any) {
i.state.postForm.nsfw = event.target.checked; i.state.postForm.nsfw = Some(event.target.checked);
i.setState(i.state); i.setState(i.state);
} }
handleHoneyPotChange(i: PostForm, event: any) { handleHoneyPotChange(i: PostForm, event: any) {
i.state.postForm.honeypot = event.target.value; i.state.postForm.honeypot = Some(event.target.value);
i.setState(i.state); i.setState(i.state);
} }
@ -549,7 +601,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
let url = `${pictrsUri}/${hash}`; let url = `${pictrsUri}/${hash}`;
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 = url; i.state.postForm.url = Some(url);
i.state.imageLoading = false; i.state.imageLoading = false;
i.setState(i.state); i.setState(i.state);
pictrsDeleteToast( pictrsDeleteToast(
@ -606,30 +658,34 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} }
} }
if (this.props.post_view) { this.props.post_view.match({
this.state.postForm.community_id = this.props.post_view.community.id; some: pv => (this.state.postForm.community_id = pv.community.id),
} else if ( none: void 0,
this.props.params && });
(this.props.params.community_id || this.props.params.community_name) this.props.params.match({
) { some: params =>
if (this.props.params.community_name) { params.nameOrId.match({
let foundCommunityId = this.props.communities.find( some: nameOrId =>
r => r.community.name == this.props.params.community_name nameOrId.match({
).community.id; left: name => {
this.state.postForm.community_id = foundCommunityId; let foundCommunityId = this.props.communities
} else if (this.props.params.community_id) { .unwrapOr([])
this.state.postForm.community_id = this.props.params.community_id; .find(r => r.community.name == name).community.id;
} this.state.postForm.community_id = foundCommunityId;
},
right: id => (this.state.postForm.community_id = id),
}),
none: void 0,
}),
none: void 0,
});
if (isBrowser()) { if (isBrowser() && this.state.postForm.community_id) {
this.choices.setChoiceByValue( this.choices.setChoiceByValue(
this.state.postForm.community_id.toString() this.state.postForm.community_id.toString()
); );
}
this.setState(this.state);
} else {
// By default, the null valued 'Select a Community'
} }
this.setState(this.state);
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -642,30 +698,34 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.setState(this.state); this.setState(this.state);
return; return;
} else if (op == UserOperation.CreatePost) { } else if (op == UserOperation.CreatePost) {
let data = wsJsonToRes<PostResponse>(msg).data; let data = wsJsonToRes<PostResponse>(msg, PostResponse);
if ( UserService.Instance.myUserInfo.match({
data.post_view.creator.id == some: mui => {
UserService.Instance.myUserInfo.local_user_view.person.id if (data.post_view.creator.id == mui.local_user_view.person.id) {
) { this.state.loading = false;
this.state.loading = false; this.props.onCreate(data.post_view);
this.props.onCreate(data.post_view); }
} },
none: void 0,
});
} else if (op == UserOperation.EditPost) { } else if (op == UserOperation.EditPost) {
let data = wsJsonToRes<PostResponse>(msg).data; let data = wsJsonToRes<PostResponse>(msg, PostResponse);
if ( UserService.Instance.myUserInfo.match({
data.post_view.creator.id == some: mui => {
UserService.Instance.myUserInfo.local_user_view.person.id if (data.post_view.creator.id == mui.local_user_view.person.id) {
) { this.state.loading = false;
this.state.loading = false; this.props.onEdit(data.post_view);
this.props.onEdit(data.post_view); }
} },
none: void 0,
});
} else if (op == UserOperation.Search) { } else if (op == UserOperation.Search) {
let data = wsJsonToRes<SearchResponse>(msg).data; let data = wsJsonToRes<SearchResponse>(msg, SearchResponse);
if (data.type_ == SearchType[SearchType.Posts]) { if (data.type_ == SearchType[SearchType.Posts]) {
this.state.suggestedPosts = data.posts; this.state.suggestedPosts = Some(data.posts);
} else if (data.type_ == SearchType[SearchType.Url]) { } else if (data.type_ == SearchType[SearchType.Url]) {
this.state.crossPosts = data.posts; this.state.crossPosts = Some(data.posts);
} }
this.setState(this.state); this.setState(this.state);
} }

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
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";
@ -13,39 +14,30 @@ interface PostListingsProps {
enableNsfw: boolean; enableNsfw: boolean;
} }
interface PostListingsState { export class PostListings extends Component<PostListingsProps, any> {
posts: PostView[];
}
export class PostListings extends Component<
PostListingsProps,
PostListingsState
> {
duplicatesMap = new Map<number, PostView[]>(); duplicatesMap = new Map<number, PostView[]>();
private emptyState: PostListingsState = {
posts: [],
};
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState; }
if (this.props.removeDuplicates) {
this.state.posts = this.removeDuplicates(); get posts() {
} else { return this.props.removeDuplicates
this.state.posts = this.props.posts; ? this.removeDuplicates()
} : this.props.posts;
} }
render() { render() {
return ( return (
<div> <div>
{this.state.posts.length > 0 ? ( {this.posts.length > 0 ? (
this.state.posts.map(post_view => ( this.posts.map(post_view => (
<> <>
<PostListing <PostListing
post_view={post_view} post_view={post_view}
duplicates={this.duplicatesMap.get(post_view.post.id)} duplicates={Some(this.duplicatesMap.get(post_view.post.id))}
moderators={None}
admins={None}
showCommunity={this.props.showCommunity} showCommunity={this.props.showCommunity}
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
@ -56,7 +48,7 @@ export class PostListings extends Component<
) : ( ) : (
<> <>
<div>{i18n.t("no_posts")}</div> <div>{i18n.t("no_posts")}</div>
{this.props.showCommunity !== undefined && ( {this.props.showCommunity && (
<T i18nKey="subscribe_to_communities"> <T i18nKey="subscribe_to_communities">
#<Link to="/communities">#</Link> #<Link to="/communities">#</Link>
</T> </T>
@ -76,19 +68,20 @@ export class PostListings extends Component<
// Loop over the posts, find ones with same urls // Loop over the posts, find ones with same urls
for (let pv of posts) { for (let pv of posts) {
if ( !pv.post.deleted &&
pv.post.url &&
!pv.post.deleted &&
!pv.post.removed && !pv.post.removed &&
!pv.community.deleted && !pv.community.deleted &&
!pv.community.removed !pv.community.removed &&
) { pv.post.url.match({
if (!urlMap.get(pv.post.url)) { some: url => {
urlMap.set(pv.post.url, [pv]); if (!urlMap.get(url)) {
} else { urlMap.set(url, [pv]);
urlMap.get(pv.post.url).push(pv); } else {
} urlMap.get(url).push(pv);
} }
},
none: void 0,
});
} }
// Sort by oldest // Sort by oldest
@ -103,19 +96,22 @@ export class PostListings extends Component<
for (let i = 0; i < posts.length; i++) { for (let i = 0; i < posts.length; i++) {
let pv = posts[i]; let pv = posts[i];
if (pv.post.url) { pv.post.url.match({
let found = urlMap.get(pv.post.url); some: url => {
if (found) { let found = urlMap.get(url);
// If its the oldest, add if (found) {
if (pv.post.id == found[0].post.id) { // If its the oldest, add
this.duplicatesMap.set(pv.post.id, found.slice(1)); if (pv.post.id == found[0].post.id) {
this.duplicatesMap.set(pv.post.id, found.slice(1));
}
// Otherwise, delete it
else {
posts.splice(i--, 1);
}
} }
// Otherwise, delete it },
else { none: void 0,
posts.splice(i--, 1); });
}
}
}
} }
return posts; return posts;

View file

@ -1,9 +1,10 @@
import { None } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { PostReportView, PostView, ResolvePostReport } from "lemmy-js-client"; import { PostReportView, PostView, ResolvePostReport } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { authField, wsClient } from "../../utils"; import { auth, wsClient } from "../../utils";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
import { PostListing } from "./post-listing"; import { PostListing } from "./post-listing";
@ -45,6 +46,9 @@ export class PostReport extends Component<PostReportProps, any> {
<div> <div>
<PostListing <PostListing
post_view={pv} post_view={pv}
duplicates={None}
moderators={None}
admins={None}
showCommunity={true} showCommunity={true}
enableDownvotes={true} enableDownvotes={true}
enableNsfw={true} enableNsfw={true}
@ -56,21 +60,24 @@ export class PostReport extends Component<PostReportProps, any> {
<div> <div>
{i18n.t("reason")}: {r.post_report.reason} {i18n.t("reason")}: {r.post_report.reason}
</div> </div>
{r.resolver && ( {r.resolver.match({
<div> some: resolver => (
{r.post_report.resolved ? ( <div>
<T i18nKey="resolved_by"> {r.post_report.resolved ? (
# <T i18nKey="resolved_by">
<PersonListing person={r.resolver} /> #
</T> <PersonListing person={resolver} />
) : ( </T>
<T i18nKey="unresolved_by"> ) : (
# <T i18nKey="unresolved_by">
<PersonListing person={r.resolver} /> #
</T> <PersonListing person={resolver} />
)} </T>
</div> )}
)} </div>
),
none: <></>,
})}
<button <button
className="btn btn-link btn-animate text-muted py-0" className="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleResolveReport)} onClick={linkEvent(this, this.handleResolveReport)}
@ -89,11 +96,11 @@ export class PostReport extends Component<PostReportProps, any> {
} }
handleResolveReport(i: PostReport) { handleResolveReport(i: PostReport) {
let form: ResolvePostReport = { let form = new ResolvePostReport({
report_id: i.props.report.post_report.id, report_id: i.props.report.post_report.id,
resolved: !i.props.report.post_report.resolved, resolved: !i.props.report.post_report.resolved,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.resolvePostReport(form)); WebSocketService.Instance.send(wsClient.resolvePostReport(form));
} }
} }

View file

@ -1,3 +1,4 @@
import { None, Option, Right, Some } from "@sniptt/monads";
import autosize from "autosize"; import autosize from "autosize";
import { Component, createRef, linkEvent, RefObject } from "inferno"; import { Component, createRef, linkEvent, RefObject } from "inferno";
import { import {
@ -23,6 +24,8 @@ import {
SearchType, SearchType,
SortType, SortType,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
@ -34,13 +37,15 @@ import {
} from "../../interfaces"; } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, auth,
buildCommentsTree, buildCommentsTree,
commentsToFlatNodes, commentsToFlatNodes,
createCommentLikeRes, createCommentLikeRes,
createPostLikeRes, createPostLikeRes,
debounce, debounce,
editCommentRes, editCommentRes,
enableDownvotes,
enableNsfw,
getCommentIdFromProps, getCommentIdFromProps,
getIdFromProps, getIdFromProps,
insertCommentIntoTree, insertCommentIntoTree,
@ -50,14 +55,12 @@ import {
saveCommentRes, saveCommentRes,
saveScrollPosition, saveScrollPosition,
setIsoData, setIsoData,
setOptionalAuth,
setupTippy, setupTippy,
toast, toast,
trendingFetchLimit,
updatePersonBlock, updatePersonBlock,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { CommentForm } from "../comment/comment-form"; import { CommentForm } from "../comment/comment-form";
import { CommentNodes } from "../comment/comment-nodes"; import { CommentNodes } from "../comment/comment-nodes";
@ -69,7 +72,7 @@ import { PostListing } from "./post-listing";
const commentsShownInterval = 15; const commentsShownInterval = 15;
interface PostState { interface PostState {
postRes: GetPostResponse; postRes: Option<GetPostResponse>;
postId: number; postId: number;
commentTree: CommentNodeI[]; commentTree: CommentNodeI[];
commentId?: number; commentId?: number;
@ -77,7 +80,7 @@ interface PostState {
commentViewType: CommentViewType; commentViewType: CommentViewType;
scrolled?: boolean; scrolled?: boolean;
loading: boolean; loading: boolean;
crossPosts: PostView[]; crossPosts: Option<PostView[]>;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
commentSectionRef?: RefObject<HTMLDivElement>; commentSectionRef?: RefObject<HTMLDivElement>;
showSidebarMobile: boolean; showSidebarMobile: boolean;
@ -86,10 +89,10 @@ interface PostState {
export class Post extends Component<any, PostState> { export class Post extends Component<any, PostState> {
private subscription: Subscription; private subscription: Subscription;
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context, GetPostResponse);
private commentScrollDebounced: () => void; private commentScrollDebounced: () => void;
private emptyState: PostState = { private emptyState: PostState = {
postRes: null, postRes: None,
postId: getIdFromProps(this.props), postId: getIdFromProps(this.props),
commentTree: [], commentTree: [],
commentId: getCommentIdFromProps(this.props), commentId: getCommentIdFromProps(this.props),
@ -97,7 +100,7 @@ export class Post extends Component<any, PostState> {
commentViewType: CommentViewType.Tree, commentViewType: CommentViewType.Tree,
scrolled: false, scrolled: false,
loading: true, loading: true,
crossPosts: [], crossPosts: None,
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
commentSectionRef: null, commentSectionRef: null,
showSidebarMobile: false, showSidebarMobile: false,
@ -115,14 +118,24 @@ export class Post extends Component<any, PostState> {
// 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 = this.isoData.routeData[0]; this.state.postRes = Some(this.isoData.routeData[0] as GetPostResponse);
this.state.commentTree = buildCommentsTree( this.state.commentTree = buildCommentsTree(
this.state.postRes.comments, this.state.postRes.unwrap().comments,
this.state.commentSort this.state.commentSort
); );
this.state.loading = false; this.state.loading = false;
if (isBrowser()) { if (isBrowser()) {
WebSocketService.Instance.send(
wsClient.communityJoin({
community_id:
this.state.postRes.unwrap().community_view.community.id,
})
);
WebSocketService.Instance.send(
wsClient.postJoin({ post_id: this.state.postId })
);
this.fetchCrossPosts(); this.fetchCrossPosts();
if (this.state.commentId) { if (this.state.commentId) {
this.scrollCommentIntoView(); this.scrollCommentIntoView();
@ -138,42 +151,47 @@ export class Post extends Component<any, PostState> {
} }
fetchPost() { fetchPost() {
let form: GetPost = { let form = new GetPost({
id: this.state.postId, id: this.state.postId,
auth: authField(false), auth: auth(false).ok(),
}; });
WebSocketService.Instance.send(wsClient.getPost(form)); WebSocketService.Instance.send(wsClient.getPost(form));
} }
fetchCrossPosts() { fetchCrossPosts() {
if (this.state.postRes.post_view.post.url) { this.state.postRes
let form: Search = { .andThen(r => r.post_view.post.url)
q: this.state.postRes.post_view.post.url, .match({
type_: SearchType.Url, some: url => {
sort: SortType.TopAll, let form = new Search({
listing_type: ListingType.All, q: url,
page: 1, type_: Some(SearchType.Url),
limit: 6, sort: Some(SortType.TopAll),
auth: authField(false), listing_type: Some(ListingType.All),
}; page: Some(1),
WebSocketService.Instance.send(wsClient.search(form)); limit: Some(trendingFetchLimit),
} community_id: None,
community_name: None,
creator_id: None,
auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.search(form));
},
none: void 0,
});
} }
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let pathSplit = req.path.split("/"); let pathSplit = req.path.split("/");
let promises: Promise<any>[] = [];
let id = Number(pathSplit[2]); let id = Number(pathSplit[2]);
let postForm: GetPost = { let postForm = new GetPost({
id, id,
}; auth: req.auth,
setOptionalAuth(postForm, req.auth); });
promises.push(req.client.getPost(postForm)); return [req.client.getPost(postForm)];
return promises;
} }
componentWillUnmount() { componentWillUnmount() {
@ -185,9 +203,6 @@ export class Post extends Component<any, PostState> {
} }
componentDidMount() { componentDidMount() {
WebSocketService.Instance.send(
wsClient.postJoin({ post_id: this.state.postId })
);
autosize(document.querySelectorAll("textarea")); autosize(document.querySelectorAll("textarea"));
this.commentScrollDebounced = debounce(this.trackCommentsBoxScrolling, 100); this.commentScrollDebounced = debounce(this.trackCommentsBoxScrolling, 100);
@ -231,34 +246,38 @@ export class Post extends Component<any, PostState> {
// TODO this needs some re-work // TODO this needs some re-work
markScrolledAsRead(commentId: number) { markScrolledAsRead(commentId: number) {
let found = this.state.postRes.comments.find( this.state.postRes.match({
c => c.comment.id == commentId some: res => {
); let found = res.comments.find(c => c.comment.id == commentId);
let parent = this.state.postRes.comments.find( let parent = res.comments.find(
c => found.comment.parent_id == c.comment.id c => found.comment.parent_id.unwrapOr(0) == c.comment.id
); );
let parent_person_id = parent let parent_person_id = parent
? parent.creator.id ? parent.creator.id
: this.state.postRes.post_view.creator.id; : res.post_view.creator.id;
if ( UserService.Instance.myUserInfo.match({
UserService.Instance.myUserInfo && some: mui => {
UserService.Instance.myUserInfo.local_user_view.person.id == if (mui.local_user_view.person.id == parent_person_id) {
parent_person_id let form = new MarkCommentAsRead({
) { comment_id: found.comment.id,
let form: MarkCommentAsRead = { read: true,
comment_id: found.comment.id, auth: auth().unwrap(),
read: true, });
auth: authField(), WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
}; UserService.Instance.unreadInboxCountSub.next(
WebSocketService.Instance.send(wsClient.markCommentAsRead(form)); UserService.Instance.unreadInboxCountSub.value - 1
UserService.Instance.unreadInboxCountSub.next( );
UserService.Instance.unreadInboxCountSub.value - 1 }
); },
} none: void 0,
});
},
none: void 0,
});
} }
isBottom(el: Element) { isBottom(el: Element): boolean {
return el?.getBoundingClientRect().bottom <= window.innerHeight; return el?.getBoundingClientRect().bottom <= window.innerHeight;
} }
@ -274,23 +293,35 @@ export class Post extends Component<any, PostState> {
}; };
get documentTitle(): string { get documentTitle(): string {
return `${this.state.postRes.post_view.post.name} - ${this.state.siteRes.site_view.site.name}`; return this.state.postRes.match({
some: res =>
this.state.siteRes.site_view.match({
some: siteView =>
`${res.post_view.post.name} - ${siteView.site.name}`,
none: "",
}),
none: "",
});
} }
get imageTag(): string { get imageTag(): Option<string> {
let post = this.state.postRes.post_view.post; return this.state.postRes.match({
return ( some: res =>
post.thumbnail_url || res.post_view.post.thumbnail_url.or(
(post.url ? (isImage(post.url) ? post.url : undefined) : undefined) res.post_view.post.url.match({
); some: url => (isImage(url) ? Some(url) : None),
none: None,
})
),
none: None,
});
} }
get descriptionTag(): string { get descriptionTag(): Option<string> {
return this.state.postRes.post_view.post.body; return this.state.postRes.andThen(r => r.post_view.post.body);
} }
render() { render() {
let pv = this.state.postRes?.post_view;
return ( return (
<div class="container"> <div class="container">
{this.state.loading ? ( {this.state.loading ? (
@ -298,56 +329,59 @@ export class Post extends Component<any, PostState> {
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
<div class="row"> this.state.postRes.match({
<div class="col-12 col-md-8 mb-3"> some: res => (
<HtmlTags <div class="row">
title={this.documentTitle} <div class="col-12 col-md-8 mb-3">
path={this.context.router.route.match.url} <HtmlTags
image={this.imageTag} title={this.documentTitle}
description={this.descriptionTag} path={this.context.router.route.match.url}
/> image={this.imageTag}
<PostListing description={this.descriptionTag}
post_view={pv}
duplicates={this.state.crossPosts}
showBody
showCommunity
moderators={this.state.postRes.moderators}
admins={this.state.siteRes.admins}
enableDownvotes={
this.state.siteRes.site_view.site.enable_downvotes
}
enableNsfw={this.state.siteRes.site_view.site.enable_nsfw}
/>
<div ref={this.state.commentSectionRef} className="mb-2" />
<CommentForm
postId={this.state.postId}
disabled={pv.post.locked}
/>
<div class="d-block d-md-none">
<button
class="btn btn-secondary d-inline-block mb-2 mr-3"
onClick={linkEvent(this, this.handleShowSidebarMobile)}
>
{i18n.t("sidebar")}{" "}
<Icon
icon={
this.state.showSidebarMobile
? `minus-square`
: `plus-square`
}
classes="icon-inline"
/> />
</button> <PostListing
{this.state.showSidebarMobile && this.sidebar()} post_view={res.post_view}
duplicates={this.state.crossPosts}
showBody
showCommunity
moderators={Some(res.moderators)}
admins={Some(this.state.siteRes.admins)}
enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)}
/>
<div ref={this.state.commentSectionRef} className="mb-2" />
<CommentForm
node={Right(this.state.postId)}
disabled={res.post_view.post.locked}
/>
<div class="d-block d-md-none">
<button
class="btn btn-secondary d-inline-block mb-2 mr-3"
onClick={linkEvent(this, this.handleShowSidebarMobile)}
>
{i18n.t("sidebar")}{" "}
<Icon
icon={
this.state.showSidebarMobile
? `minus-square`
: `plus-square`
}
classes="icon-inline"
/>
</button>
{this.state.showSidebarMobile && this.sidebar()}
</div>
{res.comments.length > 0 && this.sortRadios()}
{this.state.commentViewType == CommentViewType.Tree &&
this.commentsTree()}
{this.state.commentViewType == CommentViewType.Chat &&
this.commentsFlat()}
</div>
<div class="d-none d-md-block col-md-4">{this.sidebar()}</div>
</div> </div>
{this.state.postRes.comments.length > 0 && this.sortRadios()} ),
{this.state.commentViewType == CommentViewType.Tree && none: <></>,
this.commentsTree()} })
{this.state.commentViewType == CommentViewType.Chat &&
this.commentsFlat()}
</div>
<div class="d-none d-md-block col-md-4">{this.sidebar()}</div>
</div>
)} )}
</div> </div>
); );
@ -431,43 +465,48 @@ export class Post extends Component<any, PostState> {
commentsFlat() { commentsFlat() {
// These are already sorted by new // These are already sorted by new
return ( return this.state.postRes.match({
<div> some: res => (
<CommentNodes <div>
nodes={commentsToFlatNodes(this.state.postRes.comments)} <CommentNodes
maxCommentsShown={this.state.maxCommentsShown} nodes={commentsToFlatNodes(res.comments)}
noIndent maxCommentsShown={Some(this.state.maxCommentsShown)}
locked={this.state.postRes.post_view.post.locked} noIndent
moderators={this.state.postRes.moderators} locked={res.post_view.post.locked}
admins={this.state.siteRes.admins} moderators={Some(res.moderators)}
postCreatorId={this.state.postRes.post_view.creator.id} admins={Some(this.state.siteRes.admins)}
showContext enableDownvotes={enableDownvotes(this.state.siteRes)}
enableDownvotes={this.state.siteRes.site_view.site.enable_downvotes} showContext
/> />
</div> </div>
); ),
none: <></>,
});
} }
sidebar() { sidebar() {
return ( return this.state.postRes.match({
<div class="mb-3"> some: res => (
<Sidebar <div class="mb-3">
community_view={this.state.postRes.community_view} <Sidebar
moderators={this.state.postRes.moderators} community_view={res.community_view}
admins={this.state.siteRes.admins} moderators={res.moderators}
online={this.state.postRes.online} admins={this.state.siteRes.admins}
enableNsfw={this.state.siteRes.site_view.site.enable_nsfw} online={res.online}
showIcon enableNsfw={enableNsfw(this.state.siteRes)}
/> showIcon
</div> />
); </div>
),
none: <></>,
});
} }
handleCommentSortChange(i: Post, event: any) { handleCommentSortChange(i: Post, event: any) {
i.state.commentSort = Number(event.target.value); i.state.commentSort = Number(event.target.value);
i.state.commentViewType = CommentViewType.Tree; i.state.commentViewType = CommentViewType.Tree;
i.state.commentTree = buildCommentsTree( i.state.commentTree = buildCommentsTree(
i.state.postRes.comments, i.state.postRes.map(r => r.comments).unwrapOr([]),
i.state.commentSort i.state.commentSort
); );
i.setState(i.state); i.setState(i.state);
@ -477,7 +516,7 @@ export class Post extends Component<any, PostState> {
i.state.commentViewType = Number(event.target.value); i.state.commentViewType = Number(event.target.value);
i.state.commentSort = CommentSortType.New; i.state.commentSort = CommentSortType.New;
i.state.commentTree = buildCommentsTree( i.state.commentTree = buildCommentsTree(
i.state.postRes.comments, i.state.postRes.map(r => r.comments).unwrapOr([]),
i.state.commentSort i.state.commentSort
); );
i.setState(i.state); i.setState(i.state);
@ -489,19 +528,21 @@ export class Post extends Component<any, PostState> {
} }
commentsTree() { commentsTree() {
return ( return this.state.postRes.match({
<div> some: res => (
<CommentNodes <div>
nodes={this.state.commentTree} <CommentNodes
maxCommentsShown={this.state.maxCommentsShown} nodes={this.state.commentTree}
locked={this.state.postRes.post_view.post.locked} maxCommentsShown={Some(this.state.maxCommentsShown)}
moderators={this.state.postRes.moderators} locked={res.post_view.post.locked}
admins={this.state.siteRes.admins} moderators={Some(res.moderators)}
postCreatorId={this.state.postRes.post_view.creator.id} admins={Some(this.state.siteRes.admins)}
enableDownvotes={this.state.siteRes.site_view.site.enable_downvotes} enableDownvotes={enableDownvotes(this.state.siteRes)}
/> />
</div> </div>
); ),
none: <></>,
});
} }
parseMessage(msg: any) { parseMessage(msg: any) {
@ -516,18 +557,29 @@ export class Post extends Component<any, PostState> {
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getPost({ wsClient.getPost({
id: postId, id: postId,
auth: authField(false), auth: auth(false).ok(),
}) })
); );
} else if (op == UserOperation.GetPost) { } else if (op == UserOperation.GetPost) {
let data = wsJsonToRes<GetPostResponse>(msg).data; let data = wsJsonToRes<GetPostResponse>(msg, GetPostResponse);
this.state.postRes = data; this.state.postRes = Some(data);
this.state.commentTree = buildCommentsTree( this.state.commentTree = buildCommentsTree(
this.state.postRes.comments, this.state.postRes.map(r => r.comments).unwrapOr([]),
this.state.commentSort this.state.commentSort
); );
this.state.loading = false; this.state.loading = false;
// join the rooms
WebSocketService.Instance.send(
wsClient.postJoin({ post_id: this.state.postId })
);
WebSocketService.Instance.send(
wsClient.communityJoin({
community_id: data.community_view.community.id,
})
);
// Get cross-posts // Get cross-posts
this.fetchCrossPosts(); this.fetchCrossPosts();
this.setState(this.state); this.setState(this.state);
@ -542,18 +594,25 @@ export class Post extends Component<any, PostState> {
this.scrollCommentIntoView(); this.scrollCommentIntoView();
} }
} else if (op == UserOperation.CreateComment) { } else if (op == UserOperation.CreateComment) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
// Don't get comments from the post room, if the creator is blocked // Don't get comments from the post room, if the creator is blocked
let creatorBlocked = UserService.Instance.myUserInfo?.person_blocks let creatorBlocked = UserService.Instance.myUserInfo
.map(m => m.person_blocks)
.unwrapOr([])
.map(pb => pb.target.id) .map(pb => pb.target.id)
.includes(data.comment_view.creator.id); .includes(data.comment_view.creator.id);
// Necessary since it might be a user reply, which has the recipients, to avoid double // Necessary since it might be a user reply, which has the recipients, to avoid double
if (data.recipient_ids.length == 0 && !creatorBlocked) { if (data.recipient_ids.length == 0 && !creatorBlocked) {
this.state.postRes.comments.unshift(data.comment_view); this.state.postRes.match({
insertCommentIntoTree(this.state.commentTree, data.comment_view); some: res => {
this.state.postRes.post_view.counts.comments++; res.comments.unshift(data.comment_view);
insertCommentIntoTree(this.state.commentTree, data.comment_view);
res.post_view.counts.comments++;
},
none: void 0,
});
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} }
@ -562,21 +621,33 @@ export class Post extends Component<any, PostState> {
op == UserOperation.DeleteComment || op == UserOperation.DeleteComment ||
op == UserOperation.RemoveComment op == UserOperation.RemoveComment
) { ) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
editCommentRes(data.comment_view, this.state.postRes.comments); editCommentRes(
data.comment_view,
this.state.postRes.map(r => r.comments).unwrapOr([])
);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.SaveComment) { } else if (op == UserOperation.SaveComment) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
saveCommentRes(data.comment_view, this.state.postRes.comments); saveCommentRes(
data.comment_view,
this.state.postRes.map(r => r.comments).unwrapOr([])
);
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
createCommentLikeRes(data.comment_view, this.state.postRes.comments); createCommentLikeRes(
data.comment_view,
this.state.postRes.map(r => r.comments).unwrapOr([])
);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) { } else if (op == UserOperation.CreatePostLike) {
let data = wsJsonToRes<PostResponse>(msg).data; let data = wsJsonToRes<PostResponse>(msg, PostResponse);
createPostLikeRes(data.post_view, this.state.postRes.post_view); this.state.postRes.match({
some: res => createPostLikeRes(data.post_view, res.post_view),
none: void 0,
});
this.setState(this.state); this.setState(this.state);
} else if ( } else if (
op == UserOperation.EditPost || op == UserOperation.EditPost ||
@ -586,8 +657,11 @@ export class Post extends Component<any, PostState> {
op == UserOperation.StickyPost || op == UserOperation.StickyPost ||
op == UserOperation.SavePost op == UserOperation.SavePost
) { ) {
let data = wsJsonToRes<PostResponse>(msg).data; let data = wsJsonToRes<PostResponse>(msg, PostResponse);
this.state.postRes.post_view = data.post_view; this.state.postRes.match({
some: res => (res.post_view = data.post_view),
none: void 0,
});
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if ( } else if (
@ -596,68 +670,94 @@ export class Post extends Component<any, PostState> {
op == UserOperation.RemoveCommunity || op == UserOperation.RemoveCommunity ||
op == UserOperation.FollowCommunity op == UserOperation.FollowCommunity
) { ) {
let data = wsJsonToRes<CommunityResponse>(msg).data; let data = wsJsonToRes<CommunityResponse>(msg, CommunityResponse);
this.state.postRes.community_view = data.community_view; this.state.postRes.match({
this.state.postRes.post_view.community = data.community_view.community; some: res => {
this.setState(this.state); res.community_view = data.community_view;
this.setState(this.state); res.post_view.community = data.community_view.community;
this.setState(this.state);
},
none: void 0,
});
} else if (op == UserOperation.BanFromCommunity) { } else if (op == UserOperation.BanFromCommunity) {
let data = wsJsonToRes<BanFromCommunityResponse>(msg).data; let data = wsJsonToRes<BanFromCommunityResponse>(
this.state.postRes.comments msg,
.filter(c => c.creator.id == data.person_view.person.id) BanFromCommunityResponse
.forEach(c => (c.creator_banned_from_community = data.banned)); );
if ( this.state.postRes.match({
this.state.postRes.post_view.creator.id == data.person_view.person.id some: res => {
) { res.comments
this.state.postRes.post_view.creator_banned_from_community = .filter(c => c.creator.id == data.person_view.person.id)
data.banned; .forEach(c => (c.creator_banned_from_community = data.banned));
} if (res.post_view.creator.id == data.person_view.person.id) {
this.setState(this.state); res.post_view.creator_banned_from_community = data.banned;
}
this.setState(this.state);
},
none: void 0,
});
} else if (op == UserOperation.AddModToCommunity) { } else if (op == UserOperation.AddModToCommunity) {
let data = wsJsonToRes<AddModToCommunityResponse>(msg).data; let data = wsJsonToRes<AddModToCommunityResponse>(
this.state.postRes.moderators = data.moderators; msg,
this.setState(this.state); AddModToCommunityResponse
);
this.state.postRes.match({
some: res => {
res.moderators = data.moderators;
this.setState(this.state);
},
none: void 0,
});
} else if (op == UserOperation.BanPerson) { } else if (op == UserOperation.BanPerson) {
let data = wsJsonToRes<BanPersonResponse>(msg).data; let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
this.state.postRes.comments this.state.postRes.match({
.filter(c => c.creator.id == data.person_view.person.id) some: res => {
.forEach(c => (c.creator.banned = data.banned)); res.comments
if ( .filter(c => c.creator.id == data.person_view.person.id)
this.state.postRes.post_view.creator.id == data.person_view.person.id .forEach(c => (c.creator.banned = data.banned));
) { if (res.post_view.creator.id == data.person_view.person.id) {
this.state.postRes.post_view.creator.banned = data.banned; res.post_view.creator.banned = data.banned;
} }
this.setState(this.state); this.setState(this.state);
},
none: void 0,
});
} else if (op == UserOperation.AddAdmin) { } else if (op == UserOperation.AddAdmin) {
let data = wsJsonToRes<AddAdminResponse>(msg).data; let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
this.state.siteRes.admins = data.admins; this.state.siteRes.admins = data.admins;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.Search) { } else if (op == UserOperation.Search) {
let data = wsJsonToRes<SearchResponse>(msg).data; let data = wsJsonToRes<SearchResponse>(msg, SearchResponse);
this.state.crossPosts = 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(this.state); this.setState(this.state);
} else if (op == UserOperation.LeaveAdmin) { } else if (op == UserOperation.LeaveAdmin) {
let data = wsJsonToRes<GetSiteResponse>(msg).data; let data = wsJsonToRes<GetSiteResponse>(msg, GetSiteResponse);
this.state.siteRes = data; this.state.siteRes = data;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.TransferCommunity) { } else if (op == UserOperation.TransferCommunity) {
let data = wsJsonToRes<GetCommunityResponse>(msg).data; let data = wsJsonToRes<GetCommunityResponse>(msg, GetCommunityResponse);
this.state.postRes.community_view = data.community_view; this.state.postRes.match({
this.state.postRes.post_view.community = data.community_view.community; some: res => {
this.state.postRes.moderators = data.moderators; res.community_view = data.community_view;
this.setState(this.state); res.post_view.community = data.community_view.community;
res.moderators = data.moderators;
this.setState(this.state);
},
none: void 0,
});
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data; let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
updatePersonBlock(data); updatePersonBlock(data);
} else if (op == UserOperation.CreatePostReport) { } else if (op == UserOperation.CreatePostReport) {
let data = wsJsonToRes<PostReportResponse>(msg).data; let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }
} else if (op == UserOperation.CreateCommentReport) { } else if (op == UserOperation.CreateCommentReport) {
let data = wsJsonToRes<CommentReportResponse>(msg).data; let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
if (data) { if (data) {
toast(i18n.t("report_created")); toast(i18n.t("report_created"));
} }

View file

@ -1,34 +1,34 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component } from "inferno"; import { Component } from "inferno";
import { import {
GetPersonDetails, GetPersonDetails,
GetPersonDetailsResponse, GetPersonDetailsResponse,
PersonViewSafe, GetSiteResponse,
SiteView,
SortType, SortType,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { UserService, WebSocketService } from "../../services"; import { UserService, WebSocketService } from "../../services";
import { import {
authField, auth,
getRecipientIdFromProps, getRecipientIdFromProps,
isBrowser, isBrowser,
setIsoData, setIsoData,
toast, toast,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
import { PrivateMessageForm } from "./private-message-form"; import { PrivateMessageForm } from "./private-message-form";
interface CreatePrivateMessageState { interface CreatePrivateMessageState {
site_view: SiteView; siteRes: GetSiteResponse;
recipient: PersonViewSafe; recipientDetailsRes: Option<GetPersonDetailsResponse>;
recipient_id: number; recipient_id: number;
loading: boolean; loading: boolean;
} }
@ -37,11 +37,11 @@ export class CreatePrivateMessage extends Component<
any, any,
CreatePrivateMessageState CreatePrivateMessageState
> { > {
private isoData = setIsoData(this.context); private isoData = setIsoData(this.context, GetPersonDetailsResponse);
private subscription: Subscription; private subscription: Subscription;
private emptyState: CreatePrivateMessageState = { private emptyState: CreatePrivateMessageState = {
site_view: this.isoData.site_res.site_view, siteRes: this.isoData.site_res,
recipient: undefined, recipientDetailsRes: None,
recipient_id: getRecipientIdFromProps(this.props), recipient_id: getRecipientIdFromProps(this.props),
loading: true, loading: true,
}; };
@ -54,14 +54,16 @@ export class CreatePrivateMessage extends Component<
this.parseMessage = this.parseMessage.bind(this); this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
if (!UserService.Instance.myUserInfo) { 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`);
} }
// 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.recipient = this.isoData.routeData[0].user; this.state.recipientDetailsRes = Some(
this.isoData.routeData[0] as GetPersonDetailsResponse
);
this.state.loading = false; this.state.loading = false;
} else { } else {
this.fetchPersonDetails(); this.fetchPersonDetails();
@ -69,30 +71,40 @@ export class CreatePrivateMessage extends Component<
} }
fetchPersonDetails() { fetchPersonDetails() {
let form: GetPersonDetails = { let form = new GetPersonDetails({
person_id: this.state.recipient_id, person_id: Some(this.state.recipient_id),
sort: SortType.New, sort: Some(SortType.New),
saved_only: false, saved_only: Some(false),
auth: authField(false), username: None,
}; page: None,
limit: None,
community_id: None,
auth: auth(false).ok(),
});
WebSocketService.Instance.send(wsClient.getPersonDetails(form)); WebSocketService.Instance.send(wsClient.getPersonDetails(form));
} }
static fetchInitialData(req: InitialFetchRequest): Promise<any>[] { static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
let person_id = Number(req.path.split("/").pop()); let person_id = Some(Number(req.path.split("/").pop()));
let form: GetPersonDetails = { let form = new GetPersonDetails({
person_id, person_id,
sort: SortType.New, sort: Some(SortType.New),
saved_only: false, saved_only: Some(false),
username: None,
page: None,
limit: None,
community_id: None,
auth: req.auth, auth: req.auth,
}; });
return [req.client.getPersonDetails(form)]; return [req.client.getPersonDetails(form)];
} }
get documentTitle(): string { get documentTitle(): string {
return `${i18n.t("create_private_message")} - ${ return this.state.recipientDetailsRes.match({
this.state.site_view.site.name some: res =>
}`; `${i18n.t("create_private_message")} - ${res.person_view.person.name}`,
none: "",
});
} }
componentWillUnmount() { componentWillUnmount() {
@ -107,21 +119,29 @@ export class CreatePrivateMessage extends Component<
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
{this.state.loading ? ( {this.state.loading ? (
<h5> <h5>
<Spinner large /> <Spinner large />
</h5> </h5>
) : ( ) : (
<div class="row"> this.state.recipientDetailsRes.match({
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> some: res => (
<h5>{i18n.t("create_private_message")}</h5> <div class="row">
<PrivateMessageForm <div class="col-12 col-lg-6 offset-lg-3 mb-4">
onCreate={this.handlePrivateMessageCreate} <h5>{i18n.t("create_private_message")}</h5>
recipient={this.state.recipient.person} <PrivateMessageForm
/> privateMessageView={None}
</div> onCreate={this.handlePrivateMessageCreate}
</div> recipient={res.person_view.person}
/>
</div>
</div>
),
none: <></>,
})
)} )}
</div> </div>
); );
@ -143,8 +163,11 @@ export class CreatePrivateMessage extends Component<
this.setState(this.state); this.setState(this.state);
return; return;
} else if (op == UserOperation.GetPersonDetails) { } else if (op == UserOperation.GetPersonDetails) {
let data = wsJsonToRes<GetPersonDetailsResponse>(msg).data; let data = wsJsonToRes<GetPersonDetailsResponse>(
this.state.recipient = data.person_view; msg,
GetPersonDetailsResponse
);
this.state.recipientDetailsRes = Some(data);
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
} }

View file

@ -1,3 +1,4 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess"; import { T } from "inferno-i18next-dess";
import { Prompt } from "inferno-router"; import { Prompt } from "inferno-router";
@ -8,21 +9,21 @@ import {
PrivateMessageResponse, PrivateMessageResponse,
PrivateMessageView, PrivateMessageView,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { WebSocketService } from "../../services"; import { WebSocketService } from "../../services";
import { import {
authField, auth,
capitalizeFirstLetter, capitalizeFirstLetter,
isBrowser, isBrowser,
relTags, relTags,
setupTippy, setupTippy,
toast, toast,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../../utils"; } from "../../utils";
import { Icon, Spinner } from "../common/icon"; import { Icon, Spinner } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea"; import { MarkdownTextArea } from "../common/markdown-textarea";
@ -30,7 +31,7 @@ import { PersonListing } from "../person/person-listing";
interface PrivateMessageFormProps { interface PrivateMessageFormProps {
recipient: PersonSafe; recipient: PersonSafe;
privateMessage?: PrivateMessageView; // If a pm is given, that means this is an edit privateMessageView: Option<PrivateMessageView>; // If a pm is given, that means this is an edit
onCancel?(): any; onCancel?(): any;
onCreate?(message: PrivateMessageView): any; onCreate?(message: PrivateMessageView): any;
onEdit?(message: PrivateMessageView): any; onEdit?(message: PrivateMessageView): any;
@ -49,11 +50,11 @@ export class PrivateMessageForm extends Component<
> { > {
private subscription: Subscription; private subscription: Subscription;
private emptyState: PrivateMessageFormState = { private emptyState: PrivateMessageFormState = {
privateMessageForm: { privateMessageForm: new CreatePrivateMessage({
content: null, content: null,
recipient_id: this.props.recipient.id, recipient_id: this.props.recipient.id,
auth: authField(), auth: auth().unwrap(),
}, }),
loading: false, loading: false,
previewMode: false, previewMode: false,
showDisclaimer: false, showDisclaimer: false,
@ -70,10 +71,11 @@ export class PrivateMessageForm extends Component<
this.subscription = wsSubscribe(this.parseMessage); this.subscription = wsSubscribe(this.parseMessage);
// Its an edit // Its an edit
if (this.props.privateMessage) { this.props.privateMessageView.match({
this.state.privateMessageForm.content = some: pm =>
this.props.privateMessage.private_message.content; (this.state.privateMessageForm.content = pm.private_message.content),
} none: void 0,
});
} }
componentDidMount() { componentDidMount() {
@ -103,7 +105,7 @@ export class PrivateMessageForm extends Component<
message={i18n.t("block_leaving")} message={i18n.t("block_leaving")}
/> />
<form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}> <form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
{!this.props.privateMessage && ( {this.props.privateMessageView.isNone() && (
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label"> <label class="col-sm-2 col-form-label">
{capitalizeFirstLetter(i18n.t("to"))} {capitalizeFirstLetter(i18n.t("to"))}
@ -128,7 +130,10 @@ export class PrivateMessageForm extends Component<
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<MarkdownTextArea <MarkdownTextArea
initialContent={this.state.privateMessageForm.content} initialContent={Some(this.state.privateMessageForm.content)}
placeholder={None}
buttonTitle={None}
maxLength={None}
onContentChange={this.handleContentChange} onContentChange={this.handleContentChange}
/> />
</div> </div>
@ -161,13 +166,13 @@ export class PrivateMessageForm extends Component<
> >
{this.state.loading ? ( {this.state.loading ? (
<Spinner /> <Spinner />
) : this.props.privateMessage ? ( ) : this.props.privateMessageView.isSome() ? (
capitalizeFirstLetter(i18n.t("save")) capitalizeFirstLetter(i18n.t("save"))
) : ( ) : (
capitalizeFirstLetter(i18n.t("send_message")) capitalizeFirstLetter(i18n.t("send_message"))
)} )}
</button> </button>
{this.props.privateMessage && ( {this.props.privateMessageView.isSome() && (
<button <button
type="button" type="button"
class="btn btn-secondary" class="btn btn-secondary"
@ -188,18 +193,19 @@ export class PrivateMessageForm extends Component<
handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) { handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
event.preventDefault(); event.preventDefault();
if (i.props.privateMessage) { i.props.privateMessageView.match({
let form: EditPrivateMessage = { some: pm => {
private_message_id: i.props.privateMessage.private_message.id, let form = new EditPrivateMessage({
content: i.state.privateMessageForm.content, private_message_id: pm.private_message.id,
auth: authField(), content: i.state.privateMessageForm.content,
}; auth: auth().unwrap(),
WebSocketService.Instance.send(wsClient.editPrivateMessage(form)); });
} else { WebSocketService.Instance.send(wsClient.editPrivateMessage(form));
WebSocketService.Instance.send( },
none: WebSocketService.Instance.send(
wsClient.createPrivateMessage(i.state.privateMessageForm) wsClient.createPrivateMessage(i.state.privateMessageForm)
); ),
} });
i.state.loading = true; i.state.loading = true;
i.setState(i.state); i.setState(i.state);
} }
@ -237,11 +243,17 @@ export class PrivateMessageForm extends Component<
op == UserOperation.DeletePrivateMessage || op == UserOperation.DeletePrivateMessage ||
op == UserOperation.MarkPrivateMessageAsRead op == UserOperation.MarkPrivateMessageAsRead
) { ) {
let data = wsJsonToRes<PrivateMessageResponse>(msg).data; let data = wsJsonToRes<PrivateMessageResponse>(
msg,
PrivateMessageResponse
);
this.state.loading = false; this.state.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>(msg).data; let data = wsJsonToRes<PrivateMessageResponse>(
msg,
PrivateMessageResponse
);
this.state.loading = false; this.state.loading = false;
this.props.onCreate(data.private_message_view); this.props.onCreate(data.private_message_view);
this.setState(this.state); this.setState(this.state);

View file

@ -1,3 +1,4 @@
import { None, Some } from "@sniptt/monads/build";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
DeletePrivateMessage, DeletePrivateMessage,
@ -7,7 +8,7 @@ import {
} 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";
import { authField, mdToHtml, toast, wsClient } from "../../utils"; import { auth, mdToHtml, toast, wsClient } from "../../utils";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
import { MomentTime } from "../common/moment-time"; import { MomentTime } from "../common/moment-time";
import { PersonListing } from "../person/person-listing"; import { PersonListing } from "../person/person-listing";
@ -46,16 +47,17 @@ export class PrivateMessage extends Component<
} }
get mine(): boolean { get mine(): boolean {
return ( return UserService.Instance.myUserInfo
UserService.Instance.myUserInfo && .map(
UserService.Instance.myUserInfo.local_user_view.person.id == m =>
this.props.private_message_view.creator.id m.local_user_view.person.id ==
); this.props.private_message_view.creator.id
)
.unwrapOr(false);
} }
render() { render() {
let message_view = this.props.private_message_view; let message_view = this.props.private_message_view;
// TODO check this again
let otherPerson: PersonSafe = this.mine let otherPerson: PersonSafe = this.mine
? message_view.recipient ? message_view.recipient
: message_view.creator; : message_view.creator;
@ -73,7 +75,10 @@ export class PrivateMessage extends Component<
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<span> <span>
<MomentTime data={message_view.private_message} /> <MomentTime
published={message_view.private_message.published}
updated={message_view.private_message.updated}
/>
</span> </span>
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
@ -93,7 +98,7 @@ export class PrivateMessage extends Component<
{this.state.showEdit && ( {this.state.showEdit && (
<PrivateMessageForm <PrivateMessageForm
recipient={otherPerson} recipient={otherPerson}
privateMessage={message_view} privateMessageView={Some(message_view)}
onEdit={this.handlePrivateMessageEdit} onEdit={this.handlePrivateMessageEdit}
onCreate={this.handlePrivateMessageCreate} onCreate={this.handlePrivateMessageCreate}
onCancel={this.handleReplyCancel} onCancel={this.handleReplyCancel}
@ -207,6 +212,7 @@ export class PrivateMessage extends Component<
{this.state.showReply && ( {this.state.showReply && (
<PrivateMessageForm <PrivateMessageForm
recipient={otherPerson} recipient={otherPerson}
privateMessageView={None}
onCreate={this.handlePrivateMessageCreate} onCreate={this.handlePrivateMessageCreate}
/> />
)} )}
@ -232,11 +238,11 @@ export class PrivateMessage extends Component<
} }
handleDeleteClick(i: PrivateMessage) { handleDeleteClick(i: PrivateMessage) {
let form: DeletePrivateMessage = { let form = new DeletePrivateMessage({
private_message_id: i.props.private_message_view.private_message.id, private_message_id: i.props.private_message_view.private_message.id,
deleted: !i.props.private_message_view.private_message.deleted, deleted: !i.props.private_message_view.private_message.deleted,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.deletePrivateMessage(form)); WebSocketService.Instance.send(wsClient.deletePrivateMessage(form));
} }
@ -247,11 +253,11 @@ export class PrivateMessage extends Component<
} }
handleMarkRead(i: PrivateMessage) { handleMarkRead(i: PrivateMessage) {
let form: MarkPrivateMessageAsRead = { let form = new MarkPrivateMessageAsRead({
private_message_id: i.props.private_message_view.private_message.id, private_message_id: i.props.private_message_view.private_message.id,
read: !i.props.private_message_view.private_message.read, read: !i.props.private_message_view.private_message.read,
auth: authField(), auth: auth().unwrap(),
}; });
WebSocketService.Instance.send(wsClient.markPrivateMessageAsRead(form)); WebSocketService.Instance.send(wsClient.markPrivateMessageAsRead(form));
} }
@ -271,14 +277,15 @@ export class PrivateMessage extends Component<
} }
handlePrivateMessageCreate(message: PrivateMessageView) { handlePrivateMessageCreate(message: PrivateMessageView) {
if ( UserService.Instance.myUserInfo.match({
UserService.Instance.myUserInfo && some: mui => {
message.creator.id == if (message.creator.id == mui.local_user_view.person.id) {
UserService.Instance.myUserInfo.local_user_view.person.id this.state.showReply = false;
) { this.setState(this.state);
this.state.showReply = false; toast(i18n.t("message_sent"));
this.setState(this.state); }
toast(i18n.t("message_sent")); },
} none: void 0,
});
} }
} }

View file

@ -1,10 +1,14 @@
import { None, Option, Some } from "@sniptt/monads";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
CommentResponse, CommentResponse,
CommentView, CommentView,
CommunityView, CommunityView,
GetCommunity, GetCommunity,
GetCommunityResponse,
GetPersonDetails, GetPersonDetails,
GetPersonDetailsResponse,
GetSiteResponse,
ListCommunities, ListCommunities,
ListCommunitiesResponse, ListCommunitiesResponse,
ListingType, ListingType,
@ -16,16 +20,17 @@ import {
Search as SearchForm, Search as SearchForm,
SearchResponse, SearchResponse,
SearchType, SearchType,
Site,
SortType, SortType,
UserOperation, UserOperation,
wsJsonToRes,
wsUserOp,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { InitialFetchRequest } from "shared/interfaces"; import { InitialFetchRequest } from "shared/interfaces";
import { i18n } from "../i18next"; import { i18n } from "../i18next";
import { WebSocketService } from "../services"; import { WebSocketService } from "../services";
import { import {
authField, auth,
capitalizeFirstLetter, capitalizeFirstLetter,
choicesConfig, choicesConfig,
commentsToFlatNodes, commentsToFlatNodes,
@ -34,6 +39,8 @@ import {
createCommentLikeRes, createCommentLikeRes,
createPostLikeFindRes, createPostLikeFindRes,
debounce, debounce,
enableDownvotes,
enableNsfw,
fetchCommunities, fetchCommunities,
fetchLimit, fetchLimit,
fetchUsers, fetchUsers,
@ -48,13 +55,10 @@ import {
routeSortTypeToEnum, routeSortTypeToEnum,
saveScrollPosition, saveScrollPosition,
setIsoData, setIsoData,
setOptionalAuth,
showLocal, showLocal,
toast, toast,
wsClient, wsClient,
wsJsonToRes,
wsSubscribe, wsSubscribe,
wsUserOp,
} from "../utils"; } from "../utils";
import { CommentNodes } from "./comment/comment-nodes"; import { CommentNodes } from "./comment/comment-nodes";
import { HtmlTags } from "./common/html-tags"; import { HtmlTags } from "./common/html-tags";
@ -89,13 +93,13 @@ interface SearchState {
communityId: number; communityId: number;
creatorId: number; creatorId: number;
page: number; page: number;
searchResponse?: SearchResponse; searchResponse: Option<SearchResponse>;
communities: CommunityView[]; communities: CommunityView[];
creator?: PersonViewSafe; creatorDetails: Option<GetPersonDetailsResponse>;
loading: boolean; loading: boolean;
site: Site; siteRes: GetSiteResponse;
searchText: string; searchText: string;
resolveObjectResponse?: ResolveObjectResponse; resolveObjectResponse: Option<ResolveObjectResponse>;
} }
interface UrlParams { interface UrlParams {
@ -115,7 +119,14 @@ interface Combined {
} }
export class Search extends Component<any, SearchState> { export class Search extends Component<any, SearchState> {
private isoData = setIsoData(this.context); private isoData = setIsoData(
this.context,
GetCommunityResponse,
ListCommunitiesResponse,
GetPersonDetailsResponse,
SearchResponse,
ResolveObjectResponse
);
private communityChoices: any; private communityChoices: any;
private creatorChoices: any; private creatorChoices: any;
private subscription: Subscription; private subscription: Subscription;
@ -132,10 +143,11 @@ export class Search extends Component<any, SearchState> {
this.props.match.params.community_id this.props.match.params.community_id
), ),
creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id), creatorId: Search.getCreatorIdFromProps(this.props.match.params.creator_id),
searchResponse: null, searchResponse: None,
resolveObjectResponse: null, resolveObjectResponse: None,
creatorDetails: None,
loading: true, loading: true,
site: this.isoData.site_res.site_view.site, siteRes: this.isoData.site_res,
communities: [], communities: [],
}; };
@ -180,20 +192,29 @@ export class Search extends Component<any, SearchState> {
// 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 singleOrMultipleCommunities = this.isoData.routeData[0]; let communityRes = Some(
if (singleOrMultipleCommunities.communities) { this.isoData.routeData[0] as GetCommunityResponse
this.state.communities = this.isoData.routeData[0].communities; );
} else { let communitiesRes = Some(
this.state.communities = [this.isoData.routeData[0].community_view]; this.isoData.routeData[1] as ListCommunitiesResponse
} );
// This can be single or multiple communities given
this.state.communities = communitiesRes
.map(c => c.communities)
.unwrapOr([communityRes.map(c => c.community_view).unwrap()]);
this.state.creatorDetails = Some(
this.isoData.routeData[2] as GetPersonDetailsResponse
);
let creator = this.isoData.routeData[1];
if (creator?.person_view) {
this.state.creator = this.isoData.routeData[1].person_view;
}
if (this.state.q != "") { if (this.state.q != "") {
this.state.searchResponse = this.isoData.routeData[2]; this.state.searchResponse = Some(
this.state.resolveObjectResponse = this.isoData.routeData[3]; this.isoData.routeData[3] as SearchResponse
);
this.state.resolveObjectResponse = Some(
this.isoData.routeData[4] as ResolveObjectResponse
);
this.state.loading = false; this.state.loading = false;
} else { } else {
this.search(); this.search();
@ -231,12 +252,13 @@ export class Search extends Component<any, SearchState> {
} }
fetchCommunities() { fetchCommunities() {
let listCommunitiesForm: ListCommunities = { let listCommunitiesForm = new ListCommunities({
type_: ListingType.All, type_: Some(ListingType.All),
sort: SortType.TopAll, sort: Some(SortType.TopAll),
limit: fetchLimit, limit: Some(fetchLimit),
auth: authField(false), page: None,
}; auth: auth(false).ok(),
});
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.listCommunities(listCommunitiesForm) wsClient.listCommunities(listCommunitiesForm)
); );
@ -247,59 +269,76 @@ export class Search extends Component<any, SearchState> {
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
let communityId = this.getCommunityIdFromProps(pathSplit[11]); let communityId = this.getCommunityIdFromProps(pathSplit[11]);
if (communityId !== 0) { let community_id: Option<number> =
let getCommunityForm: GetCommunity = { communityId == 0 ? None : Some(communityId);
id: communityId, community_id.match({
}; some: id => {
setOptionalAuth(getCommunityForm, req.auth); let getCommunityForm = new GetCommunity({
promises.push(req.client.getCommunity(getCommunityForm)); id: Some(id),
} else { name: None,
let listCommunitiesForm: ListCommunities = { auth: req.auth,
type_: ListingType.All, });
sort: SortType.TopAll, promises.push(req.client.getCommunity(getCommunityForm));
limit: fetchLimit, promises.push(Promise.resolve());
}; },
setOptionalAuth(listCommunitiesForm, req.auth); none: () => {
promises.push(req.client.listCommunities(listCommunitiesForm)); let listCommunitiesForm = new ListCommunities({
} type_: Some(ListingType.All),
sort: Some(SortType.TopAll),
limit: Some(fetchLimit),
page: None,
auth: req.auth,
});
promises.push(Promise.resolve());
promises.push(req.client.listCommunities(listCommunitiesForm));
},
});
let creatorId = this.getCreatorIdFromProps(pathSplit[13]); let creatorId = this.getCreatorIdFromProps(pathSplit[13]);
if (creatorId !== 0) { let creator_id: Option<number> = creatorId == 0 ? None : Some(creatorId);
let getCreatorForm: GetPersonDetails = { creator_id.match({
person_id: creatorId, some: id => {
}; let getCreatorForm = new GetPersonDetails({
setOptionalAuth(getCreatorForm, req.auth); person_id: Some(id),
promises.push(req.client.getPersonDetails(getCreatorForm)); username: None,
} else { sort: None,
promises.push(Promise.resolve()); page: None,
} limit: None,
community_id: None,
saved_only: None,
auth: req.auth,
});
promises.push(req.client.getPersonDetails(getCreatorForm));
},
none: () => {
promises.push(Promise.resolve());
},
});
let form: SearchForm = { let form = new SearchForm({
q: this.getSearchQueryFromProps(pathSplit[3]), q: this.getSearchQueryFromProps(pathSplit[3]),
type_: this.getSearchTypeFromProps(pathSplit[5]), community_id,
sort: this.getSortTypeFromProps(pathSplit[7]), community_name: None,
listing_type: this.getListingTypeFromProps(pathSplit[9]), creator_id,
page: this.getPageFromProps(pathSplit[15]), type_: Some(this.getSearchTypeFromProps(pathSplit[5])),
limit: fetchLimit, sort: Some(this.getSortTypeFromProps(pathSplit[7])),
}; listing_type: Some(this.getListingTypeFromProps(pathSplit[9])),
if (communityId !== 0) { page: Some(this.getPageFromProps(pathSplit[15])),
form.community_id = communityId; limit: Some(fetchLimit),
} auth: req.auth,
if (creatorId !== 0) { });
form.creator_id = creatorId;
}
setOptionalAuth(form, req.auth);
let resolveObjectForm: ResolveObject = { let resolveObjectForm = new ResolveObject({
q: this.getSearchQueryFromProps(pathSplit[3]), q: this.getSearchQueryFromProps(pathSplit[3]),
}; auth: req.auth,
setOptionalAuth(resolveObjectForm, req.auth); });
if (form.q != "") { if (form.q != "") {
//this.state.loading = false;
//this.setState(this.state);
promises.push(req.client.search(form)); promises.push(req.client.search(form));
promises.push(req.client.resolveObject(resolveObjectForm)); promises.push(req.client.resolveObject(resolveObjectForm));
} else {
promises.push(Promise.resolve());
promises.push(Promise.resolve());
} }
return promises; return promises;
@ -318,19 +357,21 @@ export class Search extends Component<any, SearchState> {
this.setState({ this.setState({
loading: true, loading: true,
searchText: this.state.q, searchText: this.state.q,
searchResponse: null, searchResponse: None,
resolveObjectResponse: null, resolveObjectResponse: None,
}); });
this.search(); this.search();
} }
} }
get documentTitle(): string { get documentTitle(): string {
if (this.state.q) { return this.state.siteRes.site_view.match({
return `${i18n.t("search")} - ${this.state.q} - ${this.state.site.name}`; some: siteView =>
} else { this.state.q
return `${i18n.t("search")} - ${this.state.site.name}`; ? `${i18n.t("search")} - ${this.state.q} - ${siteView.site.name}`
} : `${i18n.t("search")} - ${siteView.site.name}`,
none: "",
});
} }
render() { render() {
@ -339,6 +380,8 @@ export class Search extends Component<any, SearchState> {
<HtmlTags <HtmlTags
title={this.documentTitle} title={this.documentTitle}
path={this.context.router.route.match.url} path={this.context.router.route.match.url}
description={None}
image={None}
/> />
<h5>{i18n.t("search")}</h5> <h5>{i18n.t("search")}</h5>
{this.selects()} {this.selects()}
@ -459,46 +502,52 @@ export class Search extends Component<any, SearchState> {
let combined: Combined[] = []; let combined: Combined[] = [];
// Push the possible resolve / federated objects first // Push the possible resolve / federated objects first
let resolveComment = this.state.resolveObjectResponse?.comment; this.state.resolveObjectResponse.match({
if (resolveComment) { some: res => {
combined.push(this.commentViewToCombined(resolveComment)); let resolveComment = res.comment;
} if (resolveComment.isSome()) {
let resolvePost = this.state.resolveObjectResponse?.post; combined.push(this.commentViewToCombined(resolveComment.unwrap()));
if (resolvePost) { }
combined.push(this.postViewToCombined(resolvePost)); let resolvePost = res.post;
} if (resolvePost.isSome()) {
let resolveCommunity = this.state.resolveObjectResponse?.community; combined.push(this.postViewToCombined(resolvePost.unwrap()));
if (resolveCommunity) { }
combined.push(this.communityViewToCombined(resolveCommunity)); let resolveCommunity = res.community;
} if (resolveCommunity.isSome()) {
let resolveUser = this.state.resolveObjectResponse?.person; combined.push(
if (resolveUser) { this.communityViewToCombined(resolveCommunity.unwrap())
combined.push(this.personViewSafeToCombined(resolveUser)); );
} }
let resolveUser = res.person;
if (resolveUser.isSome()) {
combined.push(this.personViewSafeToCombined(resolveUser.unwrap()));
}
},
none: void 0,
});
// Push the search results // Push the search results
pushNotNull( this.state.searchResponse.match({
combined, some: res => {
this.state.searchResponse?.comments?.map(e => pushNotNull(
this.commentViewToCombined(e) combined,
) res.comments?.map(e => this.commentViewToCombined(e))
); );
pushNotNull( pushNotNull(
combined, combined,
this.state.searchResponse?.posts?.map(e => this.postViewToCombined(e)) res.posts?.map(e => this.postViewToCombined(e))
); );
pushNotNull( pushNotNull(
combined, combined,
this.state.searchResponse?.communities?.map(e => res.communities?.map(e => this.communityViewToCombined(e))
this.communityViewToCombined(e) );
) pushNotNull(
); combined,
pushNotNull( res.users?.map(e => this.personViewSafeToCombined(e))
combined, );
this.state.searchResponse?.users?.map(e => },
this.personViewSafeToCombined(e) none: void 0,
) });
);
// Sort it // Sort it
if (this.state.sort == SortType.New) { if (this.state.sort == SortType.New) {
@ -528,18 +577,24 @@ export class Search extends Component<any, SearchState> {
<PostListing <PostListing
key={(i.data as PostView).post.id} key={(i.data as PostView).post.id}
post_view={i.data as PostView} post_view={i.data as PostView}
duplicates={None}
moderators={None}
admins={None}
showCommunity showCommunity
enableDownvotes={this.state.site.enable_downvotes} enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={this.state.site.enable_nsfw} enableNsfw={enableNsfw(this.state.siteRes)}
/> />
)} )}
{i.type_ == "comments" && ( {i.type_ == "comments" && (
<CommentNodes <CommentNodes
key={(i.data as CommentView).comment.id} key={(i.data as CommentView).comment.id}
nodes={[{ comment_view: i.data as CommentView }]} nodes={[{ comment_view: i.data as CommentView }]}
moderators={None}
admins={None}
maxCommentsShown={None}
locked locked
noIndent noIndent
enableDownvotes={this.state.site.enable_downvotes} enableDownvotes={enableDownvotes(this.state.siteRes)}
/> />
)} )}
{i.type_ == "communities" && ( {i.type_ == "communities" && (
@ -558,15 +613,24 @@ export class Search extends Component<any, SearchState> {
comments() { comments() {
let comments: CommentView[] = []; let comments: CommentView[] = [];
pushNotNull(comments, this.state.resolveObjectResponse?.comment); this.state.resolveObjectResponse.match({
pushNotNull(comments, this.state.searchResponse?.comments); some: res => pushNotNull(comments, res.comment),
none: void 0,
});
this.state.searchResponse.match({
some: res => pushNotNull(comments, res.comments),
none: void 0,
});
return ( return (
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(comments)} nodes={commentsToFlatNodes(comments)}
locked locked
noIndent noIndent
enableDownvotes={this.state.site.enable_downvotes} moderators={None}
admins={None}
maxCommentsShown={None}
enableDownvotes={enableDownvotes(this.state.siteRes)}
/> />
); );
} }
@ -574,8 +638,14 @@ export class Search extends Component<any, SearchState> {
posts() { posts() {
let posts: PostView[] = []; let posts: PostView[] = [];
pushNotNull(posts, this.state.resolveObjectResponse?.post); this.state.resolveObjectResponse.match({
pushNotNull(posts, this.state.searchResponse?.posts); some: res => pushNotNull(posts, res.post),
none: void 0,
});
this.state.searchResponse.match({
some: res => pushNotNull(posts, res.posts),
none: void 0,
});
return ( return (
<> <>
@ -585,8 +655,11 @@ export class Search extends Component<any, SearchState> {
<PostListing <PostListing
post_view={post} post_view={post}
showCommunity showCommunity
enableDownvotes={this.state.site.enable_downvotes} duplicates={None}
enableNsfw={this.state.site.enable_nsfw} moderators={None}
admins={None}
enableDownvotes={enableDownvotes(this.state.siteRes)}
enableNsfw={enableNsfw(this.state.siteRes)}
/> />
</div> </div>
</div> </div>
@ -598,8 +671,14 @@ export class Search extends Component<any, SearchState> {
communities() { communities() {
let communities: CommunityView[] = []; let communities: CommunityView[] = [];
pushNotNull(communities, this.state.resolveObjectResponse?.community); this.state.resolveObjectResponse.match({
pushNotNull(communities, this.state.searchResponse?.communities); some: res => pushNotNull(communities, res.community),
none: void 0,
});
this.state.searchResponse.match({
some: res => pushNotNull(communities, res.communities),
none: void 0,
});
return ( return (
<> <>
@ -615,8 +694,14 @@ export class Search extends Component<any, SearchState> {
users() { users() {
let users: PersonViewSafe[] = []; let users: PersonViewSafe[] = [];
pushNotNull(users, this.state.resolveObjectResponse?.person); this.state.resolveObjectResponse.match({
pushNotNull(users, this.state.searchResponse?.users); some: res => pushNotNull(users, res.person),
none: void 0,
});
this.state.searchResponse.match({
some: res => pushNotNull(users, res.users),
none: void 0,
});
return ( return (
<> <>
@ -692,11 +777,14 @@ export class Search extends Component<any, SearchState> {
value={this.state.creatorId} value={this.state.creatorId}
> >
<option value="0">{i18n.t("all")}</option> <option value="0">{i18n.t("all")}</option>
{this.state.creator && ( {this.state.creatorDetails.match({
<option value={this.state.creator.person.id}> some: creator => (
{personSelectName(this.state.creator)} <option value={creator.person_view.person.id}>
</option> {personSelectName(creator.person_view)}
)} </option>
),
none: <></>,
})}
</select> </select>
</div> </div>
</div> </div>
@ -704,19 +792,21 @@ export class Search extends Component<any, SearchState> {
} }
resultsCount(): number { resultsCount(): number {
let res = this.state.searchResponse; let searchCount = this.state.searchResponse
let resObj = this.state.resolveObjectResponse; .map(
let resObjCount = r =>
resObj?.post || resObj?.person || resObj?.community || resObj?.comment r.posts?.length +
? 1 r.comments?.length +
: 0; r.communities?.length +
return ( r.users?.length
res?.posts?.length + )
res?.comments?.length + .unwrapOr(0);
res?.communities?.length +
res?.users?.length + let resObjCount = this.state.resolveObjectResponse
resObjCount .map(r => (r.post || r.person || r.community || r.comment ? 1 : 0))
); .unwrapOr(0);
return resObjCount + searchCount;
} }
handlePageChange(page: number) { handlePageChange(page: number) {
@ -724,30 +814,34 @@ export class Search extends Component<any, SearchState> {
} }
search() { search() {
let form: SearchForm = { let community_id: Option<number> =
q: this.state.q, this.state.communityId == 0 ? None : Some(this.state.communityId);
type_: this.state.type_, let creator_id: Option<number> =
sort: this.state.sort, this.state.creatorId == 0 ? None : Some(this.state.creatorId);
listing_type: this.state.listingType,
page: this.state.page,
limit: fetchLimit,
auth: authField(false),
};
if (this.state.communityId !== 0) {
form.community_id = this.state.communityId;
}
if (this.state.creatorId !== 0) {
form.creator_id = this.state.creatorId;
}
let resolveObjectForm: ResolveObject = { console.log(community_id.unwrapOr(-22));
let form = new SearchForm({
q: this.state.q, q: this.state.q,
auth: authField(false), community_id,
}; community_name: None,
creator_id,
type_: Some(this.state.type_),
sort: Some(this.state.sort),
listing_type: Some(this.state.listingType),
page: Some(this.state.page),
limit: Some(fetchLimit),
auth: auth(false).ok(),
});
let resolveObjectForm = new ResolveObject({
q: this.state.q,
auth: auth(false).ok(),
});
if (this.state.q != "") { if (this.state.q != "") {
this.state.searchResponse = null; this.state.searchResponse = None;
this.state.resolveObjectResponse = null; this.state.resolveObjectResponse = None;
this.state.loading = true; this.state.loading = true;
this.setState(this.state); this.setState(this.state);
WebSocketService.Instance.send(wsClient.search(form)); WebSocketService.Instance.send(wsClient.search(form));
@ -890,50 +984,56 @@ 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 = { this.state.resolveObjectResponse = Some({
comment: null, comment: None,
post: null, post: None,
community: null, community: None,
person: null, person: None,
}; });
this.checkFinishedLoading(); this.checkFinishedLoading();
} else { } else {
toast(i18n.t(msg.error), "danger"); toast(i18n.t(msg.error), "danger");
return; return;
} }
} else if (op == UserOperation.Search) { } else if (op == UserOperation.Search) {
let data = wsJsonToRes<SearchResponse>(msg).data; let data = wsJsonToRes<SearchResponse>(msg, SearchResponse);
this.state.searchResponse = data; this.state.searchResponse = Some(data);
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.checkFinishedLoading(); this.checkFinishedLoading();
restoreScrollPosition(this.context); restoreScrollPosition(this.context);
} else if (op == UserOperation.CreateCommentLike) { } else if (op == UserOperation.CreateCommentLike) {
let data = wsJsonToRes<CommentResponse>(msg).data; let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
createCommentLikeRes( createCommentLikeRes(
data.comment_view, data.comment_view,
this.state.searchResponse?.comments this.state.searchResponse.map(r => r.comments).unwrapOr([])
); );
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) { } else if (op == UserOperation.CreatePostLike) {
let data = wsJsonToRes<PostResponse>(msg).data; let data = wsJsonToRes<PostResponse>(msg, PostResponse);
createPostLikeFindRes(data.post_view, this.state.searchResponse?.posts); createPostLikeFindRes(
data.post_view,
this.state.searchResponse.map(r => r.posts).unwrapOr([])
);
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.ListCommunities) { } else if (op == UserOperation.ListCommunities) {
let data = wsJsonToRes<ListCommunitiesResponse>(msg).data; let data = wsJsonToRes<ListCommunitiesResponse>(
msg,
ListCommunitiesResponse
);
this.state.communities = data.communities; this.state.communities = data.communities;
this.setState(this.state); this.setState(this.state);
this.setupCommunityFilter(); this.setupCommunityFilter();
} else if (op == UserOperation.ResolveObject) { } else if (op == UserOperation.ResolveObject) {
let data = wsJsonToRes<ResolveObjectResponse>(msg).data; let data = wsJsonToRes<ResolveObjectResponse>(msg, ResolveObjectResponse);
this.state.resolveObjectResponse = data; this.state.resolveObjectResponse = Some(data);
this.checkFinishedLoading(); this.checkFinishedLoading();
} }
} }
checkFinishedLoading() { checkFinishedLoading() {
if ( if (
this.state.searchResponse != null && this.state.searchResponse.isSome() &&
this.state.resolveObjectResponse != null this.state.resolveObjectResponse.isSome()
) { ) {
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);

View file

@ -1,3 +1,4 @@
import { Either, Option } from "@sniptt/monads";
import { import {
CommentView, CommentView,
GetSiteResponse, GetSiteResponse,
@ -5,6 +6,9 @@ import {
PersonMentionView, PersonMentionView,
} from "lemmy-js-client"; } from "lemmy-js-client";
/**
* This contains serialized data, it needs to be deserialized before use.
*/
export interface IsoData { export interface IsoData {
path: string; path: string;
routeData: any[]; routeData: any[];
@ -23,9 +27,9 @@ declare global {
} }
export interface InitialFetchRequest { export interface InitialFetchRequest {
auth: string; auth: Option<string>;
path: string;
client: LemmyHttp; client: LemmyHttp;
path: string;
} }
export interface CommentNode { export interface CommentNode {
@ -35,11 +39,10 @@ export interface CommentNode {
} }
export interface PostFormParams { export interface PostFormParams {
name: string; name: Option<string>;
url?: string; url: Option<string>;
body?: string; body: Option<string>;
community_name?: string; nameOrId: Option<Either<string, number>>;
community_id?: number;
} }
export enum CommentSortType { export enum CommentSortType {

View file

@ -1,9 +1,12 @@
// import Cookies from 'js-cookie'; // import Cookies from 'js-cookie';
import { Err, None, Ok, Option, Result, Some } from "@sniptt/monads";
import IsomorphicCookie from "isomorphic-cookie"; import IsomorphicCookie from "isomorphic-cookie";
import jwt_decode from "jwt-decode"; import jwt_decode from "jwt-decode";
import { LoginResponse, MyUserInfo } from "lemmy-js-client"; import { LoginResponse, MyUserInfo } from "lemmy-js-client";
import { BehaviorSubject, Subject } from "rxjs"; import { BehaviorSubject, Subject } from "rxjs";
import { isHttps } from "../env"; import { isHttps } from "../env";
import { i18n } from "../i18next";
import { isBrowser, toast } from "../utils";
interface Claims { interface Claims {
sub: number; sub: number;
@ -11,11 +14,16 @@ interface Claims {
iat: number; iat: number;
} }
interface JwtInfo {
claims: Claims;
jwt: string;
}
export class UserService { export class UserService {
private static _instance: UserService; private static _instance: UserService;
public myUserInfo: MyUserInfo; public myUserInfo: Option<MyUserInfo> = None;
public claims: Claims; public jwtInfo: Option<JwtInfo> = None;
public jwtSub: Subject<string> = new Subject<string>(); public jwtSub: Subject<Option<JwtInfo>> = new Subject<Option<JwtInfo>>();
public unreadInboxCountSub: BehaviorSubject<number> = public unreadInboxCountSub: BehaviorSubject<number> =
new BehaviorSubject<number>(0); new BehaviorSubject<number>(0);
public unreadReportCountSub: BehaviorSubject<number> = public unreadReportCountSub: BehaviorSubject<number> =
@ -24,12 +32,7 @@ export class UserService {
new BehaviorSubject<number>(0); new BehaviorSubject<number>(0);
private constructor() { private constructor() {
if (this.auth) { this.setJwtInfo();
this.setClaims(this.auth);
} else {
// setTheme();
console.log("No JWT cookie found.");
}
} }
public login(res: LoginResponse) { public login(res: LoginResponse) {
@ -37,26 +40,42 @@ export class UserService {
expires.setDate(expires.getDate() + 365); expires.setDate(expires.getDate() + 365);
IsomorphicCookie.save("jwt", res.jwt, { expires, secure: isHttps }); IsomorphicCookie.save("jwt", res.jwt, { expires, secure: isHttps });
console.log("jwt cookie set"); console.log("jwt cookie set");
this.setClaims(res.jwt); this.setJwtInfo();
} }
public logout() { public logout() {
this.claims = undefined; this.jwtInfo = None;
this.myUserInfo = undefined; this.myUserInfo = None;
// setTheme(); this.jwtSub.next(this.jwtInfo);
this.jwtSub.next("");
IsomorphicCookie.remove("jwt"); // TODO is sometimes unreliable for some reason IsomorphicCookie.remove("jwt"); // TODO is sometimes unreliable for some reason
document.cookie = "jwt=; Max-Age=0; path=/; domain=" + location.host; document.cookie = "jwt=; Max-Age=0; path=/; domain=" + location.host;
location.reload(); // TODO may not be necessary anymore
console.log("Logged out."); console.log("Logged out.");
} }
public get auth(): string { public auth(throwErr = true): Result<string, string> {
return IsomorphicCookie.load("jwt"); // Can't use match to convert to result for some reason
let jwt = this.jwtInfo.map(j => j.jwt);
if (jwt.isSome()) {
return Ok(jwt.unwrap());
} else {
let msg = "No JWT cookie found";
if (throwErr && isBrowser()) {
console.log(msg);
toast(i18n.t("not_logged_in"), "danger");
}
return Err(msg);
}
} }
private setClaims(jwt: string) { private setJwtInfo() {
this.claims = jwt_decode(jwt); let jwt = IsomorphicCookie.load("jwt");
this.jwtSub.next(jwt);
if (jwt) {
let jwtInfo: JwtInfo = { jwt, claims: jwt_decode(jwt) };
this.jwtInfo = Some(jwtInfo);
this.jwtSub.next(this.jwtInfo);
}
} }
public static get Instance() { public static get Instance() {

View file

@ -1,4 +1,3 @@
import { PersonViewSafe, WebSocketJsonResponse } from "lemmy-js-client";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { share } from "rxjs/operators"; import { share } from "rxjs/operators";
import { import {
@ -15,9 +14,6 @@ export class WebSocketService {
private ws: WS; private ws: WS;
public subject: Observable<any>; public subject: Observable<any>;
public admins: PersonViewSafe[];
public banned: PersonViewSafe[];
private constructor() { private constructor() {
let firstConnect = true; let firstConnect = true;
@ -34,7 +30,7 @@ export class WebSocketService {
console.log(`Connected to ${wsUri}`); console.log(`Connected to ${wsUri}`);
if (!firstConnect) { if (!firstConnect) {
let res: WebSocketJsonResponse<any> = { let res = {
reconnect: true, reconnect: true,
}; };
obs.next(res); obs.next(res);

View file

@ -1,3 +1,5 @@
import { None, Option, Result, Some } from "@sniptt/monads";
import { ClassConstructor, deserialize, serialize } from "class-transformer";
import emojiShortName from "emoji-short-name"; import emojiShortName from "emoji-short-name";
import { import {
BlockCommunityResponse, BlockCommunityResponse,
@ -5,6 +7,7 @@ import {
CommentReportView, CommentReportView,
CommentView, CommentView,
CommunityBlockView, CommunityBlockView,
CommunityModeratorView,
CommunityView, CommunityView,
GetSiteMetadata, GetSiteMetadata,
GetSiteResponse, GetSiteResponse,
@ -22,9 +25,6 @@ import {
Search, Search,
SearchType, SearchType,
SortType, SortType,
UserOperation,
WebSocketJsonResponse,
WebSocketResponse,
} from "lemmy-js-client"; } from "lemmy-js-client";
import markdown_it from "markdown-it"; import markdown_it from "markdown-it";
import markdown_it_container from "markdown-it-container"; import markdown_it_container from "markdown-it-container";
@ -72,6 +72,7 @@ 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 = 20;
export const trendingFetchLimit = 6;
export const mentionDropdownFetchLimit = 10; export const mentionDropdownFetchLimit = 10;
export const relTags = "noopener nofollow"; export const relTags = "noopener nofollow";
@ -98,20 +99,6 @@ export function randomStr(
.join(""); .join("");
} }
export function wsJsonToRes<ResponseType>(
msg: WebSocketJsonResponse<ResponseType>
): WebSocketResponse<ResponseType> {
return {
op: wsUserOp(msg),
data: msg.data,
};
}
export function wsUserOp(msg: any): UserOperation {
let opStr: string = msg.op;
return UserOperation[opStr];
}
export const md = new markdown_it({ export const md = new markdown_it({
html: false, html: false,
linkify: true, linkify: true,
@ -192,30 +179,135 @@ export function futureDaysToUnixTime(days: number): number {
} }
export function canMod( export function canMod(
myUserInfo: MyUserInfo, mods: Option<CommunityModeratorView[]>,
modIds: number[], admins: Option<PersonViewSafe[]>,
creator_id: number, creator_id: number,
myUserInfo = UserService.Instance.myUserInfo,
onSelf = false onSelf = false
): boolean { ): boolean {
// You can do moderator actions only on the mods added after you. // You can do moderator actions only on the mods added after you.
if (myUserInfo) { let adminsThenMods = admins
let yourIndex = modIds.findIndex( .unwrapOr([])
id => id == myUserInfo.local_user_view.person.id .map(a => a.person.id)
); .concat(mods.unwrapOr([]).map(m => m.moderator.id));
if (yourIndex == -1) {
return false; return myUserInfo.match({
} else { some: me => {
// onSelf +1 on mod actions not for yourself, IE ban, remove, etc let myIndex = adminsThenMods.findIndex(
modIds = modIds.slice(0, yourIndex + (onSelf ? 0 : 1)); id => id == me.local_user_view.person.id
return !modIds.includes(creator_id); );
} if (myIndex == -1) {
} else { return false;
return false; } else {
} // onSelf +1 on mod actions not for yourself, IE ban, remove, etc
adminsThenMods = adminsThenMods.slice(0, myIndex + (onSelf ? 0 : 1));
return !adminsThenMods.includes(creator_id);
}
},
none: false,
});
} }
export function isMod(modIds: number[], creator_id: number): boolean { export function canAdmin(
return modIds.includes(creator_id); admins: Option<PersonViewSafe[]>,
creator_id: number,
myUserInfo = UserService.Instance.myUserInfo
): boolean {
return canMod(None, admins, creator_id, myUserInfo);
}
export function isMod(
mods: Option<CommunityModeratorView[]>,
creator_id: number
): boolean {
return mods.match({
some: mods => mods.map(m => m.moderator.id).includes(creator_id),
none: false,
});
}
export function amMod(
mods: Option<CommunityModeratorView[]>,
myUserInfo = UserService.Instance.myUserInfo
): boolean {
return myUserInfo.match({
some: mui => isMod(mods, mui.local_user_view.person.id),
none: false,
});
}
export function isAdmin(
admins: Option<PersonViewSafe[]>,
creator_id: number
): boolean {
return admins.match({
some: admins => admins.map(a => a.person.id).includes(creator_id),
none: false,
});
}
export function amAdmin(
admins: Option<PersonViewSafe[]>,
myUserInfo = UserService.Instance.myUserInfo
): boolean {
return myUserInfo.match({
some: mui => isAdmin(admins, mui.local_user_view.person.id),
none: false,
});
}
export function amCommunityCreator(
mods: Option<CommunityModeratorView[]>,
creator_id: number,
myUserInfo = UserService.Instance.myUserInfo
): boolean {
return mods.match({
some: mods =>
myUserInfo
.map(mui => mui.local_user_view.person.id)
.match({
some: myId =>
myId == mods[0].moderator.id &&
// Don't allow mod actions on yourself
myId != creator_id,
none: false,
}),
none: false,
});
}
export function amSiteCreator(
admins: Option<PersonViewSafe[]>,
creator_id: number,
myUserInfo = UserService.Instance.myUserInfo
): boolean {
return admins.match({
some: admins =>
myUserInfo
.map(mui => mui.local_user_view.person.id)
.match({
some: myId =>
myId == admins[0].person.id &&
// Don't allow mod actions on yourself
myId != creator_id,
none: false,
}),
none: false,
});
}
export function amTopMod(
mods: Option<CommunityModeratorView[]>,
myUserInfo = UserService.Instance.myUserInfo
): boolean {
return mods.match({
some: mods =>
myUserInfo.match({
some: mui => mods[0].moderator.id == mui.local_user_view.person.id,
none: false,
}),
none: false,
});
} }
const imageRegex = /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/; const imageRegex = /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/;
@ -321,13 +413,14 @@ export function debounce(func: any, wait = 1000, immediate = false) {
}; };
} }
export function getLanguages(override?: string): string[] { export function getLanguages(
let myUserInfo = UserService.Instance.myUserInfo; override?: string,
let lang = myUserInfo = UserService.Instance.myUserInfo
override || ): string[] {
(myUserInfo?.local_user_view.local_user.lang let myLang = myUserInfo
? myUserInfo.local_user_view.local_user.lang .map(m => m.local_user_view.local_user.lang)
: "browser"); .unwrapOr("browser");
let lang = override || myLang;
if (lang == "browser" && isBrowser()) { if (lang == "browser" && isBrowser()) {
return getBrowserLanguages(); return getBrowserLanguages();
@ -406,24 +499,26 @@ export function objectFlip(obj: any) {
return ret; return ret;
} }
export function showAvatars(): boolean { export function showAvatars(
return ( myUserInfo: Option<MyUserInfo> = UserService.Instance.myUserInfo
UserService.Instance.myUserInfo?.local_user_view.local_user.show_avatars || ): boolean {
!UserService.Instance.myUserInfo return myUserInfo
); .map(m => m.local_user_view.local_user.show_avatars)
.unwrapOr(true);
} }
export function showScores(): boolean { export function showScores(
return ( myUserInfo: Option<MyUserInfo> = UserService.Instance.myUserInfo
UserService.Instance.myUserInfo?.local_user_view.local_user.show_scores || ): boolean {
!UserService.Instance.myUserInfo return myUserInfo
); .map(m => m.local_user_view.local_user.show_scores)
.unwrapOr(true);
} }
export function isCakeDay(published: string): boolean { export function isCakeDay(published: string): boolean {
// moment(undefined) or moment.utc(undefined) returns the current date/time // moment(undefined) or moment.utc(undefined) returns the current date/time
// moment(null) or moment.utc(null) returns null // moment(null) or moment.utc(null) returns null
const createDate = moment.utc(published || null).local(); const createDate = moment.utc(published).local();
const currentDate = moment(new Date()); const currentDate = moment(new Date());
return ( return (
@ -472,7 +567,7 @@ export function pictrsDeleteToast(
interface NotifyInfo { interface NotifyInfo {
name: string; name: string;
icon?: string; icon: Option<string>;
link: string; link: string;
body: string; body: string;
} }
@ -484,7 +579,7 @@ export function messageToastify(info: NotifyInfo, router: any) {
let toast = Toastify({ let toast = Toastify({
text: `${htmlBody}<br />${info.name}`, text: `${htmlBody}<br />${info.name}`,
avatar: info.icon ? info.icon : null, avatar: info.icon,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
className: "text-dark", className: "text-dark",
close: true, close: true,
@ -666,16 +761,18 @@ async function communitySearch(text: string): Promise<CommunityTribute[]> {
export function getListingTypeFromProps( export function getListingTypeFromProps(
props: any, props: any,
defaultListingType: ListingType defaultListingType: ListingType,
myUserInfo = UserService.Instance.myUserInfo
): ListingType { ): ListingType {
return props.match.params.listing_type return props.match.params.listing_type
? routeListingTypeToEnum(props.match.params.listing_type) ? routeListingTypeToEnum(props.match.params.listing_type)
: UserService.Instance.myUserInfo : myUserInfo.match({
? Object.values(ListingType)[ some: me =>
UserService.Instance.myUserInfo.local_user_view.local_user Object.values(ListingType)[
.default_listing_type me.local_user_view.local_user.default_listing_type
] ],
: defaultListingType; none: defaultListingType,
});
} }
export function getListingTypeFromPropsNoDefault(props: any): ListingType { export function getListingTypeFromPropsNoDefault(props: any): ListingType {
@ -691,15 +788,19 @@ export function getDataTypeFromProps(props: any): DataType {
: DataType.Post; : DataType.Post;
} }
export function getSortTypeFromProps(props: any): SortType { export function getSortTypeFromProps(
props: any,
myUserInfo = UserService.Instance.myUserInfo
): SortType {
return props.match.params.sort return props.match.params.sort
? routeSortTypeToEnum(props.match.params.sort) ? routeSortTypeToEnum(props.match.params.sort)
: UserService.Instance.myUserInfo : myUserInfo.match({
? Object.values(SortType)[ some: mui =>
UserService.Instance.myUserInfo.local_user_view.local_user Object.values(SortType)[
.default_sort_type mui.local_user_view.local_user.default_sort_type
] ],
: SortType.Active; none: SortType.Active,
});
} }
export function getPageFromProps(props: any): number { export function getPageFromProps(props: any): number {
@ -744,42 +845,53 @@ export function saveCommentRes(data: CommentView, comments: CommentView[]) {
} }
} }
// TODO Should only use the return now, no state?
export function updatePersonBlock( export function updatePersonBlock(
data: BlockPersonResponse data: BlockPersonResponse,
): PersonBlockView[] { myUserInfo = UserService.Instance.myUserInfo
if (data.blocked) { ): Option<PersonBlockView[]> {
UserService.Instance.myUserInfo.person_blocks.push({ return myUserInfo.match({
person: UserService.Instance.myUserInfo.local_user_view.person, some: (mui: MyUserInfo) => {
target: data.person_view.person, if (data.blocked) {
}); mui.person_blocks.push({
toast(`${i18n.t("blocked")} ${data.person_view.person.name}`); person: mui.local_user_view.person,
} else { target: data.person_view.person,
UserService.Instance.myUserInfo.person_blocks = });
UserService.Instance.myUserInfo.person_blocks.filter( toast(`${i18n.t("blocked")} ${data.person_view.person.name}`);
i => i.target.id != data.person_view.person.id } else {
); mui.person_blocks = mui.person_blocks.filter(
toast(`${i18n.t("unblocked")} ${data.person_view.person.name}`); i => i.target.id != data.person_view.person.id
} );
return UserService.Instance.myUserInfo.person_blocks; toast(`${i18n.t("unblocked")} ${data.person_view.person.name}`);
}
return Some(mui.person_blocks);
},
none: None,
});
} }
export function updateCommunityBlock( export function updateCommunityBlock(
data: BlockCommunityResponse data: BlockCommunityResponse,
): CommunityBlockView[] { myUserInfo = UserService.Instance.myUserInfo
if (data.blocked) { ): Option<CommunityBlockView[]> {
UserService.Instance.myUserInfo.community_blocks.push({ return myUserInfo.match({
person: UserService.Instance.myUserInfo.local_user_view.person, some: (mui: MyUserInfo) => {
community: data.community_view.community, if (data.blocked) {
}); mui.community_blocks.push({
toast(`${i18n.t("blocked")} ${data.community_view.community.name}`); person: mui.local_user_view.person,
} else { community: data.community_view.community,
UserService.Instance.myUserInfo.community_blocks = });
UserService.Instance.myUserInfo.community_blocks.filter( toast(`${i18n.t("blocked")} ${data.community_view.community.name}`);
i => i.community.id != data.community_view.community.id } else {
); mui.community_blocks = mui.community_blocks.filter(
toast(`${i18n.t("unblocked")} ${data.community_view.community.name}`); i => i.community.id != data.community_view.community.id
} );
return UserService.Instance.myUserInfo.community_blocks; toast(`${i18n.t("unblocked")} ${data.community_view.community.name}`);
}
return Some(mui.community_blocks);
},
none: None,
});
} }
export function createCommentLikeRes( export function createCommentLikeRes(
@ -910,7 +1022,8 @@ function commentSort(tree: CommentNodeI[], sort: CommentSortType) {
(a, b) => (a, b) =>
+a.comment_view.comment.removed - +b.comment_view.comment.removed || +a.comment_view.comment.removed - +b.comment_view.comment.removed ||
+a.comment_view.comment.deleted - +b.comment_view.comment.deleted || +a.comment_view.comment.deleted - +b.comment_view.comment.deleted ||
hotRankComment(b.comment_view) - hotRankComment(a.comment_view) hotRankComment(b.comment_view as CommentView) -
hotRankComment(a.comment_view as CommentView)
); );
} }
@ -953,6 +1066,7 @@ export function buildCommentsTree(
let node: CommentNodeI = { let node: CommentNodeI = {
comment_view: comment_view, comment_view: comment_view,
children: [], children: [],
depth: 0,
}; };
map.set(comment_view.comment.id, { ...node }); map.set(comment_view.comment.id, { ...node });
} }
@ -960,15 +1074,18 @@ export function buildCommentsTree(
for (let comment_view of comments) { for (let comment_view of comments) {
let child = map.get(comment_view.comment.id); let child = map.get(comment_view.comment.id);
let parent_id = comment_view.comment.parent_id; let parent_id = comment_view.comment.parent_id;
if (parent_id) { parent_id.match({
let parent = map.get(parent_id); some: parentId => {
// Necessary because blocked comment might not exist let parent = map.get(parentId);
if (parent) { // Necessary because blocked comment might not exist
parent.children.push(child); if (parent) {
} parent.children.push(child);
} else { }
tree.push(child); },
} none: () => {
tree.push(child);
},
});
setDepth(child); setDepth(child);
} }
@ -993,35 +1110,41 @@ export function insertCommentIntoTree(tree: CommentNodeI[], cv: CommentView) {
depth: 0, depth: 0,
}; };
if (cv.comment.parent_id) { cv.comment.parent_id.match({
let parentComment = searchCommentTree(tree, cv.comment.parent_id); some: parentId => {
if (parentComment) { let parentComment = searchCommentTree(tree, parentId);
node.depth = parentComment.depth + 1; parentComment.match({
parentComment.children.unshift(node); some: pComment => {
} node.depth = pComment.depth + 1;
} else { pComment.children.unshift(node);
tree.unshift(node); },
} none: void 0,
});
},
none: () => {
tree.unshift(node);
},
});
} }
export function searchCommentTree( export function searchCommentTree(
tree: CommentNodeI[], tree: CommentNodeI[],
id: number id: number
): CommentNodeI { ): Option<CommentNodeI> {
for (let node of tree) { for (let node of tree) {
if (node.comment_view.comment.id === id) { if (node.comment_view.comment.id === id) {
return node; return Some(node);
} }
for (const child of node.children) { for (const child of node.children) {
const res = searchCommentTree([child], id); let res = searchCommentTree([child], id);
if (res) { if (res.isSome()) {
return res; return res;
} }
} }
} }
return null; return None;
} }
export const colorList: string[] = [ export const colorList: string[] = [
@ -1044,7 +1167,7 @@ export function hostname(url: string): string {
export function validTitle(title?: string): boolean { export function validTitle(title?: string): boolean {
// Initial title is null, minimum length is taken care of by textarea's minLength={3} // Initial title is null, minimum length is taken care of by textarea's minLength={3}
if (title === null || title.length < 3) return true; if (!title || title.length < 3) return true;
const regex = new RegExp(/.*\S.*/, "g"); const regex = new RegExp(/.*\S.*/, "g");
@ -1068,11 +1191,51 @@ export function isBrowser() {
return typeof window !== "undefined"; return typeof window !== "undefined";
} }
export function setIsoData(context: any): IsoData { export function setIsoData<Type1, Type2, Type3, Type4, Type5>(
let isoData: IsoData = isBrowser() context: any,
? window.isoData cls1?: ClassConstructor<Type1>,
: context.router.staticContext; cls2?: ClassConstructor<Type2>,
return isoData; cls3?: ClassConstructor<Type3>,
cls4?: ClassConstructor<Type4>,
cls5?: ClassConstructor<Type5>
): IsoData {
// If its the browser, you need to deserialize the data from the window
if (isBrowser()) {
let json = window.isoData;
let routeData = json.routeData;
let routeDataOut: any[] = [];
// Can't do array looping because of specific type constructor required
if (routeData[0]) {
routeDataOut[0] = convertWindowJson(cls1, routeData[0]);
}
if (routeData[1]) {
routeDataOut[1] = convertWindowJson(cls2, routeData[1]);
}
if (routeData[2]) {
routeDataOut[2] = convertWindowJson(cls3, routeData[2]);
}
if (routeData[3]) {
routeDataOut[3] = convertWindowJson(cls4, routeData[3]);
}
if (routeData[4]) {
routeDataOut[4] = convertWindowJson(cls5, routeData[4]);
}
let isoData: IsoData = {
path: json.path,
site_res: convertWindowJson(GetSiteResponse, json.site_res),
routeData: routeDataOut,
};
return isoData;
} else return context.router.staticContext;
}
/**
* Necessary since window ISOData can't store function types like Option
*/
export function convertWindowJson<T>(cls: ClassConstructor<T>, data: any): T {
return deserialize(cls, serialize(data));
} }
export function wsSubscribe(parseMessage: any): Subscription { export function wsSubscribe(parseMessage: any): Subscription {
@ -1089,24 +1252,6 @@ export function wsSubscribe(parseMessage: any): Subscription {
} }
} }
export function setOptionalAuth(obj: any, auth = UserService.Instance.auth) {
if (auth) {
obj.auth = auth;
}
}
export function authField(
throwErr = true,
auth = UserService.Instance.auth
): string {
if (auth == null && throwErr) {
toast(i18n.t("not_logged_in"), "danger");
throw "Not logged in";
} else {
return auth;
}
}
moment.updateLocale("en", { moment.updateLocale("en", {
relativeTime: { relativeTime: {
future: "in %s", future: "in %s",
@ -1141,7 +1286,9 @@ export function restoreScrollPosition(context: any) {
} }
export function showLocal(isoData: IsoData): boolean { export function showLocal(isoData: IsoData): boolean {
return isoData.site_res.federated_instances?.linked.length > 0; return isoData.site_res.federated_instances
.map(f => f.linked.length > 0)
.unwrapOr(false);
} }
interface ChoicesValue { interface ChoicesValue {
@ -1168,12 +1315,15 @@ export function personToChoice(pvs: PersonViewSafe): ChoicesValue {
export async function fetchCommunities(q: string) { export async function fetchCommunities(q: string) {
let form: Search = { let form: Search = {
q, q,
type_: SearchType.Communities, type_: Some(SearchType.Communities),
sort: SortType.TopAll, sort: Some(SortType.TopAll),
listing_type: ListingType.All, listing_type: Some(ListingType.All),
page: 1, page: Some(1),
limit: fetchLimit, limit: Some(fetchLimit),
auth: authField(false), community_id: None,
community_name: None,
creator_id: None,
auth: auth(false).ok(),
}; };
let client = new LemmyHttp(httpBase); let client = new LemmyHttp(httpBase);
return client.search(form); return client.search(form);
@ -1182,12 +1332,15 @@ export async function fetchCommunities(q: string) {
export async function fetchUsers(q: string) { export async function fetchUsers(q: string) {
let form: Search = { let form: Search = {
q, q,
type_: SearchType.Users, type_: Some(SearchType.Users),
sort: SortType.TopAll, sort: Some(SortType.TopAll),
listing_type: ListingType.All, listing_type: Some(ListingType.All),
page: 1, page: Some(1),
limit: fetchLimit, limit: Some(fetchLimit),
auth: authField(false), community_id: None,
community_name: None,
creator_id: None,
auth: auth(false).ok(),
}; };
let client = new LemmyHttp(httpBase); let client = new LemmyHttp(httpBase);
return client.search(form); return client.search(form);
@ -1233,7 +1386,7 @@ export function communitySelectName(cv: CommunityView): string {
} }
export function personSelectName(pvs: PersonViewSafe): string { export function personSelectName(pvs: PersonViewSafe): string {
let pName = pvs.person.display_name || pvs.person.name; let pName = pvs.person.display_name.unwrapOr(pvs.person.name);
return pvs.person.local ? pName : `${hostname(pvs.person.actor_id)}/${pName}`; return pvs.person.local ? pName : `${hostname(pvs.person.actor_id)}/${pName}`;
} }
@ -1254,9 +1407,11 @@ export function numToSI(value: number): string {
} }
export function isBanned(ps: PersonSafe): boolean { export function isBanned(ps: PersonSafe): boolean {
let expires = ps.ban_expires;
// Add Z to convert from UTC date // Add Z to convert from UTC date
if (ps.ban_expires) { // TODO this check probably isn't necessary anymore
if (ps.banned && new Date(ps.ban_expires + "Z") > new Date()) { if (expires.isSome()) {
if (ps.banned && new Date(expires.unwrap() + "Z") > new Date()) {
return true; return true;
} else { } else {
return false; return false;
@ -1271,3 +1426,15 @@ export function pushNotNull(array: any[], new_item?: any) {
array.push(...new_item); array.push(...new_item);
} }
} }
export function auth(throwErr = true): Result<string, string> {
return UserService.Instance.auth(throwErr);
}
export function enableDownvotes(siteRes: GetSiteResponse): boolean {
return siteRes.site_view.map(s => s.site.enable_downvotes).unwrapOr(true);
}
export function enableNsfw(siteRes: GetSiteResponse): boolean {
return siteRes.site_view.map(s => s.site.enable_nsfw).unwrapOr(false);
}

View file

@ -18,7 +18,8 @@
"skipLibCheck": true, "skipLibCheck": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true "experimentalDecorators": true,
"noFallthroughCasesInSwitch": true
}, },
"include": [ "include": [
"src/**/*", "src/**/*",

148
yarn.lock
View file

@ -95,6 +95,15 @@
jsesc "^2.5.1" jsesc "^2.5.1"
source-map "^0.5.0" source-map "^0.5.0"
"@babel/generator@^7.18.2":
version "7.18.2"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.2.tgz#33873d6f89b21efe2da63fe554460f3df1c5880d"
integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==
dependencies:
"@babel/types" "^7.18.2"
"@jridgewell/gen-mapping" "^0.3.0"
jsesc "^2.5.1"
"@babel/helper-annotate-as-pure@^7.16.7": "@babel/helper-annotate-as-pure@^7.16.7":
version "7.16.7" version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862"
@ -156,6 +165,19 @@
"@babel/helper-replace-supers" "^7.16.7" "@babel/helper-replace-supers" "^7.16.7"
"@babel/helper-split-export-declaration" "^7.16.7" "@babel/helper-split-export-declaration" "^7.16.7"
"@babel/helper-create-class-features-plugin@^7.18.0":
version "7.18.0"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.0.tgz#fac430912606331cb075ea8d82f9a4c145a4da19"
integrity sha512-Kh8zTGR9de3J63e5nS0rQUdRs/kbtwoeQQ0sriS0lItjC96u8XXZN6lKpuyWd2coKSU13py/y+LTmThLuVX0Pg==
dependencies:
"@babel/helper-annotate-as-pure" "^7.16.7"
"@babel/helper-environment-visitor" "^7.16.7"
"@babel/helper-function-name" "^7.17.9"
"@babel/helper-member-expression-to-functions" "^7.17.7"
"@babel/helper-optimise-call-expression" "^7.16.7"
"@babel/helper-replace-supers" "^7.16.7"
"@babel/helper-split-export-declaration" "^7.16.7"
"@babel/helper-create-regexp-features-plugin@^7.16.7": "@babel/helper-create-regexp-features-plugin@^7.16.7":
version "7.16.7" version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.7.tgz#0cb82b9bac358eb73bfbd73985a776bfa6b14d48" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.7.tgz#0cb82b9bac358eb73bfbd73985a776bfa6b14d48"
@ -185,6 +207,11 @@
dependencies: dependencies:
"@babel/types" "^7.16.7" "@babel/types" "^7.16.7"
"@babel/helper-environment-visitor@^7.18.2":
version "7.18.2"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz#8a6d2dedb53f6bf248e31b4baf38739ee4a637bd"
integrity sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ==
"@babel/helper-explode-assignable-expression@^7.16.7": "@babel/helper-explode-assignable-expression@^7.16.7":
version "7.16.7" version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz#12a6d8522fdd834f194e868af6354e8650242b7a" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz#12a6d8522fdd834f194e868af6354e8650242b7a"
@ -230,6 +257,13 @@
dependencies: dependencies:
"@babel/types" "^7.16.7" "@babel/types" "^7.16.7"
"@babel/helper-member-expression-to-functions@^7.17.7":
version "7.17.7"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz#a34013b57d8542a8c4ff8ba3f747c02452a4d8c4"
integrity sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==
dependencies:
"@babel/types" "^7.17.0"
"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7":
version "7.16.7" version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437"
@ -277,6 +311,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5"
integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==
"@babel/helper-plugin-utils@^7.17.12":
version "7.17.12"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz#86c2347da5acbf5583ba0a10aed4c9bf9da9cf96"
integrity sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA==
"@babel/helper-remap-async-to-generator@^7.16.8": "@babel/helper-remap-async-to-generator@^7.16.8":
version "7.16.8" version "7.16.8"
resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3"
@ -297,6 +336,17 @@
"@babel/traverse" "^7.16.7" "@babel/traverse" "^7.16.7"
"@babel/types" "^7.16.7" "@babel/types" "^7.16.7"
"@babel/helper-replace-supers@^7.18.2":
version "7.18.2"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.2.tgz#41fdfcc9abaf900e18ba6e5931816d9062a7b2e0"
integrity sha512-XzAIyxx+vFnrOxiQrToSUOzUOn0e1J2Li40ntddek1Y69AXUTXoDJ40/D5RdjFu7s7qHiaeoTiempZcbuVXh2Q==
dependencies:
"@babel/helper-environment-visitor" "^7.18.2"
"@babel/helper-member-expression-to-functions" "^7.17.7"
"@babel/helper-optimise-call-expression" "^7.16.7"
"@babel/traverse" "^7.18.2"
"@babel/types" "^7.18.2"
"@babel/helper-simple-access@^7.16.7": "@babel/helper-simple-access@^7.16.7":
version "7.16.7" version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7"
@ -387,6 +437,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.9.tgz#9c94189a6062f0291418ca021077983058e171ef" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.9.tgz#9c94189a6062f0291418ca021077983058e171ef"
integrity sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg== integrity sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg==
"@babel/parser@^7.18.5":
version "7.18.5"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.5.tgz#337062363436a893a2d22faa60be5bb37091c83c"
integrity sha512-YZWVaglMiplo7v8f1oMQ5ZPQr0vn7HPeZXxXWsxXJRjGVrzUFn9OxFQl1sb5wzfootjA/yChhW84BV+383FSOw==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7":
version "7.16.7" version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050"
@ -429,6 +484,18 @@
"@babel/helper-plugin-utils" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-syntax-class-static-block" "^7.14.5" "@babel/plugin-syntax-class-static-block" "^7.14.5"
"@babel/plugin-proposal-decorators@^7.18.2":
version "7.18.2"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.18.2.tgz#dbe4086d2d42db489399783c3aa9272e9700afd4"
integrity sha512-kbDISufFOxeczi0v4NQP3p5kIeW6izn/6klfWBrIIdGZZe4UpHR+QU03FAoWjGGd9SUXAwbw2pup1kaL4OQsJQ==
dependencies:
"@babel/helper-create-class-features-plugin" "^7.18.0"
"@babel/helper-plugin-utils" "^7.17.12"
"@babel/helper-replace-supers" "^7.18.2"
"@babel/helper-split-export-declaration" "^7.16.7"
"@babel/plugin-syntax-decorators" "^7.17.12"
charcodes "^0.2.0"
"@babel/plugin-proposal-dynamic-import@^7.16.7": "@babel/plugin-proposal-dynamic-import@^7.16.7":
version "7.16.7" version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2"
@ -552,6 +619,13 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.14.5" "@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-syntax-decorators@^7.17.12":
version "7.17.12"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.17.12.tgz#02e8f678602f0af8222235271efea945cfdb018a"
integrity sha512-D1Hz0qtGTza8K2xGyEdVNCYLdVHukAcbQr4K3/s6r/esadyEriZovpJimQOpu8ju4/jV8dW/1xdaE0UpDroidw==
dependencies:
"@babel/helper-plugin-utils" "^7.17.12"
"@babel/plugin-syntax-dynamic-import@^7.8.3": "@babel/plugin-syntax-dynamic-import@^7.8.3":
version "7.8.3" version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
@ -1092,6 +1166,22 @@
debug "^4.1.0" debug "^4.1.0"
globals "^11.1.0" globals "^11.1.0"
"@babel/traverse@^7.18.2":
version "7.18.5"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.5.tgz#94a8195ad9642801837988ab77f36e992d9a20cd"
integrity sha512-aKXj1KT66sBj0vVzk6rEeAO6Z9aiiQ68wfDgge3nHhA/my6xMM/7HGQUNumKZaoa2qUPQ5whJG9aAifsxUKfLA==
dependencies:
"@babel/code-frame" "^7.16.7"
"@babel/generator" "^7.18.2"
"@babel/helper-environment-visitor" "^7.18.2"
"@babel/helper-function-name" "^7.17.9"
"@babel/helper-hoist-variables" "^7.16.7"
"@babel/helper-split-export-declaration" "^7.16.7"
"@babel/parser" "^7.18.5"
"@babel/types" "^7.18.4"
debug "^4.1.0"
globals "^11.1.0"
"@babel/types@^7", "@babel/types@^7.0.0-beta.54", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.4.4": "@babel/types@^7", "@babel/types@^7.0.0-beta.54", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.4.4":
version "7.16.8" version "7.16.8"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.8.tgz#0ba5da91dd71e0a4e7781a30f22770831062e3c1" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.8.tgz#0ba5da91dd71e0a4e7781a30f22770831062e3c1"
@ -1108,6 +1198,14 @@
"@babel/helper-validator-identifier" "^7.16.7" "@babel/helper-validator-identifier" "^7.16.7"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@babel/types@^7.18.2", "@babel/types@^7.18.4":
version "7.18.4"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.4.tgz#27eae9b9fd18e9dccc3f9d6ad051336f307be354"
integrity sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw==
dependencies:
"@babel/helper-validator-identifier" "^7.16.7"
to-fast-properties "^2.0.0"
"@discoveryjs/json-ext@^0.5.0": "@discoveryjs/json-ext@^0.5.0":
version "0.5.6" version "0.5.6"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f"
@ -1151,11 +1249,25 @@
update-notifier "^2.2.0" update-notifier "^2.2.0"
yargs "^8.0.2" yargs "^8.0.2"
"@jridgewell/gen-mapping@^0.3.0":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9"
integrity sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==
dependencies:
"@jridgewell/set-array" "^1.0.0"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.9"
"@jridgewell/resolve-uri@^3.0.3": "@jridgewell/resolve-uri@^3.0.3":
version "3.0.5" version "3.0.5"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c"
integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==
"@jridgewell/set-array@^1.0.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea"
integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==
"@jridgewell/sourcemap-codec@^1.4.10": "@jridgewell/sourcemap-codec@^1.4.10":
version "1.4.11" version "1.4.11"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec"
@ -1169,6 +1281,14 @@
"@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping@^0.3.9":
version "0.3.13"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea"
integrity sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@leichtgewicht/ip-codec@^2.0.1": "@leichtgewicht/ip-codec@^2.0.1":
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz#0300943770e04231041a51bd39f0439b5c7ab4f0" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz#0300943770e04231041a51bd39f0439b5c7ab4f0"
@ -1200,6 +1320,11 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9"
integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA== integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==
"@sniptt/monads@^0.5.10":
version "0.5.10"
resolved "https://registry.yarnpkg.com/@sniptt/monads/-/monads-0.5.10.tgz#a80cd00738bbd682d36d36dd36bdc0bddc96eb76"
integrity sha512-+agDOv9DpDV+9e2zN/Vmdk+XaqGx5Sykl0fqhqgiJ90r18nsBkxe44DmZ2sA1HYK+MSsBeZBiAr6pq4w+5uhfw==
"@types/autosize@^4.0.0": "@types/autosize@^4.0.0":
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/autosize/-/autosize-4.0.1.tgz#999a7c305b96766248044ebaac1a0299961f3b61" resolved "https://registry.yarnpkg.com/@types/autosize/-/autosize-4.0.1.tgz#999a7c305b96766248044ebaac1a0299961f3b61"
@ -2254,6 +2379,11 @@ chalk@^4.0.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
charcodes@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/charcodes/-/charcodes-0.2.0.tgz#5208d327e6cc05f99eb80ffc814707572d1f14e4"
integrity sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ==
check-password-strength@^2.0.5: check-password-strength@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/check-password-strength/-/check-password-strength-2.0.5.tgz#bb10da01d24bd69e5e629c5cea2a6b729e5061af" resolved "https://registry.yarnpkg.com/check-password-strength/-/check-password-strength-2.0.5.tgz#bb10da01d24bd69e5e629c5cea2a6b729e5061af"
@ -2323,6 +2453,11 @@ cidr-regex@1.0.6:
resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1" resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1"
integrity sha1-dKv9YZ3zcLnVSrFEdVaOl91kwME= integrity sha1-dKv9YZ3zcLnVSrFEdVaOl91kwME=
class-transformer@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336"
integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==
classnames@^2.3.1: classnames@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
@ -4813,10 +4948,10 @@ lcid@^1.0.0:
dependencies: dependencies:
invert-kv "^1.0.0" invert-kv "^1.0.0"
lemmy-js-client@0.16.4: lemmy-js-client@0.17.0-rc.30:
version "0.16.4" version "0.17.0-rc.30"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.16.4.tgz#d24bae2b0d93c4d13eb4a5e5ddceaa2999f94740" resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.0-rc.30.tgz#91cc926e662a5cd27f87cd2e6cdfcd210176745a"
integrity sha512-EFHl6tbFZ0jk8VE68bgZOXoWuNHVzfcsyoAEZeHP6f8PkQ1g9zjxB/e3b5cIG2fFzOLsYIDh2w/SJy21WkFiiA== integrity sha512-AcG8IZNNTa54BAXEqsL/QNlyPPwLntRLWpIOw9S3u84824d5inL7UCKnyx0UMbQklUuH/D3E2K9WNmZiUdvr3A==
levn@^0.4.1: levn@^0.4.1:
version "0.4.1" version "0.4.1"
@ -6673,6 +6808,11 @@ redux@^4.1.2:
dependencies: dependencies:
"@babel/runtime" "^7.9.2" "@babel/runtime" "^7.9.2"
reflect-metadata@^0.1.13:
version "0.1.13"
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
regenerate-unicode-properties@^9.0.0: regenerate-unicode-properties@^9.0.0:
version "9.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326"