Merge branch 'main' into breakout-role-utils

This commit is contained in:
Alec Armbruster 2023-06-17 08:44:47 -04:00 committed by GitHub
commit 7f48a38b72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 8135 additions and 4751 deletions

@ -1 +1 @@
Subproject commit f45ddff206adb52ab0ac7555bf14978edac5d2f2 Subproject commit c9a07885f35cf334d3cf167cb57587a8177fc3fb

View file

@ -14,12 +14,21 @@
"dev": "yarn start", "dev": "yarn start",
"lint": "node generate_translations.js && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx \"src/**\" && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"", "lint": "node generate_translations.js && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx \"src/**\" && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"",
"prepare": "husky install", "prepare": "husky install",
"start": "yarn build:dev --watch" "start": "yarn build:dev --watch",
"themes:build": "sass src/assets/css/themes/:src/assets/css/themes",
"themes:watch": "sass --watch src/assets/css/themes/:src/assets/css/themes"
}, },
"lint-staged": { "lint-staged": {
"*.{ts,tsx,js}": ["prettier --write", "eslint --fix"], "*.{ts,tsx,js}": [
"*.{css, scss}": ["prettier --write"], "prettier --write",
"package.json": ["sortpack"] "eslint --fix"
],
"*.{css, scss}": [
"prettier --write"
],
"package.json": [
"sortpack"
]
}, },
"dependencies": { "dependencies": {
"@babel/plugin-proposal-decorators": "^7.21.0", "@babel/plugin-proposal-decorators": "^7.21.0",
@ -94,7 +103,7 @@
"@types/toastify-js": "^1.11.1", "@types/toastify-js": "^1.11.1",
"@typescript-eslint/eslint-plugin": "^5.59.5", "@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5", "@typescript-eslint/parser": "^5.59.5",
"bootswatch": "^5.2.3", "bootstrap-v4": "npm:bootstrap@^4.6.2",
"eslint": "^8.40.0", "eslint": "^8.40.0",
"eslint-plugin-inferno": "^7.32.2", "eslint-plugin-inferno": "^7.32.2",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",

View file

@ -46,7 +46,7 @@
} }
.md-div p:last-child { .md-div p:last-child {
margin-bottom: 0px; margin-bottom: 0;
} }
.md-div img { .md-div img {
@ -371,7 +371,7 @@ br.big {
} }
.tribute-container li { .tribute-container li {
padding: 5px 5px; padding: 5px;
cursor: pointer; cursor: pointer;
} }
@ -410,13 +410,22 @@ br.big {
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.lang-select-action {
width: 100px;
}
.lang-select-action:focus { .emoji-picker {
width: auto;
}
em-emoji-picker {
width: 100%; width: 100%;
} }
.skip-link {
top: -40px;
transition: top 0.3s ease;
}
@media (prefers-reduced-motion: reduce) {
.skip-link {
transition: none;
}
}
.skip-link:focus {
top: 0;
}

View file

@ -0,0 +1,108 @@
$white: #fff;
$gray-100: #f8f9fa;
$gray-200: #ebebeb;
$gray-300: #dee2e6;
$gray-400: #ced4da;
$gray-500: #adb5bd;
$gray-600: #888;
$gray-700: #444;
$gray-800: #303030;
$gray-900: #222;
$black: #000;
$blue: #375a7f;
$indigo: #6610f2;
$purple: #6f42c1;
$pink: #e83e8c;
$red: #e74c3c;
$orange: #fd7e14;
$yellow: #f39c12;
$green: #00bc8c;
$teal: #20c997;
$cyan: #3498db;
$primary: $blue;
$secondary: #444;
$success: $green;
$info: $cyan;
$warning: $yellow;
$danger: $red;
$light: $gray-800;
$dark: $gray-300;
$yiq-contrasted-threshold: 175;
$body-bg: $gray-900;
$body-color: $gray-300;
$link-color: $red;
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol";
$font-size-base: 0.9375rem;
$h1-font-size: 3rem;
$h2-font-size: 2.5rem;
$h3-font-size: 2rem;
$text-muted: $gray-600;
$table-accent-bg: $gray-800;
$table-border-color: $gray-700;
$input-border-color: $body-bg;
$input-group-addon-color: $gray-500;
$input-group-addon-bg: $gray-700;
$custom-file-color: $gray-500;
$custom-file-border-color: $body-bg;
$dropdown-bg: $gray-900;
$dropdown-border-color: $gray-700;
$dropdown-divider-bg: $gray-700;
$dropdown-link-color: $white;
$dropdown-link-hover-color: $white;
$dropdown-link-hover-bg: $primary;
$nav-link-padding-x: 2rem;
$nav-link-disabled-color: $gray-500;
$nav-tabs-border-color: $gray-700;
$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color
transparent;
$nav-tabs-link-active-color: $white;
$nav-tabs-link-active-border-color: $nav-tabs-border-color
$nav-tabs-border-color transparent;
$navbar-padding-y: 1rem;
$navbar-dark-color: rgba($white, 0.6);
$navbar-dark-hover-color: $white;
$navbar-light-color: rgba($white, 0.6);
$navbar-light-hover-color: $white;
$navbar-light-active-color: $white;
$navbar-light-toggler-border-color: rgba($gray-900, 0.1);
$pagination-color: $white;
$pagination-bg: $success;
$pagination-border-width: 0;
$pagination-border-color: transparent;
$pagination-hover-color: $white;
$pagination-hover-bg: lighten($success, 10%);
$pagination-hover-border-color: transparent;
$pagination-active-bg: $pagination-hover-bg;
$pagination-active-border-color: transparent;
$pagination-disabled-color: $white;
$pagination-disabled-bg: darken($success, 15%);
$pagination-disabled-border-color: transparent;
$jumbotron-bg: $gray-800;
$card-cap-bg: $gray-700;
$card-bg: $gray-800;
$popover-bg: $gray-800;
$popover-header-bg: $gray-700;
$toast-background-color: $gray-700;
$toast-header-background-color: $gray-800;
$modal-content-bg: $gray-800;
$modal-content-border-color: $gray-700;
$modal-header-border-color: $gray-700;
$progress-bg: $gray-700;
$list-group-bg: $gray-800;
$list-group-border-color: $gray-700;
$list-group-hover-bg: $gray-700;
$breadcrumb-bg: $gray-700;
$close-color: $white;
$close-text-shadow: none;
$pre-color: inherit;
$mark-bg: #333;
$custom-select-bg: $secondary;
$custom-select-color: $white;
$input-bg: $secondary;
$input-color: $white;
$input-disabled-bg: darken($secondary, 10%);
$light: $gray-800;
$navbar-light-brand-color: $white;
$navbar-light-brand-hover-color: $navbar-light-brand-color;

View file

@ -103,5 +103,5 @@ $input-bg: $secondary;
$input-color: $white; $input-color: $white;
$input-disabled-bg: darken($secondary, 10%); $input-disabled-bg: darken($secondary, 10%);
$light: $gray-800; $light: $gray-800;
$navbar-light-brand-color: $navbar-dark-active-color; $navbar-light-brand-color: $white;
$navbar-light-brand-hover-color: $navbar-dark-active-color; $navbar-light-brand-hover-color: $navbar-light-brand-color;

View file

@ -0,0 +1,47 @@
$white: #fff;
$gray-100: #f8f9fa;
$gray-200: #e9ecef;
$gray-300: #dee2e6;
$gray-400: #ced4da;
$gray-500: #adb5bd;
$gray-600: #6c757d;
$gray-700: #495057;
$gray-800: #343a40;
$gray-900: #212529;
$black: #000;
$blue: #007bff;
$indigo: #6610f2;
$white: #ffffff;
$orange: #f1641e;
$cyan: #02bdc2;
$green: #00c853;
$primary: #f1641e;
$secondary: #c80000;
$info: $blue;
$body-color: $gray-700;
$link-color: $primary;
$red: #d8486a;
$border-radius: 0.5rem;
$border-radius-lg: 0.5rem;
$border-radius-sm: 1rem;
$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Droid Sans",
"Segoe UI", "Helvetica", Arial, sans-serif;
$headings-color: $gray-700;
$input-btn-focus-color: rgba($primary, 0.75);
$form-feedback-valid-color: $info;
$navbar-light-color: $gray-600;
$black: #222222;
$navbar-dark-toggler-border-color: rgba($black, 0.1);
$navbar-light-active-color: $gray-900;
$card-color: $gray-700;
$card-cap-color: $gray-700;
$info: $blue;
$body-bg: #fff;
$success: $indigo;
$danger: darken($primary, 24%);
$navbar-light-hover-color: $gray-900;
$card-bg: $gray-100;
$border-color: $gray-700;
$mark-bg: rgb(255, 252, 239);
$font-weight-bold: 600;
$rounded-pill: 0.25rem;

View file

@ -1,11 +1,25 @@
$white: #fff;
$gray-100: #f8f9fa;
$gray-200: #e9ecef;
$gray-300: #dee2e6;
$gray-400: #ced4da;
$gray-500: #adb5bd;
$gray-600: #6c757d;
$gray-700: #495057;
$gray-800: #343a40;
$gray-900: #212529;
$black: #000;
$blue: #007bff;
$indigo: #6610f2;
$white: #ffffff; $white: #ffffff;
$orange: #f1641e; $orange: #f1641e;
$cyan: #02bdc2; $cyan: #02bdc2;
$green: #00c853; $green: #00c853;
$secondary: $green;
$body-color: $gray-700;
$link-color: theme-color("primary");
$primary: $orange; $primary: $orange;
$secondary: $green;
$info: $cyan;
$body-color: $gray-700;
$link-color: $primary;
$red: #d8486a; $red: #d8486a;
$border-radius: 0.5rem; $border-radius: 0.5rem;
$border-radius-lg: 0.5rem; $border-radius-lg: 0.5rem;
@ -13,8 +27,8 @@ $border-radius-sm: 1rem;
$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Droid Sans", $font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Droid Sans",
"Segoe UI", "Helvetica", Arial, sans-serif; "Segoe UI", "Helvetica", Arial, sans-serif;
$headings-color: $gray-700; $headings-color: $gray-700;
$input-btn-focus-color: rgba($component-active-bg, 0.75); $input-btn-focus-color: rgba($primary, 0.75);
$form-feedback-valid-color: theme-color("info"); $form-feedback-valid-color: $info;
$navbar-light-color: $gray-600; $navbar-light-color: $gray-600;
$black: #222222; $black: #222222;
$navbar-dark-toggler-border-color: rgba($black, 0.1); $navbar-dark-toggler-border-color: rgba($black, 0.1);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
@import "variables.darkly-red";
@import "../../../../node_modules/bootstrap-v4/scss/bootstrap";

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
@import "variables.darkly";
@import "../../../../node_modules/bootstrap-v4/scss/bootstrap";

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
@import "variables.litely-red";
@import "../../../../node_modules/bootstrap-v4/scss/bootstrap";

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
@import "variables.litely";
@import "../../../../node_modules/bootstrap-v4/scss/bootstrap";

View file

@ -17,9 +17,10 @@ import {
ILemmyConfig, ILemmyConfig,
InitialFetchRequest, InitialFetchRequest,
IsoDataOptionalSite, IsoDataOptionalSite,
RouteData,
} from "../shared/interfaces"; } from "../shared/interfaces";
import { routes } from "../shared/routes"; import { routes } from "../shared/routes";
import { RequestState, wrapClient } from "../shared/services/HttpService"; import { FailedRequestState, wrapClient } from "../shared/services/HttpService";
import { import {
ErrorPageData, ErrorPageData,
favIconPngUrl, favIconPngUrl,
@ -136,7 +137,7 @@ server.get("/*", async (req, res) => {
// This bypasses errors, so that the client can hit the error on its own, // This bypasses errors, so that the client can hit the error on its own,
// in order to remove the jwt on the browser. Necessary for wrong jwts // in order to remove the jwt on the browser. Necessary for wrong jwts
let site: GetSiteResponse | undefined = undefined; let site: GetSiteResponse | undefined = undefined;
const routeData: RequestState<any>[] = []; let routeData: RouteData = {};
let errorPageData: ErrorPageData | undefined = undefined; let errorPageData: ErrorPageData | undefined = undefined;
let try_site = await client.getSite(getSiteForm); let try_site = await client.getSite(getSiteForm);
if (try_site.state === "failed" && try_site.msg == "not_logged_in") { if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
@ -160,7 +161,7 @@ server.get("/*", async (req, res) => {
return res.redirect("/setup"); return res.redirect("/setup");
} }
if (site) { if (site && activeRoute?.fetchInitialData) {
const initialFetchReq: InitialFetchRequest = { const initialFetchReq: InitialFetchRequest = {
client, client,
auth, auth,
@ -169,26 +170,23 @@ server.get("/*", async (req, res) => {
site, site,
}; };
if (activeRoute?.fetchInitialData) { routeData = await activeRoute.fetchInitialData(initialFetchReq);
routeData.push(
...(await Promise.all([
...activeRoute.fetchInitialData(initialFetchReq),
]))
);
}
} }
} else if (try_site.state === "failed") { } else if (try_site.state === "failed") {
errorPageData = getErrorPageData(new Error(try_site.msg), site); errorPageData = getErrorPageData(new Error(try_site.msg), site);
} }
const error = Object.values(routeData).find(
res => res.state === "failed"
) as FailedRequestState | undefined;
// Redirect to the 404 if there's an API error // Redirect to the 404 if there's an API error
if (routeData[0] && routeData[0].state === "failed") { if (error) {
const error = routeData[0].msg; console.error(error.msg);
console.error(error); if (error.msg === "instance_is_private") {
if (error === "instance_is_private") {
return res.redirect(`/signup`); return res.redirect(`/signup`);
} else { } else {
errorPageData = getErrorPageData(new Error(error), site); errorPageData = getErrorPageData(new Error(error.msg), site);
} }
} }

View file

@ -1,4 +1,4 @@
import { Component } from "inferno"; import { Component, createRef, linkEvent, RefObject } from "inferno";
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 { i18n } from "../../i18next"; import { i18n } from "../../i18next";
@ -15,8 +15,15 @@ import { Theme } from "./theme";
export class App extends Component<any, any> { export class App extends Component<any, any> {
private isoData: IsoDataOptionalSite = setIsoData(this.context); private isoData: IsoDataOptionalSite = setIsoData(this.context);
private readonly mainContentRef: RefObject<HTMLElement>;
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.mainContentRef = createRef();
}
handleJumpToContent(event) {
event.preventDefault();
this.mainContentRef.current?.focus();
} }
render() { render() {
const siteRes = this.isoData.site_res; const siteRes = this.isoData.site_res;
@ -26,6 +33,12 @@ export class App extends Component<any, any> {
<> <>
<Provider i18next={i18n}> <Provider i18next={i18n}>
<div id="app" className="lemmy-site"> <div id="app" className="lemmy-site">
<a
className="skip-link bg-light text-dark p-2 text-decoration-none position-absolute start-0 z-3"
onClick={linkEvent(this, this.handleJumpToContent)}
>
${i18n.t("jump_to_content", "Jump to content")}
</a>
{siteView && ( {siteView && (
<Theme defaultTheme={siteView.local_site.default_theme} /> <Theme defaultTheme={siteView.local_site.default_theme} />
)} )}
@ -39,14 +52,16 @@ export class App extends Component<any, any> {
exact exact
component={routeProps => ( component={routeProps => (
<ErrorGuard> <ErrorGuard>
{RouteComponent && <main tabIndex={-1} ref={this.mainContentRef}>
(isAuthPath(path ?? "") ? ( {RouteComponent &&
<AuthGuard> (isAuthPath(path ?? "") ? (
<AuthGuard>
<RouteComponent {...routeProps} />
</AuthGuard>
) : (
<RouteComponent {...routeProps} /> <RouteComponent {...routeProps} />
</AuthGuard> ))}
) : ( </main>
<RouteComponent {...routeProps} />
))}
</ErrorGuard> </ErrorGuard>
)} )}
/> />

View file

@ -224,11 +224,14 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
)} )}
<li className="nav-item"> <li className="nav-item">
<a <a
className="nav-link" className="nav-link d-inline-flex align-items-center d-md-inline-block"
title={i18n.t("support_lemmy")} title={i18n.t("support_lemmy")}
href={donateLemmyUrl} href={donateLemmyUrl}
> >
<Icon icon="heart" classes="small" /> <Icon icon="heart" classes="small" />
<span className="d-inline ml-1 d-md-none ml-md-0">
{i18n.t("support_lemmy")}
</span>
</a> </a>
</li> </li>
</ul> </ul>
@ -236,22 +239,28 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
<li id="navSearch" className="nav-item"> <li id="navSearch" className="nav-item">
<NavLink <NavLink
to="/search" to="/search"
className="nav-link" className="nav-link d-inline-flex align-items-center d-md-inline-block"
title={i18n.t("search")} title={i18n.t("search")}
onMouseUp={linkEvent(this, handleCollapseClick)} onMouseUp={linkEvent(this, handleCollapseClick)}
> >
<Icon icon="search" /> <Icon icon="search" />
<span className="d-inline ml-1 d-md-none ml-md-0">
{i18n.t("search")}
</span>
</NavLink> </NavLink>
</li> </li>
{amAdmin() && ( {amAdmin() && (
<li id="navAdmin" className="nav-item"> <li id="navAdmin" className="nav-item">
<NavLink <NavLink
to="/admin" to="/admin"
className="nav-link" className="nav-link d-inline-flex align-items-center d-md-inline-block"
title={i18n.t("admin_settings")} title={i18n.t("admin_settings")}
onMouseUp={linkEvent(this, handleCollapseClick)} onMouseUp={linkEvent(this, handleCollapseClick)}
> >
<Icon icon="settings" /> <Icon icon="settings" />
<span className="d-inline ml-1 d-md-none ml-md-0">
{i18n.t("admin_settings")}
</span>
</NavLink> </NavLink>
</li> </li>
)} )}
@ -259,7 +268,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
<> <>
<li id="navMessages" className="nav-item"> <li id="navMessages" className="nav-item">
<NavLink <NavLink
className="nav-link" className="nav-link d-inline-flex align-items-center d-md-inline-block"
to="/inbox" to="/inbox"
title={i18n.t("unread_messages", { title={i18n.t("unread_messages", {
count: Number(this.unreadInboxCount), count: Number(this.unreadInboxCount),
@ -268,6 +277,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
onMouseUp={linkEvent(this, handleCollapseClick)} onMouseUp={linkEvent(this, handleCollapseClick)}
> >
<Icon icon="bell" /> <Icon icon="bell" />
<span className="badge badge-light d-inline ml-1 d-md-none ml-md-0">
{i18n.t("unread_messages", {
count: Number(this.unreadInboxCount),
formattedCount: numToSI(this.unreadInboxCount),
})}
</span>
{this.unreadInboxCount > 0 && ( {this.unreadInboxCount > 0 && (
<span className="mx-1 badge badge-light"> <span className="mx-1 badge badge-light">
{numToSI(this.unreadInboxCount)} {numToSI(this.unreadInboxCount)}
@ -278,7 +293,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
{this.moderatesSomething && ( {this.moderatesSomething && (
<li id="navModeration" className="nav-item"> <li id="navModeration" className="nav-item">
<NavLink <NavLink
className="nav-link" className="nav-link d-inline-flex align-items-center d-md-inline-block"
to="/reports" to="/reports"
title={i18n.t("unread_reports", { title={i18n.t("unread_reports", {
count: Number(this.unreadReportCount), count: Number(this.unreadReportCount),
@ -287,6 +302,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
onMouseUp={linkEvent(this, handleCollapseClick)} onMouseUp={linkEvent(this, handleCollapseClick)}
> >
<Icon icon="shield" /> <Icon icon="shield" />
<span className="badge badge-light d-inline ml-1 d-md-none ml-md-0">
{i18n.t("unread_reports", {
count: Number(this.unreadReportCount),
formattedCount: numToSI(this.unreadReportCount),
})}
</span>
{this.unreadReportCount > 0 && ( {this.unreadReportCount > 0 && (
<span className="mx-1 badge badge-light"> <span className="mx-1 badge badge-light">
{numToSI(this.unreadReportCount)} {numToSI(this.unreadReportCount)}
@ -299,7 +320,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
<li id="navApplications" className="nav-item"> <li id="navApplications" className="nav-item">
<NavLink <NavLink
to="/registration_applications" to="/registration_applications"
className="nav-link" className="nav-link d-inline-flex align-items-center d-md-inline-block"
title={i18n.t("unread_registration_applications", { title={i18n.t("unread_registration_applications", {
count: Number(this.unreadApplicationCount), count: Number(this.unreadApplicationCount),
formattedCount: numToSI(this.unreadApplicationCount), formattedCount: numToSI(this.unreadApplicationCount),
@ -307,6 +328,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
onMouseUp={linkEvent(this, handleCollapseClick)} onMouseUp={linkEvent(this, handleCollapseClick)}
> >
<Icon icon="clipboard" /> <Icon icon="clipboard" />
<span className="badge badge-light d-inline ml-1 d-md-none ml-md-0">
{i18n.t("unread_registration_applications", {
count: Number(this.unreadApplicationCount),
formattedCount: numToSI(this.unreadApplicationCount),
})}
</span>
{this.unreadApplicationCount > 0 && ( {this.unreadApplicationCount > 0 && (
<span className="mx-1 badge badge-light"> <span className="mx-1 badge badge-light">
{numToSI(this.unreadApplicationCount)} {numToSI(this.unreadApplicationCount)}

View file

@ -0,0 +1,128 @@
import { Link } from "inferno-router";
import {
CommunityAggregates,
CommunityId,
SiteAggregates,
} from "lemmy-js-client";
import { i18n } from "../../i18next";
import { numToSI } from "../../utils";
interface BadgesProps {
counts: CommunityAggregates | SiteAggregates;
communityId?: CommunityId;
}
const isCommunityAggregates = (
counts: CommunityAggregates | SiteAggregates
): counts is CommunityAggregates => {
return "subscribers" in counts;
};
const isSiteAggregates = (
counts: CommunityAggregates | SiteAggregates
): counts is SiteAggregates => {
return "communities" in counts;
};
export const Badges = ({ counts, communityId }: BadgesProps) => {
return (
<ul className="my-1 list-inline">
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={i18n.t("active_users_in_the_last_day", {
count: Number(counts.users_active_day),
formattedCount: numToSI(counts.users_active_day),
})}
>
{i18n.t("number_of_users", {
count: Number(counts.users_active_day),
formattedCount: numToSI(counts.users_active_day),
})}{" "}
/ {i18n.t("day")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={i18n.t("active_users_in_the_last_week", {
count: Number(counts.users_active_week),
formattedCount: numToSI(counts.users_active_week),
})}
>
{i18n.t("number_of_users", {
count: Number(counts.users_active_week),
formattedCount: numToSI(counts.users_active_week),
})}{" "}
/ {i18n.t("week")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={i18n.t("active_users_in_the_last_month", {
count: Number(counts.users_active_month),
formattedCount: numToSI(counts.users_active_month),
})}
>
{i18n.t("number_of_users", {
count: Number(counts.users_active_month),
formattedCount: numToSI(counts.users_active_month),
})}{" "}
/ {i18n.t("month")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={i18n.t("active_users_in_the_last_six_months", {
count: Number(counts.users_active_half_year),
formattedCount: numToSI(counts.users_active_half_year),
})}
>
{i18n.t("number_of_users", {
count: Number(counts.users_active_half_year),
formattedCount: numToSI(counts.users_active_half_year),
})}{" "}
/ {i18n.t("number_of_months", { count: 6, formattedCount: 6 })}
</li>
{isSiteAggregates(counts) && (
<>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_users", {
count: Number(counts.users),
formattedCount: numToSI(counts.users),
})}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_communities", {
count: Number(counts.communities),
formattedCount: numToSI(counts.communities),
})}
</li>
</>
)}
{isCommunityAggregates(counts) && (
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_subscribers", {
count: Number(counts.subscribers),
formattedCount: numToSI(counts.subscribers),
})}
</li>
)}
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_posts", {
count: Number(counts.posts),
formattedCount: numToSI(counts.posts),
})}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_comments", {
count: Number(counts.comments),
formattedCount: numToSI(counts.comments),
})}
</li>
<li className="list-inline-item">
<Link
className="badge badge-primary"
to={`/modlog${communityId ? `/${communityId}` : ""}`}
>
{i18n.t("modlog")}
</Link>
</li>
</ul>
);
};

View file

@ -100,12 +100,9 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
return ( return (
<select <select
className={classNames( className={classNames("lang-select-action", {
"lang-select-action", "form-control custom-select": !this.props.iconVersion,
this.props.iconVersion })}
? "btn btn-sm text-muted"
: "form-control custom-select"
)}
id={this.id} id={this.id}
onChange={linkEvent(this, this.handleLanguageChange)} onChange={linkEvent(this, this.handleLanguageChange)}
aria-label={i18n.t("language_select_placeholder")} aria-label={i18n.t("language_select_placeholder")}

View file

@ -1,4 +1,5 @@
import autosize from "autosize"; import autosize from "autosize";
import classNames from "classnames";
import { NoOptionI18nKeys } from "i18next"; import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Language } from "lemmy-js-client"; import { Language } from "lemmy-js-client";
@ -143,101 +144,116 @@ export class MarkdownTextArea extends Component<
} }
/> />
<div className="form-group row"> <div className="form-group row">
<div className={`col-sm-12`}> <div className="col-12">
<textarea <div className="rounded bg-light border border-light">
id={this.id} <div className="d-flex flex-wrap border-bottom border-light">
className={`form-control ${this.state.previewMode && "d-none"}`} {this.getFormatButton("bold", this.handleInsertBold)}
value={this.state.content} {this.getFormatButton("italic", this.handleInsertItalic)}
onInput={linkEvent(this, this.handleContentChange)} {this.getFormatButton("link", this.handleInsertLink)}
onPaste={linkEvent(this, this.handleImageUploadPaste)} <EmojiPicker
onKeyDown={linkEvent(this, this.handleKeyBinds)} onEmojiClick={e => this.handleEmoji(this, e)}
required disabled={this.isDisabled}
disabled={this.isDisabled} ></EmojiPicker>
rows={2} <form className="btn btn-sm text-muted font-weight-bold">
maxLength={this.props.maxLength ?? markdownFieldCharacterLimit} <label
placeholder={this.props.placeholder} htmlFor={`file-upload-${this.id}`}
/> className={`mb-0 ${
{this.state.previewMode && this.state.content && ( UserService.Instance.myUserInfo && "pointer"
<div }`}
className="card border-secondary card-body md-div" data-tippy-content={i18n.t("upload_image")}
dangerouslySetInnerHTML={mdToHtml(this.state.content)} >
/> {this.state.imageUploadStatus ? (
)} <Spinner />
{this.state.imageUploadStatus && ) : (
this.state.imageUploadStatus.total > 1 && ( <Icon icon="image" classes="icon-inline" />
<ProgressBar )}
className="mt-2" </label>
striped <input
animated id={`file-upload-${this.id}`}
value={this.state.imageUploadStatus.uploaded} type="file"
max={this.state.imageUploadStatus.total} accept="image/*,video/*"
text={i18n.t("pictures_uploded_progess", { name="file"
uploaded: this.state.imageUploadStatus.uploaded, className="d-none"
total: this.state.imageUploadStatus.total, multiple
})} disabled={
/> !UserService.Instance.myUserInfo || this.isDisabled
)} }
</div> onChange={linkEvent(this, this.handleImageUpload)}
<label className="sr-only" htmlFor={this.id}> />
{i18n.t("body")} </form>
</label> {this.getFormatButton("header", this.handleInsertHeader)}
</div> {this.getFormatButton(
<div className="row"> "strikethrough",
<div className="col-sm-12 d-flex flex-wrap"> this.handleInsertStrikethrough
{this.getFormatButton("bold", this.handleInsertBold)}
{this.getFormatButton("italic", this.handleInsertItalic)}
{this.getFormatButton("link", this.handleInsertLink)}
<EmojiPicker
onEmojiClick={e => this.handleEmoji(this, e)}
disabled={this.isDisabled}
></EmojiPicker>
<form className="btn btn-sm text-muted font-weight-bold">
<label
htmlFor={`file-upload-${this.id}`}
className={`mb-0 ${
UserService.Instance.myUserInfo && "pointer"
}`}
data-tippy-content={i18n.t("upload_image")}
>
{this.state.imageUploadStatus ? (
<Spinner />
) : (
<Icon icon="image" classes="icon-inline" />
)} )}
{this.getFormatButton("quote", this.handleInsertQuote)}
{this.getFormatButton("list", this.handleInsertList)}
{this.getFormatButton("code", this.handleInsertCode)}
{this.getFormatButton("subscript", this.handleInsertSubscript)}
{this.getFormatButton(
"superscript",
this.handleInsertSuperscript
)}
{this.getFormatButton("spoiler", this.handleInsertSpoiler)}
<a
href={markdownHelpUrl}
className="btn btn-sm text-muted font-weight-bold"
title={i18n.t("formatting_help")}
rel={relTags}
>
<Icon icon="help-circle" classes="icon-inline" />
</a>
</div>
<div>
<textarea
id={this.id}
className={classNames(
"form-control border-0 rounded-top-0 rounded-bottom",
{
"d-none": this.state.previewMode,
}
)}
value={this.state.content}
onInput={linkEvent(this, this.handleContentChange)}
onPaste={linkEvent(this, this.handleImageUploadPaste)}
onKeyDown={linkEvent(this, this.handleKeyBinds)}
required
disabled={this.isDisabled}
rows={2}
maxLength={
this.props.maxLength ?? markdownFieldCharacterLimit
}
placeholder={this.props.placeholder}
/>
{this.state.previewMode && this.state.content && (
<div
className="card border-secondary card-body md-div"
dangerouslySetInnerHTML={mdToHtml(this.state.content)}
/>
)}
{this.state.imageUploadStatus &&
this.state.imageUploadStatus.total > 1 && (
<ProgressBar
className="mt-2"
striped
animated
value={this.state.imageUploadStatus.uploaded}
max={this.state.imageUploadStatus.total}
text={i18n.t("pictures_uploded_progess", {
uploaded: this.state.imageUploadStatus.uploaded,
total: this.state.imageUploadStatus.total,
})}
/>
)}
</div>
<label className="sr-only" htmlFor={this.id}>
{i18n.t("body")}
</label> </label>
<input </div>
id={`file-upload-${this.id}`}
type="file"
accept="image/*,video/*"
name="file"
className="d-none"
multiple
disabled={!UserService.Instance.myUserInfo || this.isDisabled}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form>
{this.getFormatButton("header", this.handleInsertHeader)}
{this.getFormatButton(
"strikethrough",
this.handleInsertStrikethrough
)}
{this.getFormatButton("quote", this.handleInsertQuote)}
{this.getFormatButton("list", this.handleInsertList)}
{this.getFormatButton("code", this.handleInsertCode)}
{this.getFormatButton("subscript", this.handleInsertSubscript)}
{this.getFormatButton("superscript", this.handleInsertSuperscript)}
{this.getFormatButton("spoiler", this.handleInsertSpoiler)}
<a
href={markdownHelpUrl}
className="btn btn-sm text-muted font-weight-bold"
title={i18n.t("formatting_help")}
rel={relTags}
>
<Icon icon="help-circle" classes="icon-inline" />
</a>
</div> </div>
<div className="col-sm-12 d-flex align-items-center flex-wrap"> <div className="col-12 d-flex align-items-center flex-wrap mt-2">
{this.props.showLanguage && ( {this.props.showLanguage && (
<LanguageSelect <LanguageSelect
iconVersion iconVersion
@ -257,7 +273,7 @@ export class MarkdownTextArea extends Component<
{this.props.buttonTitle && ( {this.props.buttonTitle && (
<button <button
type="submit" type="submit"
className="btn btn-sm btn-secondary mr-2" className="btn btn-sm btn-secondary ml-2"
disabled={this.isDisabled} disabled={this.isDisabled}
> >
{this.state.loading ? ( {this.state.loading ? (
@ -270,7 +286,7 @@ export class MarkdownTextArea extends Component<
{this.props.replyType && ( {this.props.replyType && (
<button <button
type="button" type="button"
className="btn btn-sm btn-secondary mr-2" className="btn btn-sm btn-secondary ml-2"
onClick={linkEvent(this, this.handleReplyCancel)} onClick={linkEvent(this, this.handleReplyCancel)}
> >
{i18n.t("cancel")} {i18n.t("cancel")}
@ -278,7 +294,7 @@ export class MarkdownTextArea extends Component<
)} )}
{this.state.content && ( {this.state.content && (
<button <button
className={`btn btn-sm btn-secondary mr-2 ${ className={`btn btn-sm btn-secondary ml-2 ${
this.state.previewMode && "active" this.state.previewMode && "active"
}`} }`}
onClick={linkEvent(this, this.handlePreviewToggle)} onClick={linkEvent(this, this.handlePreviewToggle)}

View file

@ -22,7 +22,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
render() { render() {
return ( return (
<picture> <picture className="d-inline-block overflow-hidden">
<source srcSet={this.src("webp")} type="image/webp" /> <source srcSet={this.src("webp")} type="image/webp" />
<source srcSet={this.props.src} /> <source srcSet={this.props.src} />
<source srcSet={this.src("jpg")} type="image/jpeg" /> <source srcSet={this.src("jpg")} type="image/jpeg" />

View file

@ -30,6 +30,10 @@ import { CommunityLink } from "./community-link";
const communityLimit = 50; const communityLimit = 50;
type CommunitiesData = RouteDataResponse<{
listCommunitiesResponse: ListCommunitiesResponse;
}>;
interface CommunitiesState { interface CommunitiesState {
listCommunitiesResponse: RequestState<ListCommunitiesResponse>; listCommunitiesResponse: RequestState<ListCommunitiesResponse>;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
@ -47,7 +51,7 @@ function getListingTypeFromQuery(listingType?: string): ListingType {
} }
export class Communities extends Component<any, CommunitiesState> { export class Communities extends Component<any, CommunitiesState> {
private isoData = setIsoData(this.context); private isoData = setIsoData<CommunitiesData>(this.context);
state: CommunitiesState = { state: CommunitiesState = {
listCommunitiesResponse: { state: "empty" }, listCommunitiesResponse: { state: "empty" },
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
@ -62,9 +66,11 @@ export class Communities extends Component<any, CommunitiesState> {
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const { listCommunitiesResponse } = this.isoData.routeData;
this.state = { this.state = {
...this.state, ...this.state,
listCommunitiesResponse: this.isoData.routeData[0], listCommunitiesResponse,
isIsomorphic: true, isIsomorphic: true,
}; };
} }
@ -274,13 +280,13 @@ export class Communities extends Component<any, CommunitiesState> {
i.context.router.history.push(`/search?q=${searchParamEncoded}`); i.context.router.history.push(`/search?q=${searchParamEncoded}`);
} }
static fetchInitialData({ static async fetchInitialData({
query: { listingType, page }, query: { listingType, page },
client, client,
auth, auth,
}: InitialFetchRequest<QueryParams<CommunitiesProps>>): Promise< }: InitialFetchRequest<
RequestState<any> QueryParams<CommunitiesProps>
>[] { >): Promise<CommunitiesData> {
const listCommunitiesForm: ListCommunities = { const listCommunitiesForm: ListCommunities = {
type_: getListingTypeFromQuery(listingType), type_: getListingTypeFromQuery(listingType),
sort: "TopMonth", sort: "TopMonth",
@ -289,7 +295,11 @@ export class Communities extends Component<any, CommunitiesState> {
auth: auth, auth: auth,
}; };
return [client.listCommunities(listCommunitiesForm)]; return {
listCommunitiesResponse: await client.listCommunities(
listCommunitiesForm
),
};
} }
getCommunitiesQueryParams() { getCommunitiesQueryParams() {

View file

@ -99,6 +99,13 @@ import { Sidebar } from "../community/sidebar";
import { SiteSidebar } from "../home/site-sidebar"; import { SiteSidebar } from "../home/site-sidebar";
import { PostListings } from "../post/post-listings"; import { PostListings } from "../post/post-listings";
import { CommunityLink } from "./community-link"; import { CommunityLink } from "./community-link";
type CommunityData = RouteDataResponse<{
communityRes: GetCommunityResponse;
postsRes: GetPostsResponse;
commentsRes: GetCommentsResponse;
}>;
interface State { interface State {
communityRes: RequestState<GetCommunityResponse>; communityRes: RequestState<GetCommunityResponse>;
postsRes: RequestState<GetPostsResponse>; postsRes: RequestState<GetPostsResponse>;
@ -139,7 +146,7 @@ export class Community extends Component<
RouteComponentProps<{ name: string }>, RouteComponentProps<{ name: string }>,
State State
> { > {
private isoData = setIsoData(this.context); private isoData = setIsoData<CommunityData>(this.context);
state: State = { state: State = {
communityRes: { state: "empty" }, communityRes: { state: "empty" },
postsRes: { state: "empty" }, postsRes: { state: "empty" },
@ -193,13 +200,14 @@ export class Community extends Component<
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const [communityRes, postsRes, commentsRes] = this.isoData.routeData; const { communityRes, commentsRes, postsRes } = this.isoData.routeData;
this.state = { this.state = {
...this.state, ...this.state,
isIsomorphic: true,
commentsRes,
communityRes, communityRes,
postsRes, postsRes,
commentsRes,
isIsomorphic: true,
}; };
} }
} }
@ -226,23 +234,21 @@ export class Community extends Component<
saveScrollPosition(this.context); saveScrollPosition(this.context);
} }
static fetchInitialData({ static async fetchInitialData({
client, client,
path, path,
query: { dataType: urlDataType, page: urlPage, sort: urlSort }, query: { dataType: urlDataType, page: urlPage, sort: urlSort },
auth, auth,
}: InitialFetchRequest<QueryParams<CommunityProps>>): Promise< }: InitialFetchRequest<QueryParams<CommunityProps>>): Promise<
RequestState<any> Promise<CommunityData>
>[] { > {
const pathSplit = path.split("/"); const pathSplit = path.split("/");
const promises: Promise<RequestState<any>>[] = [];
const communityName = pathSplit[2]; const communityName = pathSplit[2];
const communityForm: GetCommunity = { const communityForm: GetCommunity = {
name: communityName, name: communityName,
auth, auth,
}; };
promises.push(client.getCommunity(communityForm));
const dataType = getDataTypeFromQuery(urlDataType); const dataType = getDataTypeFromQuery(urlDataType);
@ -250,6 +256,11 @@ export class Community extends Component<
const page = getPageFromString(urlPage); const page = getPageFromString(urlPage);
let postsResponse: RequestState<GetPostsResponse> = { state: "empty" };
let commentsResponse: RequestState<GetCommentsResponse> = {
state: "empty",
};
if (dataType === DataType.Post) { if (dataType === DataType.Post) {
const getPostsForm: GetPosts = { const getPostsForm: GetPosts = {
community_name: communityName, community_name: communityName,
@ -260,8 +271,8 @@ export class Community extends Component<
saved_only: false, saved_only: false,
auth, auth,
}; };
promises.push(client.getPosts(getPostsForm));
promises.push(Promise.resolve({ state: "empty" })); postsResponse = await client.getPosts(getPostsForm);
} else { } else {
const getCommentsForm: GetComments = { const getCommentsForm: GetComments = {
community_name: communityName, community_name: communityName,
@ -272,11 +283,15 @@ export class Community extends Component<
saved_only: false, saved_only: false,
auth, auth,
}; };
promises.push(Promise.resolve({ state: "empty" }));
promises.push(client.getComments(getCommentsForm)); commentsResponse = await client.getComments(getCommentsForm);
} }
return promises; return {
communityRes: await client.getCommunity(communityForm),
commentsRes: commentsResponse,
postsRes: postsResponse,
};
} }
get documentTitle(): string { get documentTitle(): string {

View file

@ -21,11 +21,11 @@ import {
hostname, hostname,
mdToHtml, mdToHtml,
myAuthRequired, myAuthRequired,
numToSI,
} from "../../utils"; } from "../../utils";
import { amAdmin } from "../../utils/roles/am-admin"; import { amAdmin } from "../../utils/roles/am-admin";
import { amMod } from "../../utils/roles/am-mod"; import { amMod } from "../../utils/roles/am-mod";
import { amTopMod } from "../../utils/roles/am-top-mod"; import { amTopMod } from "../../utils/roles/am-top-mod";
import { Badges } from "../common/badges";
import { BannerIconHeader } from "../common/banner-icon-header"; import { BannerIconHeader } from "../common/banner-icon-header";
import { Icon, PurgeWarning, Spinner } from "../common/icon"; import { Icon, PurgeWarning, Spinner } from "../common/icon";
import { CommunityForm } from "../community/community-form"; import { CommunityForm } from "../community/community-form";
@ -158,7 +158,10 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<section id="sidebarInfo" className="card border-secondary mb-3"> <section id="sidebarInfo" className="card border-secondary mb-3">
<div className="card-body"> <div className="card-body">
{this.description()} {this.description()}
{this.badges()} <Badges
communityId={this.props.community_view.community.id}
counts={this.props.community_view.counts}
/>
{this.mods()} {this.mods()}
</div> </div>
</section> </section>
@ -233,93 +236,6 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
); );
} }
badges() {
const community_view = this.props.community_view;
const counts = community_view.counts;
return (
<ul className="my-1 list-inline">
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={i18n.t("active_users_in_the_last_day", {
count: Number(counts.users_active_day),
formattedCount: numToSI(counts.users_active_day),
})}
>
{i18n.t("number_of_users", {
count: Number(counts.users_active_day),
formattedCount: numToSI(counts.users_active_day),
})}{" "}
/ {i18n.t("day")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={i18n.t("active_users_in_the_last_week", {
count: Number(counts.users_active_week),
formattedCount: numToSI(counts.users_active_week),
})}
>
{i18n.t("number_of_users", {
count: Number(counts.users_active_week),
formattedCount: numToSI(counts.users_active_week),
})}{" "}
/ {i18n.t("week")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={i18n.t("active_users_in_the_last_month", {
count: Number(counts.users_active_month),
formattedCount: numToSI(counts.users_active_month),
})}
>
{i18n.t("number_of_users", {
count: Number(counts.users_active_month),
formattedCount: numToSI(counts.users_active_month),
})}{" "}
/ {i18n.t("month")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={i18n.t("active_users_in_the_last_six_months", {
count: Number(counts.users_active_half_year),
formattedCount: numToSI(counts.users_active_half_year),
})}
>
{i18n.t("number_of_users", {
count: Number(counts.users_active_half_year),
formattedCount: numToSI(counts.users_active_half_year),
})}{" "}
/ {i18n.t("number_of_months", { count: 6, formattedCount: 6 })}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_subscribers", {
count: Number(counts.subscribers),
formattedCount: numToSI(counts.subscribers),
})}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_posts", {
count: Number(counts.posts),
formattedCount: numToSI(counts.posts),
})}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_comments", {
count: Number(counts.comments),
formattedCount: numToSI(counts.comments),
})}
</li>
<li className="list-inline-item">
<Link
className="badge badge-primary"
to={`/modlog/${this.props.community_view.community.id}`}
>
{i18n.t("modlog")}
</Link>
</li>
</ul>
);
}
mods() { mods() {
return ( return (
<ul className="list-inline small"> <ul className="list-inline small">

View file

@ -14,6 +14,7 @@ import { InitialFetchRequest } from "../../interfaces";
import { FirstLoadService } from "../../services/FirstLoadService"; import { FirstLoadService } from "../../services/FirstLoadService";
import { HttpService, RequestState } from "../../services/HttpService"; import { HttpService, RequestState } from "../../services/HttpService";
import { import {
RouteDataResponse,
capitalizeFirstLetter, capitalizeFirstLetter,
fetchThemeList, fetchThemeList,
myAuthRequired, myAuthRequired,
@ -32,6 +33,11 @@ import RateLimitForm from "./rate-limit-form";
import { SiteForm } from "./site-form"; import { SiteForm } from "./site-form";
import { TaglineForm } from "./tagline-form"; import { TaglineForm } from "./tagline-form";
type AdminSettingsData = RouteDataResponse<{
bannedRes: BannedPersonsResponse;
instancesRes: GetFederatedInstancesResponse;
}>;
interface AdminSettingsState { interface AdminSettingsState {
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
banned: PersonView[]; banned: PersonView[];
@ -46,7 +52,7 @@ interface AdminSettingsState {
} }
export class AdminSettings extends Component<any, AdminSettingsState> { export class AdminSettings extends Component<any, AdminSettingsState> {
private isoData = setIsoData(this.context); private isoData = setIsoData<AdminSettingsData>(this.context);
state: AdminSettingsState = { state: AdminSettingsState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
banned: [], banned: [],
@ -70,7 +76,8 @@ 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 (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const [bannedRes, instancesRes] = this.isoData.routeData; const { bannedRes, instancesRes } = this.isoData.routeData;
this.state = { this.state = {
...this.state, ...this.state,
bannedRes, bannedRes,
@ -80,47 +87,18 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
} }
} }
async fetchData() { static async fetchInitialData({
this.setState({
bannedRes: { state: "loading" },
instancesRes: { state: "loading" },
themeList: [],
loading: true,
});
const auth = myAuthRequired();
const [bannedRes, instancesRes, themeList] = await Promise.all([
HttpService.client.getBannedPersons({ auth }),
HttpService.client.getFederatedInstances({ auth }),
fetchThemeList(),
]);
this.setState({
bannedRes,
instancesRes,
themeList,
loading: false,
});
}
static fetchInitialData({
auth, auth,
client, client,
}: InitialFetchRequest): Promise<any>[] { }: InitialFetchRequest): Promise<AdminSettingsData> {
const promises: Promise<RequestState<any>>[] = []; return {
bannedRes: await client.getBannedPersons({
if (auth) { auth: auth as string,
promises.push(client.getBannedPersons({ auth })); }),
promises.push(client.getFederatedInstances({ auth })); instancesRes: await client.getFederatedInstances({
} else { auth: auth as string,
promises.push( }),
Promise.resolve({ state: "empty" }), };
Promise.resolve({ state: "empty" })
);
}
return promises;
} }
async componentDidMount() { async componentDidMount() {
@ -218,6 +196,28 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
); );
} }
async fetchData() {
this.setState({
bannedRes: { state: "loading" },
instancesRes: { state: "loading" },
themeList: [],
});
const auth = myAuthRequired();
const [bannedRes, instancesRes, themeList] = await Promise.all([
HttpService.client.getBannedPersons({ auth }),
HttpService.client.getFederatedInstances({ auth }),
fetchThemeList(),
]);
this.setState({
bannedRes,
instancesRes,
themeList,
});
}
admins() { admins() {
return ( return (
<> <>

View file

@ -73,6 +73,7 @@ import {
postToCommentSortType, postToCommentSortType,
relTags, relTags,
restoreScrollPosition, restoreScrollPosition,
RouteDataResponse,
saveScrollPosition, saveScrollPosition,
setIsoData, setIsoData,
setupTippy, setupTippy,
@ -117,6 +118,45 @@ interface HomeProps {
page: number; page: number;
} }
type HomeData = RouteDataResponse<{
postsRes: GetPostsResponse;
commentsRes: GetCommentsResponse;
trendingCommunitiesRes: ListCommunitiesResponse;
}>;
function getRss(listingType: ListingType) {
const { sort } = getHomeQueryParams();
const auth = myAuth();
let rss: string | undefined = undefined;
switch (listingType) {
case "All": {
rss = `/feeds/all.xml?sort=${sort}`;
break;
}
case "Local": {
rss = `/feeds/local.xml?sort=${sort}`;
break;
}
case "Subscribed": {
rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
break;
}
}
return (
rss && (
<>
<a href={rss} rel={relTags} title="RSS">
<Icon icon="rss" classes="text-muted small" />
</a>
<link rel="alternate" type="application/atom+xml" href={rss} />
</>
)
);
}
function getDataTypeFromQuery(type?: string): DataType { function getDataTypeFromQuery(type?: string): DataType {
return type ? DataType[type] : DataType.Post; return type ? DataType[type] : DataType.Post;
} }
@ -176,7 +216,7 @@ const LinkButton = ({
); );
export class Home extends Component<any, HomeState> { export class Home extends Component<any, HomeState> {
private isoData = setIsoData(this.context); private isoData = setIsoData<HomeData>(this.context);
state: HomeState = { state: HomeState = {
postsRes: { state: "empty" }, postsRes: { state: "empty" },
commentsRes: { state: "empty" }, commentsRes: { state: "empty" },
@ -228,14 +268,14 @@ 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 (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const [postsRes, commentsRes, trendingCommunitiesRes] = const { trendingCommunitiesRes, commentsRes, postsRes } =
this.isoData.routeData; this.isoData.routeData;
this.state = { this.state = {
...this.state, ...this.state,
postsRes,
commentsRes,
trendingCommunitiesRes, trendingCommunitiesRes,
commentsRes,
postsRes,
tagline: getRandomFromList(this.state?.siteRes?.taglines ?? []) tagline: getRandomFromList(this.state?.siteRes?.taglines ?? [])
?.content, ?.content,
isIsomorphic: true, isIsomorphic: true,
@ -244,7 +284,12 @@ export class Home extends Component<any, HomeState> {
} }
async componentDidMount() { async componentDidMount() {
if (!this.state.isIsomorphic || !this.isoData.routeData.length) { if (
!this.state.isIsomorphic ||
!Object.values(this.isoData.routeData).some(
res => res.state === "success" || res.state === "failed"
)
) {
await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]); await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]);
} }
@ -255,13 +300,11 @@ export class Home extends Component<any, HomeState> {
saveScrollPosition(this.context); saveScrollPosition(this.context);
} }
static fetchInitialData({ static async fetchInitialData({
client, client,
auth, auth,
query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort }, query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
}: InitialFetchRequest<QueryParams<HomeProps>>): Promise< }: InitialFetchRequest<QueryParams<HomeProps>>): Promise<HomeData> {
RequestState<any>
>[] {
const dataType = getDataTypeFromQuery(urlDataType); const dataType = getDataTypeFromQuery(urlDataType);
// TODO figure out auth default_listingType, default_sort_type // TODO figure out auth default_listingType, default_sort_type
@ -270,7 +313,10 @@ export class Home extends Component<any, HomeState> {
const page = urlPage ? Number(urlPage) : 1; const page = urlPage ? Number(urlPage) : 1;
const promises: Promise<RequestState<any>>[] = []; let postsRes: RequestState<GetPostsResponse> = { state: "empty" };
let commentsRes: RequestState<GetCommentsResponse> = {
state: "empty",
};
if (dataType === DataType.Post) { if (dataType === DataType.Post) {
const getPostsForm: GetPosts = { const getPostsForm: GetPosts = {
@ -282,8 +328,7 @@ export class Home extends Component<any, HomeState> {
auth, auth,
}; };
promises.push(client.getPosts(getPostsForm)); postsRes = await client.getPosts(getPostsForm);
promises.push(Promise.resolve({ state: "empty" }));
} else { } else {
const getCommentsForm: GetComments = { const getCommentsForm: GetComments = {
page, page,
@ -293,8 +338,8 @@ export class Home extends Component<any, HomeState> {
saved_only: false, saved_only: false,
auth, auth,
}; };
promises.push(Promise.resolve({ state: "empty" }));
promises.push(client.getComments(getCommentsForm)); commentsRes = await client.getComments(getCommentsForm);
} }
const trendingCommunitiesForm: ListCommunities = { const trendingCommunitiesForm: ListCommunities = {
@ -303,9 +348,14 @@ export class Home extends Component<any, HomeState> {
limit: trendingFetchLimit, limit: trendingFetchLimit,
auth, auth,
}; };
promises.push(client.listCommunities(trendingCommunitiesForm));
return promises; return {
trendingCommunitiesRes: await client.listCommunities(
trendingCommunitiesForm
),
commentsRes,
postsRes,
};
} }
get documentTitle(): string { get documentTitle(): string {
@ -340,7 +390,7 @@ export class Home extends Component<any, HomeState> {
></div> ></div>
)} )}
<div className="d-block d-md-none">{this.mobileView}</div> <div className="d-block d-md-none">{this.mobileView}</div>
{this.posts()} {this.posts}
</main> </main>
<aside className="d-none d-md-block col-md-4"> <aside className="d-none d-md-block col-md-4">
{this.mySidebar} {this.mySidebar}
@ -552,7 +602,7 @@ export class Home extends Component<any, HomeState> {
await this.fetchData(); await this.fetchData();
} }
posts() { get posts() {
const { page } = getHomeQueryParams(); const { page } = getHomeQueryParams();
return ( return (
@ -571,7 +621,7 @@ export class Home extends Component<any, HomeState> {
const siteRes = this.state.siteRes; const siteRes = this.state.siteRes;
if (dataType === DataType.Post) { if (dataType === DataType.Post) {
switch (this.state.postsRes?.state) { switch (this.state.postsRes.state) {
case "loading": case "loading":
return ( return (
<h5> <h5>
@ -677,44 +727,11 @@ export class Home extends Component<any, HomeState> {
<span className="mr-2"> <span className="mr-2">
<SortSelect sort={sort} onChange={this.handleSortChange} /> <SortSelect sort={sort} onChange={this.handleSortChange} />
</span> </span>
{this.getRss(listingType)} {getRss(listingType)}
</div> </div>
); );
} }
getRss(listingType: ListingType) {
const { sort } = getHomeQueryParams();
const auth = myAuth();
let rss: string | undefined = undefined;
switch (listingType) {
case "All": {
rss = `/feeds/all.xml?sort=${sort}`;
break;
}
case "Local": {
rss = `/feeds/local.xml?sort=${sort}`;
break;
}
case "Subscribed": {
rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
break;
}
}
return (
rss && (
<>
<a href={rss} rel={relTags} title="RSS">
<Icon icon="rss" classes="text-muted small" />
</a>
<link rel="alternate" type="application/atom+xml" href={rss} />
</>
)
);
}
async fetchTrendingCommunities() { async fetchTrendingCommunities() {
this.setState({ trendingCommunitiesRes: { state: "loading" } }); this.setState({ trendingCommunitiesRes: { state: "loading" } });
this.setState({ this.setState({

View file

@ -8,10 +8,14 @@ import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces"; import { InitialFetchRequest } from "../../interfaces";
import { FirstLoadService } from "../../services/FirstLoadService"; import { FirstLoadService } from "../../services/FirstLoadService";
import { HttpService, RequestState } from "../../services/HttpService"; import { HttpService, RequestState } from "../../services/HttpService";
import { relTags, setIsoData } from "../../utils"; import { RouteDataResponse, relTags, setIsoData } from "../../utils";
import { HtmlTags } from "../common/html-tags"; import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon"; import { Spinner } from "../common/icon";
type InstancesData = RouteDataResponse<{
federatedInstancesResponse: GetFederatedInstancesResponse;
}>;
interface InstancesState { interface InstancesState {
instancesRes: RequestState<GetFederatedInstancesResponse>; instancesRes: RequestState<GetFederatedInstancesResponse>;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
@ -19,7 +23,7 @@ interface InstancesState {
} }
export class Instances extends Component<any, InstancesState> { export class Instances extends Component<any, InstancesState> {
private isoData = setIsoData(this.context); private isoData = setIsoData<InstancesData>(this.context);
state: InstancesState = { state: InstancesState = {
instancesRes: { state: "empty" }, instancesRes: { state: "empty" },
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
@ -33,7 +37,7 @@ export class Instances extends Component<any, InstancesState> {
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
this.state = { this.state = {
...this.state, ...this.state,
instancesRes: this.isoData.routeData[0], instancesRes: this.isoData.routeData.federatedInstancesResponse,
isIsomorphic: true, isIsomorphic: true,
}; };
} }
@ -55,10 +59,12 @@ export class Instances extends Component<any, InstancesState> {
}); });
} }
static fetchInitialData( static async fetchInitialData({
req: InitialFetchRequest client,
): Promise<RequestState<any>>[] { }: InitialFetchRequest): Promise<InstancesData> {
return [req.client.getFederatedInstances({})]; return {
federatedInstancesResponse: await client.getFederatedInstances({}),
};
} }
get documentTitle(): string { get documentTitle(): string {

View file

@ -1,8 +1,8 @@
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { Link } from "inferno-router";
import { PersonView, Site, SiteAggregates } from "lemmy-js-client"; import { PersonView, Site, SiteAggregates } from "lemmy-js-client";
import { i18n } from "../../i18next"; import { i18n } from "../../i18next";
import { mdToHtml, numToSI } from "../../utils"; import { mdToHtml } from "../../utils";
import { Badges } from "../common/badges";
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";
@ -71,7 +71,7 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
<div> <div>
{site.description && <h6>{site.description}</h6>} {site.description && <h6>{site.description}</h6>}
{site.sidebar && this.siteSidebar(site.sidebar)} {site.sidebar && this.siteSidebar(site.sidebar)}
{this.props.counts && this.badges(this.props.counts)} {this.props.counts && <Badges counts={this.props.counts} />}
{this.props.admins && this.admins(this.props.admins)} {this.props.admins && this.admins(this.props.admins)}
</div> </div>
); );
@ -96,95 +96,6 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
); );
} }
badges(siteAggregates: SiteAggregates) {
const counts = siteAggregates;
return (
<ul className="my-2 list-inline">
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={i18n.t("active_users_in_the_last_day", {
count: Number(counts.users_active_day),
formattedCount: numToSI(counts.users_active_day),
})}
>
{i18n.t("number_of_users", {
count: Number(counts.users_active_day),
formattedCount: numToSI(counts.users_active_day),
})}{" "}
/ {i18n.t("day")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={i18n.t("active_users_in_the_last_week", {
count: Number(counts.users_active_week),
formattedCount: numToSI(counts.users_active_week),
})}
>
{i18n.t("number_of_users", {
count: Number(counts.users_active_week),
formattedCount: numToSI(counts.users_active_week),
})}{" "}
/ {i18n.t("week")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={i18n.t("active_users_in_the_last_month", {
count: Number(counts.users_active_month),
formattedCount: numToSI(counts.users_active_month),
})}
>
{i18n.t("number_of_users", {
count: Number(counts.users_active_month),
formattedCount: numToSI(counts.users_active_month),
})}{" "}
/ {i18n.t("month")}
</li>
<li
className="list-inline-item badge badge-secondary pointer"
data-tippy-content={i18n.t("active_users_in_the_last_six_months", {
count: Number(counts.users_active_half_year),
formattedCount: numToSI(counts.users_active_half_year),
})}
>
{i18n.t("number_of_users", {
count: Number(counts.users_active_half_year),
formattedCount: numToSI(counts.users_active_half_year),
})}{" "}
/ {i18n.t("number_of_months", { count: 6, formattedCount: 6 })}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_users", {
count: Number(counts.users),
formattedCount: numToSI(counts.users),
})}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_communities", {
count: Number(counts.communities),
formattedCount: numToSI(counts.communities),
})}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_posts", {
count: Number(counts.posts),
formattedCount: numToSI(counts.posts),
})}
</li>
<li className="list-inline-item badge badge-secondary">
{i18n.t("number_of_comments", {
count: Number(counts.comments),
formattedCount: numToSI(counts.comments),
})}
</li>
<li className="list-inline-item">
<Link className="badge badge-primary" to="/modlog">
{i18n.t("modlog")}
</Link>
</li>
</ul>
);
}
handleCollapseSidebar(i: SiteSidebar) { handleCollapseSidebar(i: SiteSidebar) {
i.setState({ collapsed: !i.state.collapsed }); i.setState({ collapsed: !i.state.collapsed });
} }

View file

@ -13,6 +13,7 @@ import {
GetModlog, GetModlog,
GetModlogResponse, GetModlogResponse,
GetPersonDetails, GetPersonDetails,
GetPersonDetailsResponse,
ModAddCommunityView, ModAddCommunityView,
ModAddView, ModAddView,
ModBanFromCommunityView, ModBanFromCommunityView,
@ -74,6 +75,13 @@ type View =
| AdminPurgePostView | AdminPurgePostView
| AdminPurgeCommentView; | AdminPurgeCommentView;
type ModlogData = RouteDataResponse<{
res: GetModlogResponse;
communityRes: GetCommunityResponse;
modUserResponse: GetPersonDetailsResponse;
userResponse: GetPersonDetailsResponse;
}>;
interface ModlogType { interface ModlogType {
id: number; id: number;
type_: ModlogActionType; type_: ModlogActionType;
@ -631,7 +639,7 @@ export class Modlog extends Component<
RouteComponentProps<{ communityId?: string }>, RouteComponentProps<{ communityId?: string }>,
ModlogState ModlogState
> { > {
private isoData = setIsoData(this.context); private isoData = setIsoData<ModlogData>(this.context);
state: ModlogState = { state: ModlogState = {
res: { state: "empty" }, res: { state: "empty" },
@ -653,25 +661,26 @@ export class Modlog extends Component<
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const [res, communityRes, filteredModRes, filteredUserRes] = const { res, communityRes, modUserResponse, userResponse } =
this.isoData.routeData; this.isoData.routeData;
this.state = { this.state = {
...this.state, ...this.state,
res, res,
communityRes, communityRes,
}; };
if (filteredModRes.state === "success") { if (modUserResponse.state === "success") {
this.state = { this.state = {
...this.state, ...this.state,
modSearchOptions: [personToChoice(filteredModRes.data.person_view)], modSearchOptions: [personToChoice(modUserResponse.data.person_view)],
}; };
} }
if (filteredUserRes.state === "success") { if (userResponse.state === "success") {
this.state = { this.state = {
...this.state, ...this.state,
userSearchOptions: [personToChoice(filteredUserRes.data.person_view)], userSearchOptions: [personToChoice(userResponse.data.person_view)],
}; };
} }
} }
@ -958,17 +967,14 @@ export class Modlog extends Component<
} }
} }
static fetchInitialData({ static async fetchInitialData({
client, client,
path, path,
query: { modId: urlModId, page, userId: urlUserId, actionType }, query: { modId: urlModId, page, userId: urlUserId, actionType },
auth, auth,
site, site,
}: InitialFetchRequest<QueryParams<ModlogProps>>): Promise< }: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> {
RequestState<any>
>[] {
const pathSplit = path.split("/"); const pathSplit = path.split("/");
const promises: Promise<RequestState<any>>[] = [];
const communityId = getIdFromString(pathSplit[2]); const communityId = getIdFromString(pathSplit[2]);
const modId = !site.site_view.local_site.hide_modlog_mod_names const modId = !site.site_view.local_site.hide_modlog_mod_names
? getIdFromString(urlModId) ? getIdFromString(urlModId)
@ -985,40 +991,50 @@ export class Modlog extends Component<
auth, auth,
}; };
promises.push(client.getModlog(modlogForm)); let communityResponse: RequestState<GetCommunityResponse> = {
state: "empty",
};
if (communityId) { if (communityId) {
const communityForm: GetCommunity = { const communityForm: GetCommunity = {
id: communityId, id: communityId,
auth, auth,
}; };
promises.push(client.getCommunity(communityForm));
} else { communityResponse = await client.getCommunity(communityForm);
promises.push(Promise.resolve({ state: "empty" }));
} }
let modUserResponse: RequestState<GetPersonDetailsResponse> = {
state: "empty",
};
if (modId) { if (modId) {
const getPersonForm: GetPersonDetails = { const getPersonForm: GetPersonDetails = {
person_id: modId, person_id: modId,
auth, auth,
}; };
promises.push(client.getPersonDetails(getPersonForm)); modUserResponse = await client.getPersonDetails(getPersonForm);
} else {
promises.push(Promise.resolve({ state: "empty" }));
} }
let userResponse: RequestState<GetPersonDetailsResponse> = {
state: "empty",
};
if (userId) { if (userId) {
const getPersonForm: GetPersonDetails = { const getPersonForm: GetPersonDetails = {
person_id: userId, person_id: userId,
auth, auth,
}; };
promises.push(client.getPersonDetails(getPersonForm)); userResponse = await client.getPersonDetails(getPersonForm);
} else {
promises.push(Promise.resolve({ state: "empty" }));
} }
return promises; return {
res: await client.getModlog(modlogForm),
communityRes: communityResponse,
modUserResponse,
userResponse,
};
} }
} }

View file

@ -24,10 +24,7 @@ import {
DistinguishComment, DistinguishComment,
EditComment, EditComment,
EditPrivateMessage, EditPrivateMessage,
GetPersonMentions,
GetPersonMentionsResponse, GetPersonMentionsResponse,
GetPrivateMessages,
GetReplies,
GetRepliesResponse, GetRepliesResponse,
GetSiteResponse, GetSiteResponse,
MarkCommentReplyAsRead, MarkCommentReplyAsRead,
@ -53,6 +50,7 @@ import { UserService } from "../../services";
import { FirstLoadService } from "../../services/FirstLoadService"; import { FirstLoadService } from "../../services/FirstLoadService";
import { HttpService, RequestState } from "../../services/HttpService"; import { HttpService, RequestState } from "../../services/HttpService";
import { import {
RouteDataResponse,
commentsToFlatNodes, commentsToFlatNodes,
editCommentReply, editCommentReply,
editMention, editMention,
@ -92,6 +90,13 @@ enum ReplyEnum {
Mention, Mention,
Message, Message,
} }
type InboxData = RouteDataResponse<{
repliesRes: GetRepliesResponse;
mentionsRes: GetPersonMentionsResponse;
messagesRes: PrivateMessagesResponse;
}>;
type ReplyType = { type ReplyType = {
id: number; id: number;
type_: ReplyEnum; type_: ReplyEnum;
@ -114,7 +119,7 @@ interface InboxState {
} }
export class Inbox extends Component<any, InboxState> { export class Inbox extends Component<any, InboxState> {
private isoData = setIsoData(this.context); private isoData = setIsoData<InboxData>(this.context);
state: InboxState = { state: InboxState = {
unreadOrAll: UnreadOrAll.Unread, unreadOrAll: UnreadOrAll.Unread,
messageType: MessageType.All, messageType: MessageType.All,
@ -162,7 +167,7 @@ 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 (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const [repliesRes, mentionsRes, messagesRes] = this.isoData.routeData; const { mentionsRes, messagesRes, repliesRes } = this.isoData.routeData;
this.state = { this.state = {
...this.state, ...this.state,
@ -686,50 +691,40 @@ export class Inbox extends Component<any, InboxState> {
await i.refetch(); await i.refetch();
} }
static fetchInitialData({ static async fetchInitialData({
client, client,
auth, auth,
}: InitialFetchRequest): Promise<any>[] { }: InitialFetchRequest): Promise<InboxData> {
const promises: Promise<RequestState<any>>[] = [];
const sort: CommentSortType = "New"; const sort: CommentSortType = "New";
if (auth) { return {
// It can be /u/me, or /username/1 mentionsRes: auth
const repliesForm: GetReplies = { ? await client.getPersonMentions({
sort, sort,
unread_only: true, unread_only: true,
page: 1, page: 1,
limit: fetchLimit, limit: fetchLimit,
auth, auth,
}; })
promises.push(client.getReplies(repliesForm)); : { state: "empty" },
messagesRes: auth
const personMentionsForm: GetPersonMentions = { ? await client.getPrivateMessages({
sort, unread_only: true,
unread_only: true, page: 1,
page: 1, limit: fetchLimit,
limit: fetchLimit, auth,
auth, })
}; : { state: "empty" },
promises.push(client.getPersonMentions(personMentionsForm)); repliesRes: auth
? await client.getReplies({
const privateMessagesForm: GetPrivateMessages = { sort,
unread_only: true, unread_only: true,
page: 1, page: 1,
limit: fetchLimit, limit: fetchLimit,
auth, auth,
}; })
promises.push(client.getPrivateMessages(privateMessagesForm)); : { state: "empty" },
} else { };
promises.push(
Promise.resolve({ state: "empty" }),
Promise.resolve({ state: "empty" }),
Promise.resolve({ state: "empty" })
);
}
return promises;
} }
async refetch() { async refetch() {

View file

@ -107,16 +107,6 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
setupTippy(); setupTippy();
} }
// TODO wut?
// componentDidUpdate(lastProps: UserDetailsProps) {
// for (const key of Object.keys(lastProps)) {
// if (lastProps[key] !== this.props[key]) {
// this.fetchUserData();
// break;
// }
// }
// }
render() { render() {
return ( return (
<div> <div>

View file

@ -73,10 +73,14 @@ export class PersonListing extends Component<PersonListingProps, any> {
const avatar = this.props.person.avatar; const avatar = this.props.person.avatar;
return ( return (
<> <>
{avatar && {!this.props.hideAvatar &&
!this.props.hideAvatar &&
!this.props.person.banned && !this.props.person.banned &&
showAvatars() && <PictrsImage src={avatar} icon />} showAvatars() && (
<PictrsImage
src={avatar ?? "/static/assets/icons/icon-96x96.png"}
icon
/>
)}
<span>{displayName}</span> <span>{displayName}</span>
</> </>
); );

View file

@ -90,6 +90,10 @@ import { CommunityLink } from "../community/community-link";
import { PersonDetails } from "./person-details"; import { PersonDetails } from "./person-details";
import { PersonListing } from "./person-listing"; import { PersonListing } from "./person-listing";
type ProfileData = RouteDataResponse<{
personResponse: GetPersonDetailsResponse;
}>;
interface ProfileState { interface ProfileState {
personRes: RequestState<GetPersonDetailsResponse>; personRes: RequestState<GetPersonDetailsResponse>;
personBlocked: boolean; personBlocked: boolean;
@ -156,7 +160,7 @@ export class Profile extends Component<
RouteComponentProps<{ username: string }>, RouteComponentProps<{ username: string }>,
ProfileState ProfileState
> { > {
private isoData = setIsoData(this.context); private isoData = setIsoData<ProfileData>(this.context);
state: ProfileState = { state: ProfileState = {
personRes: { state: "empty" }, personRes: { state: "empty" },
personBlocked: false, personBlocked: false,
@ -208,7 +212,7 @@ export class Profile extends Component<
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
this.state = { this.state = {
...this.state, ...this.state,
personRes: this.isoData.routeData[0], personRes: this.isoData.routeData.personResponse,
isIsomorphic: true, isIsomorphic: true,
}; };
} }
@ -267,14 +271,12 @@ export class Profile extends Component<
} }
} }
static fetchInitialData({ static async fetchInitialData({
client, client,
path, path,
query: { page, sort, view: urlView }, query: { page, sort, view: urlView },
auth, auth,
}: InitialFetchRequest<QueryParams<ProfileProps>>): Promise< }: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<ProfileData> {
RequestState<any>
>[] {
const pathSplit = path.split("/"); const pathSplit = path.split("/");
const username = pathSplit[2]; const username = pathSplit[2];
@ -289,7 +291,9 @@ export class Profile extends Component<
auth, auth,
}; };
return [client.getPersonDetails(form)]; return {
personResponse: await client.getPersonDetails(form),
};
} }
get documentTitle(): string { get documentTitle(): string {

View file

@ -2,7 +2,6 @@ import { Component, linkEvent } from "inferno";
import { import {
ApproveRegistrationApplication, ApproveRegistrationApplication,
GetSiteResponse, GetSiteResponse,
ListRegistrationApplications,
ListRegistrationApplicationsResponse, ListRegistrationApplicationsResponse,
RegistrationApplicationView, RegistrationApplicationView,
} from "lemmy-js-client"; } from "lemmy-js-client";
@ -12,6 +11,7 @@ import { UserService } from "../../services";
import { FirstLoadService } from "../../services/FirstLoadService"; import { FirstLoadService } from "../../services/FirstLoadService";
import { HttpService, RequestState } from "../../services/HttpService"; import { HttpService, RequestState } from "../../services/HttpService";
import { import {
RouteDataResponse,
editRegistrationApplication, editRegistrationApplication,
fetchLimit, fetchLimit,
myAuthRequired, myAuthRequired,
@ -28,6 +28,10 @@ enum UnreadOrAll {
All, All,
} }
type RegistrationApplicationsData = RouteDataResponse<{
listRegistrationApplicationsResponse: ListRegistrationApplicationsResponse;
}>;
interface RegistrationApplicationsState { interface RegistrationApplicationsState {
appsRes: RequestState<ListRegistrationApplicationsResponse>; appsRes: RequestState<ListRegistrationApplicationsResponse>;
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
@ -40,7 +44,7 @@ export class RegistrationApplications extends Component<
any, any,
RegistrationApplicationsState RegistrationApplicationsState
> { > {
private isoData = setIsoData(this.context); private isoData = setIsoData<RegistrationApplicationsData>(this.context);
state: RegistrationApplicationsState = { state: RegistrationApplicationsState = {
appsRes: { state: "empty" }, appsRes: { state: "empty" },
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
@ -59,7 +63,7 @@ export class RegistrationApplications extends Component<
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
this.state = { this.state = {
...this.state, ...this.state,
appsRes: this.isoData.routeData[0], appsRes: this.isoData.routeData.listRegistrationApplicationsResponse,
isIsomorphic: true, isIsomorphic: true,
}; };
} }
@ -184,25 +188,20 @@ export class RegistrationApplications extends Component<
this.refetch(); this.refetch();
} }
static fetchInitialData({ static async fetchInitialData({
auth, auth,
client, client,
}: InitialFetchRequest): Promise<any>[] { }: InitialFetchRequest): Promise<RegistrationApplicationsData> {
const promises: Promise<RequestState<any>>[] = []; return {
listRegistrationApplicationsResponse: auth
if (auth) { ? await client.listRegistrationApplications({
const form: ListRegistrationApplications = { unread_only: true,
unread_only: true, page: 1,
page: 1, limit: fetchLimit,
limit: fetchLimit, auth: auth as string,
auth, })
}; : { state: "empty" },
promises.push(client.listRegistrationApplications(form)); };
} else {
promises.push(Promise.resolve({ state: "empty" }));
}
return promises;
} }
async refetch() { async refetch() {

View file

@ -23,6 +23,7 @@ import { HttpService, UserService } from "../../services";
import { FirstLoadService } from "../../services/FirstLoadService"; import { FirstLoadService } from "../../services/FirstLoadService";
import { RequestState } from "../../services/HttpService"; import { RequestState } from "../../services/HttpService";
import { import {
RouteDataResponse,
editCommentReport, editCommentReport,
editPostReport, editPostReport,
editPrivateMessageReport, editPrivateMessageReport,
@ -56,6 +57,12 @@ enum MessageEnum {
PrivateMessageReport, PrivateMessageReport,
} }
type ReportsData = RouteDataResponse<{
commentReportsRes: ListCommentReportsResponse;
postReportsRes: ListPostReportsResponse;
messageReportsRes: ListPrivateMessageReportsResponse;
}>;
type ItemType = { type ItemType = {
id: number; id: number;
type_: MessageEnum; type_: MessageEnum;
@ -75,7 +82,7 @@ interface ReportsState {
} }
export class Reports extends Component<any, ReportsState> { export class Reports extends Component<any, ReportsState> {
private isoData = setIsoData(this.context); private isoData = setIsoData<ReportsData>(this.context);
state: ReportsState = { state: ReportsState = {
commentReportsRes: { state: "empty" }, commentReportsRes: { state: "empty" },
postReportsRes: { state: "empty" }, postReportsRes: { state: "empty" },
@ -99,8 +106,9 @@ 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 (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const [commentReportsRes, postReportsRes, messageReportsRes] = const { commentReportsRes, postReportsRes, messageReportsRes } =
this.isoData.routeData; this.isoData.routeData;
this.state = { this.state = {
...this.state, ...this.state,
commentReportsRes, commentReportsRes,
@ -111,7 +119,7 @@ export class Reports extends Component<any, ReportsState> {
if (amAdmin()) { if (amAdmin()) {
this.state = { this.state = {
...this.state, ...this.state,
messageReportsRes, messageReportsRes: messageReportsRes,
}; };
} }
} }
@ -481,55 +489,48 @@ export class Reports extends Component<any, ReportsState> {
await i.refetch(); await i.refetch();
} }
static fetchInitialData({ static async fetchInitialData({
auth, auth,
client, client,
}: InitialFetchRequest): Promise<any>[] { }: InitialFetchRequest): Promise<ReportsData> {
const promises: Promise<RequestState<any>>[] = [];
const unresolved_only = true; const unresolved_only = true;
const page = 1; const page = 1;
const limit = fetchLimit; const limit = fetchLimit;
if (auth) { const commentReportsForm: ListCommentReports = {
const commentReportsForm: ListCommentReports = { unresolved_only,
page,
limit,
auth: auth as string,
};
const postReportsForm: ListPostReports = {
unresolved_only,
page,
limit,
auth: auth as string,
};
const data: ReportsData = {
commentReportsRes: await client.listCommentReports(commentReportsForm),
postReportsRes: await client.listPostReports(postReportsForm),
messageReportsRes: { state: "empty" },
};
if (amAdmin()) {
const privateMessageReportsForm: ListPrivateMessageReports = {
unresolved_only, unresolved_only,
page, page,
limit, limit,
auth, auth: auth as string,
}; };
promises.push(client.listCommentReports(commentReportsForm));
const postReportsForm: ListPostReports = { data.messageReportsRes = await client.listPrivateMessageReports(
unresolved_only, privateMessageReportsForm
page,
limit,
auth,
};
promises.push(client.listPostReports(postReportsForm));
if (amAdmin()) {
const privateMessageReportsForm: ListPrivateMessageReports = {
unresolved_only,
page,
limit,
auth,
};
promises.push(
client.listPrivateMessageReports(privateMessageReportsForm)
);
} else {
promises.push(Promise.resolve({ state: "empty" }));
}
} else {
promises.push(
Promise.resolve({ state: "empty" }),
Promise.resolve({ state: "empty" }),
Promise.resolve({ state: "empty" })
); );
} }
return promises; return data;
} }
async refetch() { async refetch() {

View file

@ -3,6 +3,7 @@ import { RouteComponentProps } from "inferno-router/dist/Route";
import { import {
CreatePost as CreatePostI, CreatePost as CreatePostI,
GetCommunity, GetCommunity,
GetCommunityResponse,
GetSiteResponse, GetSiteResponse,
ListCommunitiesResponse, ListCommunitiesResponse,
} from "lemmy-js-client"; } from "lemmy-js-client";
@ -16,6 +17,7 @@ import {
} from "../../services/HttpService"; } from "../../services/HttpService";
import { import {
Choice, Choice,
RouteDataResponse,
enableDownvotes, enableDownvotes,
enableNsfw, enableNsfw,
getIdFromString, getIdFromString,
@ -32,6 +34,11 @@ export interface CreatePostProps {
communityId?: number; communityId?: number;
} }
type CreatePostData = RouteDataResponse<{
communityResponse: GetCommunityResponse;
initialCommunitiesRes: ListCommunitiesResponse;
}>;
function getCreatePostQueryParams() { function getCreatePostQueryParams() {
return getQueryParams<CreatePostProps>({ return getQueryParams<CreatePostProps>({
communityId: getIdFromString, communityId: getIdFromString,
@ -54,7 +61,7 @@ export class CreatePost extends Component<
RouteComponentProps<Record<string, never>>, RouteComponentProps<Record<string, never>>,
CreatePostState CreatePostState
> { > {
private isoData = setIsoData(this.context); private isoData = setIsoData<CreatePostData>(this.context);
state: CreatePostState = { state: CreatePostState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
loading: true, loading: true,
@ -71,7 +78,15 @@ export class CreatePost extends Component<
// Only fetch the data if coming from another route // Only fetch the data if coming from another route
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const [communityRes, listCommunitiesRes] = this.isoData.routeData; const { communityResponse: communityRes, initialCommunitiesRes } =
this.isoData.routeData;
this.state = {
...this.state,
loading: false,
initialCommunitiesRes,
isIsomorphic: true,
};
if (communityRes?.state === "success") { if (communityRes?.state === "success") {
const communityChoice: Choice = { const communityChoice: Choice = {
@ -84,13 +99,6 @@ export class CreatePost extends Component<
selectedCommunityChoice: communityChoice, selectedCommunityChoice: communityChoice,
}; };
} }
this.state = {
...this.state,
loading: false,
initialCommunitiesRes: listCommunitiesRes,
isIsomorphic: true,
};
} }
} }
@ -234,14 +242,17 @@ export class CreatePost extends Component<
} }
} }
static fetchInitialData({ static async fetchInitialData({
client, client,
query: { communityId }, query: { communityId },
auth, auth,
}: InitialFetchRequest<QueryParams<CreatePostProps>>): Promise< }: InitialFetchRequest<
RequestState<any> QueryParams<CreatePostProps>
>[] { >): Promise<CreatePostData> {
const promises: Promise<RequestState<any>>[] = []; const data: CreatePostData = {
initialCommunitiesRes: await fetchCommunitiesForOptions(client),
communityResponse: { state: "empty" },
};
if (communityId) { if (communityId) {
const form: GetCommunity = { const form: GetCommunity = {
@ -249,13 +260,9 @@ export class CreatePost extends Component<
id: getIdFromString(communityId), id: getIdFromString(communityId),
}; };
promises.push(client.getCommunity(form)); data.communityResponse = await client.getCommunity(form);
} else {
promises.push(Promise.resolve({ state: "empty" }));
} }
promises.push(fetchCommunitiesForOptions(client)); return data;
return promises;
} }
} }

View file

@ -631,7 +631,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
const post = this.postView.post; const post = this.postView.post;
return ( return (
<div className="d-flex justify-content-start flex-wrap text-muted font-weight-bold mb-1"> <div className="d-flex align-items-center justify-content-start flex-wrap text-muted font-weight-bold mb-1">
{this.commentsButton} {this.commentsButton}
{canShare() && ( {canShare() && (
<button <button

View file

@ -75,6 +75,7 @@ import {
isImage, isImage,
myAuth, myAuth,
restoreScrollPosition, restoreScrollPosition,
RouteDataResponse,
saveScrollPosition, saveScrollPosition,
setIsoData, setIsoData,
setupTippy, setupTippy,
@ -93,6 +94,11 @@ import { PostListing } from "./post-listing";
const commentsShownInterval = 15; const commentsShownInterval = 15;
type PostData = RouteDataResponse<{
postRes: GetPostResponse;
commentsRes: GetCommentsResponse;
}>;
interface PostState { interface PostState {
postId?: number; postId?: number;
commentId?: number; commentId?: number;
@ -110,7 +116,7 @@ interface PostState {
} }
export class Post extends Component<any, PostState> { export class Post extends Component<any, PostState> {
private isoData = setIsoData(this.context); private isoData = setIsoData<PostData>(this.context);
private commentScrollDebounced: () => void; private commentScrollDebounced: () => void;
state: PostState = { state: PostState = {
postRes: { state: "empty" }, postRes: { state: "empty" },
@ -169,7 +175,7 @@ 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 (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const [postRes, commentsRes] = this.isoData.routeData; const { commentsRes, postRes } = this.isoData.routeData;
this.state = { this.state = {
...this.state, ...this.state,
@ -220,13 +226,12 @@ export class Post extends Component<any, PostState> {
} }
} }
static fetchInitialData({ static async fetchInitialData({
auth,
client, client,
path, path,
}: InitialFetchRequest): Promise<any>[] { auth,
}: InitialFetchRequest): Promise<PostData> {
const pathSplit = path.split("/"); const pathSplit = path.split("/");
const promises: Promise<RequestState<any>>[] = [];
const pathType = pathSplit.at(1); const pathType = pathSplit.at(1);
const id = pathSplit.at(2) ? Number(pathSplit.at(2)) : undefined; const id = pathSplit.at(2) ? Number(pathSplit.at(2)) : undefined;
@ -252,10 +257,10 @@ export class Post extends Component<any, PostState> {
commentsForm.parent_id = id; commentsForm.parent_id = id;
} }
promises.push(client.getPost(postForm)); return {
promises.push(client.getComments(commentsForm)); postRes: await client.getPost(postForm),
commentsRes: await client.getComments(commentsForm),
return promises; };
} }
componentWillUnmount() { componentWillUnmount() {

View file

@ -10,6 +10,7 @@ import { InitialFetchRequest } from "../../interfaces";
import { FirstLoadService } from "../../services/FirstLoadService"; import { FirstLoadService } from "../../services/FirstLoadService";
import { HttpService, RequestState } from "../../services/HttpService"; import { HttpService, RequestState } from "../../services/HttpService";
import { import {
RouteDataResponse,
getRecipientIdFromProps, getRecipientIdFromProps,
myAuth, myAuth,
setIsoData, setIsoData,
@ -19,6 +20,10 @@ 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";
type CreatePrivateMessageData = RouteDataResponse<{
recipientDetailsResponse: GetPersonDetailsResponse;
}>;
interface CreatePrivateMessageState { interface CreatePrivateMessageState {
siteRes: GetSiteResponse; siteRes: GetSiteResponse;
recipientRes: RequestState<GetPersonDetailsResponse>; recipientRes: RequestState<GetPersonDetailsResponse>;
@ -30,7 +35,7 @@ export class CreatePrivateMessage extends Component<
any, any,
CreatePrivateMessageState CreatePrivateMessageState
> { > {
private isoData = setIsoData(this.context); private isoData = setIsoData<CreatePrivateMessageData>(this.context);
state: CreatePrivateMessageState = { state: CreatePrivateMessageState = {
siteRes: this.isoData.site_res, siteRes: this.isoData.site_res,
recipientRes: { state: "empty" }, recipientRes: { state: "empty" },
@ -47,7 +52,7 @@ export class CreatePrivateMessage extends Component<
if (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
this.state = { this.state = {
...this.state, ...this.state,
recipientRes: this.isoData.routeData[0], recipientRes: this.isoData.routeData.recipientDetailsResponse,
isIsomorphic: true, isIsomorphic: true,
}; };
} }
@ -59,6 +64,25 @@ export class CreatePrivateMessage extends Component<
} }
} }
static async fetchInitialData({
client,
path,
auth,
}: InitialFetchRequest): Promise<CreatePrivateMessageData> {
const person_id = Number(path.split("/").pop());
const form: GetPersonDetails = {
person_id,
sort: "New",
saved_only: false,
auth,
};
return {
recipientDetailsResponse: await client.getPersonDetails(form),
};
}
async fetchPersonDetails() { async fetchPersonDetails() {
this.setState({ this.setState({
recipientRes: { state: "loading" }, recipientRes: { state: "loading" },
@ -74,19 +98,6 @@ export class CreatePrivateMessage extends Component<
}); });
} }
static fetchInitialData(
req: InitialFetchRequest
): Promise<RequestState<any>>[] {
const person_id = Number(req.path.split("/").pop());
const form: GetPersonDetails = {
person_id,
sort: "New",
saved_only: false,
auth: req.auth,
};
return [req.client.getPersonDetails(form)];
}
get documentTitle(): string { get documentTitle(): string {
if (this.state.recipientRes.state == "success") { if (this.state.recipientRes.state == "success") {
const name_ = this.state.recipientRes.data.person_view.person.name; const name_ = this.state.recipientRes.data.person_view.person.name;

View file

@ -26,6 +26,7 @@ import { FirstLoadService } from "../services/FirstLoadService";
import { HttpService, RequestState } from "../services/HttpService"; import { HttpService, RequestState } from "../services/HttpService";
import { import {
Choice, Choice,
RouteDataResponse,
capitalizeFirstLetter, capitalizeFirstLetter,
commentsToFlatNodes, commentsToFlatNodes,
communityToChoice, communityToChoice,
@ -70,6 +71,14 @@ interface SearchProps {
page: number; page: number;
} }
type SearchData = RouteDataResponse<{
communityResponse: GetCommunityResponse;
listCommunitiesResponse: ListCommunitiesResponse;
creatorDetailsResponse: GetPersonDetailsResponse;
searchResponse: SearchResponse;
resolveObjectResponse: ResolveObjectResponse;
}>;
type FilterType = "creator" | "community"; type FilterType = "creator" | "community";
interface SearchState { interface SearchState {
@ -228,7 +237,8 @@ function getListing(
} }
export class Search extends Component<any, SearchState> { export class Search extends Component<any, SearchState> {
private isoData = setIsoData(this.context); private isoData = setIsoData<SearchData>(this.context);
state: SearchState = { state: SearchState = {
resolveObjectRes: { state: "empty" }, resolveObjectRes: { state: "empty" },
creatorDetailsRes: { state: "empty" }, creatorDetailsRes: { state: "empty" },
@ -262,42 +272,63 @@ 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 (FirstLoadService.isFirstLoad) { if (FirstLoadService.isFirstLoad) {
const [ const {
communityRes, communityResponse: communityRes,
communitiesRes, creatorDetailsResponse: creatorDetailsRes,
creatorDetailsRes, listCommunitiesResponse: communitiesRes,
searchRes, resolveObjectResponse: resolveObjectRes,
resolveObjectRes, searchResponse: searchRes,
] = this.isoData.routeData; } = this.isoData.routeData;
this.state = { this.state = {
...this.state, ...this.state,
communitiesRes,
communityRes,
creatorDetailsRes,
creatorSearchOptions:
creatorDetailsRes.state == "success"
? [personToChoice(creatorDetailsRes.data.person_view)]
: [],
isIsomorphic: true, isIsomorphic: true,
}; };
if (communityRes.state === "success") { if (creatorDetailsRes?.state === "success") {
this.state = { this.state = {
...this.state, ...this.state,
communitySearchOptions: [ creatorSearchOptions:
communityToChoice(communityRes.data.community_view), creatorDetailsRes?.state === "success"
], ? [personToChoice(creatorDetailsRes.data.person_view)]
: [],
creatorDetailsRes,
}; };
} }
if (q) { if (communitiesRes?.state === "success") {
this.state = { this.state = {
...this.state, ...this.state,
searchRes, communitiesRes,
resolveObjectRes,
}; };
} }
if (communityRes?.state === "success") {
this.state = {
...this.state,
communityRes,
};
}
if (q !== "") {
this.state = {
...this.state,
};
if (searchRes?.state === "success") {
this.state = {
...this.state,
searchRes,
};
}
if (resolveObjectRes?.state === "success") {
this.state = {
...this.state,
resolveObjectRes,
};
}
}
} }
} }
@ -328,23 +359,25 @@ export class Search extends Component<any, SearchState> {
saveScrollPosition(this.context); saveScrollPosition(this.context);
} }
static fetchInitialData({ static async fetchInitialData({
client, client,
auth, auth,
query: { communityId, creatorId, q, type, sort, listingType, page }, query: { communityId, creatorId, q, type, sort, listingType, page },
}: InitialFetchRequest<QueryParams<SearchProps>>): Promise< }: InitialFetchRequest<QueryParams<SearchProps>>): Promise<SearchData> {
RequestState<any>
>[] {
const promises: Promise<RequestState<any>>[] = [];
const community_id = getIdFromString(communityId); const community_id = getIdFromString(communityId);
let communityResponse: RequestState<GetCommunityResponse> = {
state: "empty",
};
let listCommunitiesResponse: RequestState<ListCommunitiesResponse> = {
state: "empty",
};
if (community_id) { if (community_id) {
const getCommunityForm: GetCommunity = { const getCommunityForm: GetCommunity = {
id: community_id, id: community_id,
auth, auth,
}; };
promises.push(client.getCommunity(getCommunityForm));
promises.push(Promise.resolve({ state: "empty" })); communityResponse = await client.getCommunity(getCommunityForm);
} else { } else {
const listCommunitiesForm: ListCommunities = { const listCommunitiesForm: ListCommunities = {
type_: defaultListingType, type_: defaultListingType,
@ -352,23 +385,32 @@ export class Search extends Component<any, SearchState> {
limit: fetchLimit, limit: fetchLimit,
auth, auth,
}; };
promises.push(Promise.resolve({ state: "empty" }));
promises.push(client.listCommunities(listCommunitiesForm)); listCommunitiesResponse = await client.listCommunities(
listCommunitiesForm
);
} }
const creator_id = getIdFromString(creatorId); const creator_id = getIdFromString(creatorId);
let creatorDetailsResponse: RequestState<GetPersonDetailsResponse> = {
state: "empty",
};
if (creator_id) { if (creator_id) {
const getCreatorForm: GetPersonDetails = { const getCreatorForm: GetPersonDetails = {
person_id: creator_id, person_id: creator_id,
auth, auth,
}; };
promises.push(client.getPersonDetails(getCreatorForm));
} else { creatorDetailsResponse = await client.getPersonDetails(getCreatorForm);
promises.push(Promise.resolve({ state: "empty" }));
} }
const query = getSearchQueryFromQuery(q); const query = getSearchQueryFromQuery(q);
let searchResponse: RequestState<SearchResponse> = { state: "empty" };
let resolveObjectResponse: RequestState<ResolveObjectResponse> = {
state: "empty",
};
if (query) { if (query) {
const form: SearchForm = { const form: SearchForm = {
q: query, q: query,
@ -383,21 +425,24 @@ export class Search extends Component<any, SearchState> {
}; };
if (query !== "") { if (query !== "") {
promises.push(client.search(form)); searchResponse = await client.search(form);
if (auth) { if (auth) {
const resolveObjectForm: ResolveObject = { const resolveObjectForm: ResolveObject = {
q: query, q: query,
auth, auth,
}; };
promises.push(client.resolveObject(resolveObjectForm)); resolveObjectResponse = await client.resolveObject(resolveObjectForm);
} }
} else {
promises.push(Promise.resolve({ state: "empty" }));
promises.push(Promise.resolve({ state: "empty" }));
} }
} }
return promises; return {
communityResponse,
creatorDetailsResponse,
listCommunitiesResponse,
resolveObjectResponse,
searchResponse,
};
} }
get documentTitle(): string { get documentTitle(): string {
@ -463,7 +508,7 @@ export class Search extends Component<any, SearchState> {
minLength={1} minLength={1}
/> />
<button type="submit" className="btn btn-secondary mr-2 mb-2"> <button type="submit" className="btn btn-secondary mr-2 mb-2">
{this.state.searchRes.state == "loading" ? ( {this.state.searchRes.state === "loading" ? (
<Spinner /> <Spinner />
) : ( ) : (
<span>{i18n.t("search")}</span> <span>{i18n.t("search")}</span>

View file

@ -6,15 +6,17 @@ import { ErrorPageData } from "./utils";
/** /**
* This contains serialized data, it needs to be deserialized before use. * This contains serialized data, it needs to be deserialized before use.
*/ */
export interface IsoData { export interface IsoData<T extends RouteData = any> {
path: string; path: string;
routeData: RequestState<any>[]; routeData: T;
site_res: GetSiteResponse; site_res: GetSiteResponse;
errorPageData?: ErrorPageData; errorPageData?: ErrorPageData;
} }
export type IsoDataOptionalSite = Partial<IsoData> & export type IsoDataOptionalSite<T extends RouteData = any> = Partial<
Pick<IsoData, Exclude<keyof IsoData, "site_res">>; IsoData<T>
> &
Pick<IsoData<T>, Exclude<keyof IsoData<T>, "site_res">>;
export interface ILemmyConfig { export interface ILemmyConfig {
wsHost?: string; wsHost?: string;
@ -80,3 +82,5 @@ export interface CommentNodeI {
children: Array<CommentNodeI>; children: Array<CommentNodeI>;
depth: number; depth: number;
} }
export type RouteData = Record<string, RequestState<any>>;

View file

@ -21,15 +21,13 @@ import { CreatePost } from "./components/post/create-post";
import { Post } from "./components/post/post"; import { Post } from "./components/post/post";
import { CreatePrivateMessage } from "./components/private_message/create-private-message"; import { CreatePrivateMessage } from "./components/private_message/create-private-message";
import { Search } from "./components/search"; import { Search } from "./components/search";
import { InitialFetchRequest } from "./interfaces"; import { InitialFetchRequest, RouteData } from "./interfaces";
import { RequestState } from "./services/HttpService";
interface IRoutePropsWithFetch extends IRouteProps { interface IRoutePropsWithFetch<T extends RouteData> extends IRouteProps {
// TODO Make sure this one is good. fetchInitialData?(req: InitialFetchRequest): Promise<T>;
fetchInitialData?(req: InitialFetchRequest): Promise<RequestState<any>>[];
} }
export const routes: IRoutePropsWithFetch[] = [ export const routes: IRoutePropsWithFetch<Record<string, any>>[] = [
{ {
path: `/`, path: `/`,
component: Home, component: Home,

View file

@ -11,7 +11,7 @@ type LoadingRequestState = {
state: "loading"; state: "loading";
}; };
type FailedRequestState = { export type FailedRequestState = {
state: "failed"; state: "failed";
msg: string; msg: string;
}; };
@ -58,7 +58,7 @@ class WrappedLemmyHttpClient {
return { return {
data: res, data: res,
state: "success", state: !(res === undefined || res === null) ? "success" : "empty",
}; };
} catch (error) { } catch (error) {
console.error(`API error: ${error}`); console.error(`API error: ${error}`);

View file

@ -41,11 +41,18 @@ import tippy from "tippy.js";
import Toastify from "toastify-js"; import Toastify from "toastify-js";
import { getHttpBase } from "./env"; import { getHttpBase } from "./env";
import { i18n } from "./i18next"; import { i18n } from "./i18next";
import { CommentNodeI, DataType, IsoData, VoteType } from "./interfaces"; import {
CommentNodeI,
DataType,
IsoData,
RouteData,
VoteType,
} from "./interfaces";
import { HttpService, UserService } from "./services"; import { HttpService, UserService } from "./services";
import { isBrowser } from "./utils/browser/is-browser"; import { isBrowser } from "./utils/browser/is-browser";
import { debounce } from "./utils/helpers/debounce"; import { debounce } from "./utils/helpers/debounce";
import { groupBy } from "./utils/helpers/group-by"; import { groupBy } from "./utils/helpers/group-by";
import { RequestState } from "./services/HttpService";
let Tribute: any; let Tribute: any;
if (isBrowser()) { if (isBrowser()) {
@ -242,7 +249,12 @@ export function isVideo(url: string) {
} }
export function validURL(str: string) { export function validURL(str: string) {
return !!new URL(str); try {
new URL(str);
return true;
} catch {
return false;
}
} }
export function validInstanceTLD(str: string) { export function validInstanceTLD(str: string) {
@ -1011,7 +1023,7 @@ export function siteBannerCss(banner: string): string {
`; `;
} }
export function setIsoData(context: any): IsoData { export function setIsoData<T extends RouteData>(context: any): IsoData<T> {
// If its the browser, you need to deserialize the data from the window // If its the browser, you need to deserialize the data from the window
if (isBrowser()) { if (isBrowser()) {
return window.isoData; return window.isoData;
@ -1264,3 +1276,7 @@ export function newVote(voteType: VoteType, myVote?: number): number {
return myVote == -1 ? 0 : -1; return myVote == -1 ? 0 : -1;
} }
} }
export type RouteDataResponse<T extends Record<string, any>> = {
[K in keyof T]: RequestState<T[K]>;
};