mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-23 11:21:26 +00:00
Merge branch 'main' into breakout-role-utils
This commit is contained in:
commit
7f48a38b72
45 changed files with 8135 additions and 4751 deletions
|
@ -1 +1 @@
|
|||
Subproject commit f45ddff206adb52ab0ac7555bf14978edac5d2f2
|
||||
Subproject commit c9a07885f35cf334d3cf167cb57587a8177fc3fb
|
19
package.json
19
package.json
|
@ -14,12 +14,21 @@
|
|||
"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}\"",
|
||||
"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": {
|
||||
"*.{ts,tsx,js}": ["prettier --write", "eslint --fix"],
|
||||
"*.{css, scss}": ["prettier --write"],
|
||||
"package.json": ["sortpack"]
|
||||
"*.{ts,tsx,js}": [
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
],
|
||||
"*.{css, scss}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"package.json": [
|
||||
"sortpack"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
|
@ -94,7 +103,7 @@
|
|||
"@types/toastify-js": "^1.11.1",
|
||||
"@typescript-eslint/eslint-plugin": "^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-plugin-inferno": "^7.32.2",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
}
|
||||
|
||||
.md-div p:last-child {
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.md-div img {
|
||||
|
@ -371,7 +371,7 @@ br.big {
|
|||
}
|
||||
|
||||
.tribute-container li {
|
||||
padding: 5px 5px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -410,13 +410,22 @@ br.big {
|
|||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.lang-select-action {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.lang-select-action:focus {
|
||||
width: auto;
|
||||
}
|
||||
em-emoji-picker {
|
||||
.emoji-picker {
|
||||
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;
|
||||
}
|
||||
|
|
108
src/assets/css/themes/_variables.darkly-red.scss
Normal file
108
src/assets/css/themes/_variables.darkly-red.scss
Normal 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;
|
|
@ -103,5 +103,5 @@ $input-bg: $secondary;
|
|||
$input-color: $white;
|
||||
$input-disabled-bg: darken($secondary, 10%);
|
||||
$light: $gray-800;
|
||||
$navbar-light-brand-color: $navbar-dark-active-color;
|
||||
$navbar-light-brand-hover-color: $navbar-dark-active-color;
|
||||
$navbar-light-brand-color: $white;
|
||||
$navbar-light-brand-hover-color: $navbar-light-brand-color;
|
||||
|
|
47
src/assets/css/themes/_variables.litely-red.scss
Normal file
47
src/assets/css/themes/_variables.litely-red.scss
Normal 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;
|
|
@ -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;
|
||||
$orange: #f1641e;
|
||||
$cyan: #02bdc2;
|
||||
$green: #00c853;
|
||||
$secondary: $green;
|
||||
$body-color: $gray-700;
|
||||
$link-color: theme-color("primary");
|
||||
$primary: $orange;
|
||||
$secondary: $green;
|
||||
$info: $cyan;
|
||||
$body-color: $gray-700;
|
||||
$link-color: $primary;
|
||||
$red: #d8486a;
|
||||
$border-radius: 0.5rem;
|
||||
$border-radius-lg: 0.5rem;
|
||||
|
@ -13,8 +27,8 @@ $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($component-active-bg, 0.75);
|
||||
$form-feedback-valid-color: theme-color("info");
|
||||
$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);
|
||||
|
|
File diff suppressed because it is too large
Load diff
2
src/assets/css/themes/darkly-red.scss
Normal file
2
src/assets/css/themes/darkly-red.scss
Normal 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
2
src/assets/css/themes/darkly.scss
Normal file
2
src/assets/css/themes/darkly.scss
Normal 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
2
src/assets/css/themes/litely-red.scss
Normal file
2
src/assets/css/themes/litely-red.scss
Normal 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
2
src/assets/css/themes/litely.scss
Normal file
2
src/assets/css/themes/litely.scss
Normal file
|
@ -0,0 +1,2 @@
|
|||
@import "variables.litely";
|
||||
@import "../../../../node_modules/bootstrap-v4/scss/bootstrap";
|
|
@ -17,9 +17,10 @@ import {
|
|||
ILemmyConfig,
|
||||
InitialFetchRequest,
|
||||
IsoDataOptionalSite,
|
||||
RouteData,
|
||||
} from "../shared/interfaces";
|
||||
import { routes } from "../shared/routes";
|
||||
import { RequestState, wrapClient } from "../shared/services/HttpService";
|
||||
import { FailedRequestState, wrapClient } from "../shared/services/HttpService";
|
||||
import {
|
||||
ErrorPageData,
|
||||
favIconPngUrl,
|
||||
|
@ -136,7 +137,7 @@ server.get("/*", async (req, res) => {
|
|||
// 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
|
||||
let site: GetSiteResponse | undefined = undefined;
|
||||
const routeData: RequestState<any>[] = [];
|
||||
let routeData: RouteData = {};
|
||||
let errorPageData: ErrorPageData | undefined = undefined;
|
||||
let try_site = await client.getSite(getSiteForm);
|
||||
if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
|
||||
|
@ -160,7 +161,7 @@ server.get("/*", async (req, res) => {
|
|||
return res.redirect("/setup");
|
||||
}
|
||||
|
||||
if (site) {
|
||||
if (site && activeRoute?.fetchInitialData) {
|
||||
const initialFetchReq: InitialFetchRequest = {
|
||||
client,
|
||||
auth,
|
||||
|
@ -169,26 +170,23 @@ server.get("/*", async (req, res) => {
|
|||
site,
|
||||
};
|
||||
|
||||
if (activeRoute?.fetchInitialData) {
|
||||
routeData.push(
|
||||
...(await Promise.all([
|
||||
...activeRoute.fetchInitialData(initialFetchReq),
|
||||
]))
|
||||
);
|
||||
}
|
||||
routeData = await activeRoute.fetchInitialData(initialFetchReq);
|
||||
}
|
||||
} else if (try_site.state === "failed") {
|
||||
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
|
||||
if (routeData[0] && routeData[0].state === "failed") {
|
||||
const error = routeData[0].msg;
|
||||
console.error(error);
|
||||
if (error === "instance_is_private") {
|
||||
if (error) {
|
||||
console.error(error.msg);
|
||||
if (error.msg === "instance_is_private") {
|
||||
return res.redirect(`/signup`);
|
||||
} else {
|
||||
errorPageData = getErrorPageData(new Error(error), site);
|
||||
errorPageData = getErrorPageData(new Error(error.msg), site);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component } from "inferno";
|
||||
import { Component, createRef, linkEvent, RefObject } from "inferno";
|
||||
import { Provider } from "inferno-i18next-dess";
|
||||
import { Route, Switch } from "inferno-router";
|
||||
import { i18n } from "../../i18next";
|
||||
|
@ -15,8 +15,15 @@ import { Theme } from "./theme";
|
|||
|
||||
export class App extends Component<any, any> {
|
||||
private isoData: IsoDataOptionalSite = setIsoData(this.context);
|
||||
private readonly mainContentRef: RefObject<HTMLElement>;
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.mainContentRef = createRef();
|
||||
}
|
||||
|
||||
handleJumpToContent(event) {
|
||||
event.preventDefault();
|
||||
this.mainContentRef.current?.focus();
|
||||
}
|
||||
render() {
|
||||
const siteRes = this.isoData.site_res;
|
||||
|
@ -26,6 +33,12 @@ export class App extends Component<any, any> {
|
|||
<>
|
||||
<Provider i18next={i18n}>
|
||||
<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 && (
|
||||
<Theme defaultTheme={siteView.local_site.default_theme} />
|
||||
)}
|
||||
|
@ -39,6 +52,7 @@ export class App extends Component<any, any> {
|
|||
exact
|
||||
component={routeProps => (
|
||||
<ErrorGuard>
|
||||
<main tabIndex={-1} ref={this.mainContentRef}>
|
||||
{RouteComponent &&
|
||||
(isAuthPath(path ?? "") ? (
|
||||
<AuthGuard>
|
||||
|
@ -47,6 +61,7 @@ export class App extends Component<any, any> {
|
|||
) : (
|
||||
<RouteComponent {...routeProps} />
|
||||
))}
|
||||
</main>
|
||||
</ErrorGuard>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -224,11 +224,14 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
)}
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className="nav-link"
|
||||
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
||||
title={i18n.t("support_lemmy")}
|
||||
href={donateLemmyUrl}
|
||||
>
|
||||
<Icon icon="heart" classes="small" />
|
||||
<span className="d-inline ml-1 d-md-none ml-md-0">
|
||||
{i18n.t("support_lemmy")}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -236,22 +239,28 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
<li id="navSearch" className="nav-item">
|
||||
<NavLink
|
||||
to="/search"
|
||||
className="nav-link"
|
||||
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
||||
title={i18n.t("search")}
|
||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||
>
|
||||
<Icon icon="search" />
|
||||
<span className="d-inline ml-1 d-md-none ml-md-0">
|
||||
{i18n.t("search")}
|
||||
</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
{amAdmin() && (
|
||||
<li id="navAdmin" className="nav-item">
|
||||
<NavLink
|
||||
to="/admin"
|
||||
className="nav-link"
|
||||
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
||||
title={i18n.t("admin_settings")}
|
||||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||
>
|
||||
<Icon icon="settings" />
|
||||
<span className="d-inline ml-1 d-md-none ml-md-0">
|
||||
{i18n.t("admin_settings")}
|
||||
</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
)}
|
||||
|
@ -259,7 +268,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
<>
|
||||
<li id="navMessages" className="nav-item">
|
||||
<NavLink
|
||||
className="nav-link"
|
||||
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
||||
to="/inbox"
|
||||
title={i18n.t("unread_messages", {
|
||||
count: Number(this.unreadInboxCount),
|
||||
|
@ -268,6 +277,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||
>
|
||||
<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 && (
|
||||
<span className="mx-1 badge badge-light">
|
||||
{numToSI(this.unreadInboxCount)}
|
||||
|
@ -278,7 +293,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
{this.moderatesSomething && (
|
||||
<li id="navModeration" className="nav-item">
|
||||
<NavLink
|
||||
className="nav-link"
|
||||
className="nav-link d-inline-flex align-items-center d-md-inline-block"
|
||||
to="/reports"
|
||||
title={i18n.t("unread_reports", {
|
||||
count: Number(this.unreadReportCount),
|
||||
|
@ -287,6 +302,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||
>
|
||||
<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 && (
|
||||
<span className="mx-1 badge badge-light">
|
||||
{numToSI(this.unreadReportCount)}
|
||||
|
@ -299,7 +320,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
<li id="navApplications" className="nav-item">
|
||||
<NavLink
|
||||
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", {
|
||||
count: Number(this.unreadApplicationCount),
|
||||
formattedCount: numToSI(this.unreadApplicationCount),
|
||||
|
@ -307,6 +328,12 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
|||
onMouseUp={linkEvent(this, handleCollapseClick)}
|
||||
>
|
||||
<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 && (
|
||||
<span className="mx-1 badge badge-light">
|
||||
{numToSI(this.unreadApplicationCount)}
|
||||
|
|
128
src/shared/components/common/badges.tsx
Normal file
128
src/shared/components/common/badges.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -100,12 +100,9 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
|||
|
||||
return (
|
||||
<select
|
||||
className={classNames(
|
||||
"lang-select-action",
|
||||
this.props.iconVersion
|
||||
? "btn btn-sm text-muted"
|
||||
: "form-control custom-select"
|
||||
)}
|
||||
className={classNames("lang-select-action", {
|
||||
"form-control custom-select": !this.props.iconVersion,
|
||||
})}
|
||||
id={this.id}
|
||||
onChange={linkEvent(this, this.handleLanguageChange)}
|
||||
aria-label={i18n.t("language_select_placeholder")}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import autosize from "autosize";
|
||||
import classNames from "classnames";
|
||||
import { NoOptionI18nKeys } from "i18next";
|
||||
import { Component, linkEvent } from "inferno";
|
||||
import { Language } from "lemmy-js-client";
|
||||
|
@ -143,47 +144,9 @@ export class MarkdownTextArea extends Component<
|
|||
}
|
||||
/>
|
||||
<div className="form-group row">
|
||||
<div className={`col-sm-12`}>
|
||||
<textarea
|
||||
id={this.id}
|
||||
className={`form-control ${this.state.previewMode && "d-none"}`}
|
||||
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>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-12 d-flex flex-wrap">
|
||||
<div className="col-12">
|
||||
<div className="rounded bg-light border border-light">
|
||||
<div className="d-flex flex-wrap border-bottom border-light">
|
||||
{this.getFormatButton("bold", this.handleInsertBold)}
|
||||
{this.getFormatButton("italic", this.handleInsertItalic)}
|
||||
{this.getFormatButton("link", this.handleInsertLink)}
|
||||
|
@ -212,7 +175,9 @@ export class MarkdownTextArea extends Component<
|
|||
name="file"
|
||||
className="d-none"
|
||||
multiple
|
||||
disabled={!UserService.Instance.myUserInfo || this.isDisabled}
|
||||
disabled={
|
||||
!UserService.Instance.myUserInfo || this.isDisabled
|
||||
}
|
||||
onChange={linkEvent(this, this.handleImageUpload)}
|
||||
/>
|
||||
</form>
|
||||
|
@ -225,7 +190,10 @@ export class MarkdownTextArea extends Component<
|
|||
{this.getFormatButton("list", this.handleInsertList)}
|
||||
{this.getFormatButton("code", this.handleInsertCode)}
|
||||
{this.getFormatButton("subscript", this.handleInsertSubscript)}
|
||||
{this.getFormatButton("superscript", this.handleInsertSuperscript)}
|
||||
{this.getFormatButton(
|
||||
"superscript",
|
||||
this.handleInsertSuperscript
|
||||
)}
|
||||
{this.getFormatButton("spoiler", this.handleInsertSpoiler)}
|
||||
<a
|
||||
href={markdownHelpUrl}
|
||||
|
@ -237,7 +205,55 @@ export class MarkdownTextArea extends Component<
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-12 d-flex align-items-center flex-wrap">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 d-flex align-items-center flex-wrap mt-2">
|
||||
{this.props.showLanguage && (
|
||||
<LanguageSelect
|
||||
iconVersion
|
||||
|
@ -257,7 +273,7 @@ export class MarkdownTextArea extends Component<
|
|||
{this.props.buttonTitle && (
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-secondary mr-2"
|
||||
className="btn btn-sm btn-secondary ml-2"
|
||||
disabled={this.isDisabled}
|
||||
>
|
||||
{this.state.loading ? (
|
||||
|
@ -270,7 +286,7 @@ export class MarkdownTextArea extends Component<
|
|||
{this.props.replyType && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary mr-2"
|
||||
className="btn btn-sm btn-secondary ml-2"
|
||||
onClick={linkEvent(this, this.handleReplyCancel)}
|
||||
>
|
||||
{i18n.t("cancel")}
|
||||
|
@ -278,7 +294,7 @@ export class MarkdownTextArea extends Component<
|
|||
)}
|
||||
{this.state.content && (
|
||||
<button
|
||||
className={`btn btn-sm btn-secondary mr-2 ${
|
||||
className={`btn btn-sm btn-secondary ml-2 ${
|
||||
this.state.previewMode && "active"
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
|
|
|
@ -22,7 +22,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<picture>
|
||||
<picture className="d-inline-block overflow-hidden">
|
||||
<source srcSet={this.src("webp")} type="image/webp" />
|
||||
<source srcSet={this.props.src} />
|
||||
<source srcSet={this.src("jpg")} type="image/jpeg" />
|
||||
|
|
|
@ -30,6 +30,10 @@ import { CommunityLink } from "./community-link";
|
|||
|
||||
const communityLimit = 50;
|
||||
|
||||
type CommunitiesData = RouteDataResponse<{
|
||||
listCommunitiesResponse: ListCommunitiesResponse;
|
||||
}>;
|
||||
|
||||
interface CommunitiesState {
|
||||
listCommunitiesResponse: RequestState<ListCommunitiesResponse>;
|
||||
siteRes: GetSiteResponse;
|
||||
|
@ -47,7 +51,7 @@ function getListingTypeFromQuery(listingType?: string): ListingType {
|
|||
}
|
||||
|
||||
export class Communities extends Component<any, CommunitiesState> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<CommunitiesData>(this.context);
|
||||
state: CommunitiesState = {
|
||||
listCommunitiesResponse: { state: "empty" },
|
||||
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
|
||||
if (FirstLoadService.isFirstLoad) {
|
||||
const { listCommunitiesResponse } = this.isoData.routeData;
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
listCommunitiesResponse: this.isoData.routeData[0],
|
||||
listCommunitiesResponse,
|
||||
isIsomorphic: true,
|
||||
};
|
||||
}
|
||||
|
@ -274,13 +280,13 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
i.context.router.history.push(`/search?q=${searchParamEncoded}`);
|
||||
}
|
||||
|
||||
static fetchInitialData({
|
||||
static async fetchInitialData({
|
||||
query: { listingType, page },
|
||||
client,
|
||||
auth,
|
||||
}: InitialFetchRequest<QueryParams<CommunitiesProps>>): Promise<
|
||||
RequestState<any>
|
||||
>[] {
|
||||
}: InitialFetchRequest<
|
||||
QueryParams<CommunitiesProps>
|
||||
>): Promise<CommunitiesData> {
|
||||
const listCommunitiesForm: ListCommunities = {
|
||||
type_: getListingTypeFromQuery(listingType),
|
||||
sort: "TopMonth",
|
||||
|
@ -289,7 +295,11 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
auth: auth,
|
||||
};
|
||||
|
||||
return [client.listCommunities(listCommunitiesForm)];
|
||||
return {
|
||||
listCommunitiesResponse: await client.listCommunities(
|
||||
listCommunitiesForm
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
getCommunitiesQueryParams() {
|
||||
|
|
|
@ -99,6 +99,13 @@ import { Sidebar } from "../community/sidebar";
|
|||
import { SiteSidebar } from "../home/site-sidebar";
|
||||
import { PostListings } from "../post/post-listings";
|
||||
import { CommunityLink } from "./community-link";
|
||||
|
||||
type CommunityData = RouteDataResponse<{
|
||||
communityRes: GetCommunityResponse;
|
||||
postsRes: GetPostsResponse;
|
||||
commentsRes: GetCommentsResponse;
|
||||
}>;
|
||||
|
||||
interface State {
|
||||
communityRes: RequestState<GetCommunityResponse>;
|
||||
postsRes: RequestState<GetPostsResponse>;
|
||||
|
@ -139,7 +146,7 @@ export class Community extends Component<
|
|||
RouteComponentProps<{ name: string }>,
|
||||
State
|
||||
> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<CommunityData>(this.context);
|
||||
state: State = {
|
||||
communityRes: { state: "empty" },
|
||||
postsRes: { state: "empty" },
|
||||
|
@ -193,13 +200,14 @@ export class Community extends Component<
|
|||
|
||||
// Only fetch the data if coming from another route
|
||||
if (FirstLoadService.isFirstLoad) {
|
||||
const [communityRes, postsRes, commentsRes] = this.isoData.routeData;
|
||||
const { communityRes, commentsRes, postsRes } = this.isoData.routeData;
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
isIsomorphic: true,
|
||||
commentsRes,
|
||||
communityRes,
|
||||
postsRes,
|
||||
commentsRes,
|
||||
isIsomorphic: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -226,23 +234,21 @@ export class Community extends Component<
|
|||
saveScrollPosition(this.context);
|
||||
}
|
||||
|
||||
static fetchInitialData({
|
||||
static async fetchInitialData({
|
||||
client,
|
||||
path,
|
||||
query: { dataType: urlDataType, page: urlPage, sort: urlSort },
|
||||
auth,
|
||||
}: InitialFetchRequest<QueryParams<CommunityProps>>): Promise<
|
||||
RequestState<any>
|
||||
>[] {
|
||||
Promise<CommunityData>
|
||||
> {
|
||||
const pathSplit = path.split("/");
|
||||
const promises: Promise<RequestState<any>>[] = [];
|
||||
|
||||
const communityName = pathSplit[2];
|
||||
const communityForm: GetCommunity = {
|
||||
name: communityName,
|
||||
auth,
|
||||
};
|
||||
promises.push(client.getCommunity(communityForm));
|
||||
|
||||
const dataType = getDataTypeFromQuery(urlDataType);
|
||||
|
||||
|
@ -250,6 +256,11 @@ export class Community extends Component<
|
|||
|
||||
const page = getPageFromString(urlPage);
|
||||
|
||||
let postsResponse: RequestState<GetPostsResponse> = { state: "empty" };
|
||||
let commentsResponse: RequestState<GetCommentsResponse> = {
|
||||
state: "empty",
|
||||
};
|
||||
|
||||
if (dataType === DataType.Post) {
|
||||
const getPostsForm: GetPosts = {
|
||||
community_name: communityName,
|
||||
|
@ -260,8 +271,8 @@ export class Community extends Component<
|
|||
saved_only: false,
|
||||
auth,
|
||||
};
|
||||
promises.push(client.getPosts(getPostsForm));
|
||||
promises.push(Promise.resolve({ state: "empty" }));
|
||||
|
||||
postsResponse = await client.getPosts(getPostsForm);
|
||||
} else {
|
||||
const getCommentsForm: GetComments = {
|
||||
community_name: communityName,
|
||||
|
@ -272,11 +283,15 @@ export class Community extends Component<
|
|||
saved_only: false,
|
||||
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 {
|
||||
|
|
|
@ -21,11 +21,11 @@ import {
|
|||
hostname,
|
||||
mdToHtml,
|
||||
myAuthRequired,
|
||||
numToSI,
|
||||
} from "../../utils";
|
||||
import { amAdmin } from "../../utils/roles/am-admin";
|
||||
import { amMod } from "../../utils/roles/am-mod";
|
||||
import { amTopMod } from "../../utils/roles/am-top-mod";
|
||||
import { Badges } from "../common/badges";
|
||||
import { BannerIconHeader } from "../common/banner-icon-header";
|
||||
import { Icon, PurgeWarning, Spinner } from "../common/icon";
|
||||
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">
|
||||
<div className="card-body">
|
||||
{this.description()}
|
||||
{this.badges()}
|
||||
<Badges
|
||||
communityId={this.props.community_view.community.id}
|
||||
counts={this.props.community_view.counts}
|
||||
/>
|
||||
{this.mods()}
|
||||
</div>
|
||||
</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() {
|
||||
return (
|
||||
<ul className="list-inline small">
|
||||
|
|
|
@ -14,6 +14,7 @@ import { InitialFetchRequest } from "../../interfaces";
|
|||
import { FirstLoadService } from "../../services/FirstLoadService";
|
||||
import { HttpService, RequestState } from "../../services/HttpService";
|
||||
import {
|
||||
RouteDataResponse,
|
||||
capitalizeFirstLetter,
|
||||
fetchThemeList,
|
||||
myAuthRequired,
|
||||
|
@ -32,6 +33,11 @@ import RateLimitForm from "./rate-limit-form";
|
|||
import { SiteForm } from "./site-form";
|
||||
import { TaglineForm } from "./tagline-form";
|
||||
|
||||
type AdminSettingsData = RouteDataResponse<{
|
||||
bannedRes: BannedPersonsResponse;
|
||||
instancesRes: GetFederatedInstancesResponse;
|
||||
}>;
|
||||
|
||||
interface AdminSettingsState {
|
||||
siteRes: GetSiteResponse;
|
||||
banned: PersonView[];
|
||||
|
@ -46,7 +52,7 @@ interface AdminSettingsState {
|
|||
}
|
||||
|
||||
export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<AdminSettingsData>(this.context);
|
||||
state: AdminSettingsState = {
|
||||
siteRes: this.isoData.site_res,
|
||||
banned: [],
|
||||
|
@ -70,7 +76,8 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
|||
|
||||
// Only fetch the data if coming from another route
|
||||
if (FirstLoadService.isFirstLoad) {
|
||||
const [bannedRes, instancesRes] = this.isoData.routeData;
|
||||
const { bannedRes, instancesRes } = this.isoData.routeData;
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
bannedRes,
|
||||
|
@ -80,47 +87,18 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
|||
}
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
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({
|
||||
static async fetchInitialData({
|
||||
auth,
|
||||
client,
|
||||
}: InitialFetchRequest): Promise<any>[] {
|
||||
const promises: Promise<RequestState<any>>[] = [];
|
||||
|
||||
if (auth) {
|
||||
promises.push(client.getBannedPersons({ auth }));
|
||||
promises.push(client.getFederatedInstances({ auth }));
|
||||
} else {
|
||||
promises.push(
|
||||
Promise.resolve({ state: "empty" }),
|
||||
Promise.resolve({ state: "empty" })
|
||||
);
|
||||
}
|
||||
|
||||
return promises;
|
||||
}: InitialFetchRequest): Promise<AdminSettingsData> {
|
||||
return {
|
||||
bannedRes: await client.getBannedPersons({
|
||||
auth: auth as string,
|
||||
}),
|
||||
instancesRes: await client.getFederatedInstances({
|
||||
auth: auth as string,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
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() {
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -73,6 +73,7 @@ import {
|
|||
postToCommentSortType,
|
||||
relTags,
|
||||
restoreScrollPosition,
|
||||
RouteDataResponse,
|
||||
saveScrollPosition,
|
||||
setIsoData,
|
||||
setupTippy,
|
||||
|
@ -117,6 +118,45 @@ interface HomeProps {
|
|||
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 {
|
||||
return type ? DataType[type] : DataType.Post;
|
||||
}
|
||||
|
@ -176,7 +216,7 @@ const LinkButton = ({
|
|||
);
|
||||
|
||||
export class Home extends Component<any, HomeState> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<HomeData>(this.context);
|
||||
state: HomeState = {
|
||||
postsRes: { 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
|
||||
if (FirstLoadService.isFirstLoad) {
|
||||
const [postsRes, commentsRes, trendingCommunitiesRes] =
|
||||
const { trendingCommunitiesRes, commentsRes, postsRes } =
|
||||
this.isoData.routeData;
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
postsRes,
|
||||
commentsRes,
|
||||
trendingCommunitiesRes,
|
||||
commentsRes,
|
||||
postsRes,
|
||||
tagline: getRandomFromList(this.state?.siteRes?.taglines ?? [])
|
||||
?.content,
|
||||
isIsomorphic: true,
|
||||
|
@ -244,7 +284,12 @@ export class Home extends Component<any, HomeState> {
|
|||
}
|
||||
|
||||
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()]);
|
||||
}
|
||||
|
||||
|
@ -255,13 +300,11 @@ export class Home extends Component<any, HomeState> {
|
|||
saveScrollPosition(this.context);
|
||||
}
|
||||
|
||||
static fetchInitialData({
|
||||
static async fetchInitialData({
|
||||
client,
|
||||
auth,
|
||||
query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
|
||||
}: InitialFetchRequest<QueryParams<HomeProps>>): Promise<
|
||||
RequestState<any>
|
||||
>[] {
|
||||
}: InitialFetchRequest<QueryParams<HomeProps>>): Promise<HomeData> {
|
||||
const dataType = getDataTypeFromQuery(urlDataType);
|
||||
|
||||
// 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 promises: Promise<RequestState<any>>[] = [];
|
||||
let postsRes: RequestState<GetPostsResponse> = { state: "empty" };
|
||||
let commentsRes: RequestState<GetCommentsResponse> = {
|
||||
state: "empty",
|
||||
};
|
||||
|
||||
if (dataType === DataType.Post) {
|
||||
const getPostsForm: GetPosts = {
|
||||
|
@ -282,8 +328,7 @@ export class Home extends Component<any, HomeState> {
|
|||
auth,
|
||||
};
|
||||
|
||||
promises.push(client.getPosts(getPostsForm));
|
||||
promises.push(Promise.resolve({ state: "empty" }));
|
||||
postsRes = await client.getPosts(getPostsForm);
|
||||
} else {
|
||||
const getCommentsForm: GetComments = {
|
||||
page,
|
||||
|
@ -293,8 +338,8 @@ export class Home extends Component<any, HomeState> {
|
|||
saved_only: false,
|
||||
auth,
|
||||
};
|
||||
promises.push(Promise.resolve({ state: "empty" }));
|
||||
promises.push(client.getComments(getCommentsForm));
|
||||
|
||||
commentsRes = await client.getComments(getCommentsForm);
|
||||
}
|
||||
|
||||
const trendingCommunitiesForm: ListCommunities = {
|
||||
|
@ -303,9 +348,14 @@ export class Home extends Component<any, HomeState> {
|
|||
limit: trendingFetchLimit,
|
||||
auth,
|
||||
};
|
||||
promises.push(client.listCommunities(trendingCommunitiesForm));
|
||||
|
||||
return promises;
|
||||
return {
|
||||
trendingCommunitiesRes: await client.listCommunities(
|
||||
trendingCommunitiesForm
|
||||
),
|
||||
commentsRes,
|
||||
postsRes,
|
||||
};
|
||||
}
|
||||
|
||||
get documentTitle(): string {
|
||||
|
@ -340,7 +390,7 @@ export class Home extends Component<any, HomeState> {
|
|||
></div>
|
||||
)}
|
||||
<div className="d-block d-md-none">{this.mobileView}</div>
|
||||
{this.posts()}
|
||||
{this.posts}
|
||||
</main>
|
||||
<aside className="d-none d-md-block col-md-4">
|
||||
{this.mySidebar}
|
||||
|
@ -552,7 +602,7 @@ export class Home extends Component<any, HomeState> {
|
|||
await this.fetchData();
|
||||
}
|
||||
|
||||
posts() {
|
||||
get posts() {
|
||||
const { page } = getHomeQueryParams();
|
||||
|
||||
return (
|
||||
|
@ -571,7 +621,7 @@ export class Home extends Component<any, HomeState> {
|
|||
const siteRes = this.state.siteRes;
|
||||
|
||||
if (dataType === DataType.Post) {
|
||||
switch (this.state.postsRes?.state) {
|
||||
switch (this.state.postsRes.state) {
|
||||
case "loading":
|
||||
return (
|
||||
<h5>
|
||||
|
@ -677,44 +727,11 @@ export class Home extends Component<any, HomeState> {
|
|||
<span className="mr-2">
|
||||
<SortSelect sort={sort} onChange={this.handleSortChange} />
|
||||
</span>
|
||||
{this.getRss(listingType)}
|
||||
{getRss(listingType)}
|
||||
</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() {
|
||||
this.setState({ trendingCommunitiesRes: { state: "loading" } });
|
||||
this.setState({
|
||||
|
|
|
@ -8,10 +8,14 @@ import { i18n } from "../../i18next";
|
|||
import { InitialFetchRequest } from "../../interfaces";
|
||||
import { FirstLoadService } from "../../services/FirstLoadService";
|
||||
import { HttpService, RequestState } from "../../services/HttpService";
|
||||
import { relTags, setIsoData } from "../../utils";
|
||||
import { RouteDataResponse, relTags, setIsoData } from "../../utils";
|
||||
import { HtmlTags } from "../common/html-tags";
|
||||
import { Spinner } from "../common/icon";
|
||||
|
||||
type InstancesData = RouteDataResponse<{
|
||||
federatedInstancesResponse: GetFederatedInstancesResponse;
|
||||
}>;
|
||||
|
||||
interface InstancesState {
|
||||
instancesRes: RequestState<GetFederatedInstancesResponse>;
|
||||
siteRes: GetSiteResponse;
|
||||
|
@ -19,7 +23,7 @@ interface InstancesState {
|
|||
}
|
||||
|
||||
export class Instances extends Component<any, InstancesState> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<InstancesData>(this.context);
|
||||
state: InstancesState = {
|
||||
instancesRes: { state: "empty" },
|
||||
siteRes: this.isoData.site_res,
|
||||
|
@ -33,7 +37,7 @@ export class Instances extends Component<any, InstancesState> {
|
|||
if (FirstLoadService.isFirstLoad) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
instancesRes: this.isoData.routeData[0],
|
||||
instancesRes: this.isoData.routeData.federatedInstancesResponse,
|
||||
isIsomorphic: true,
|
||||
};
|
||||
}
|
||||
|
@ -55,10 +59,12 @@ export class Instances extends Component<any, InstancesState> {
|
|||
});
|
||||
}
|
||||
|
||||
static fetchInitialData(
|
||||
req: InitialFetchRequest
|
||||
): Promise<RequestState<any>>[] {
|
||||
return [req.client.getFederatedInstances({})];
|
||||
static async fetchInitialData({
|
||||
client,
|
||||
}: InitialFetchRequest): Promise<InstancesData> {
|
||||
return {
|
||||
federatedInstancesResponse: await client.getFederatedInstances({}),
|
||||
};
|
||||
}
|
||||
|
||||
get documentTitle(): string {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Component, linkEvent } from "inferno";
|
||||
import { Link } from "inferno-router";
|
||||
import { PersonView, Site, SiteAggregates } from "lemmy-js-client";
|
||||
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 { Icon } from "../common/icon";
|
||||
import { PersonListing } from "../person/person-listing";
|
||||
|
@ -71,7 +71,7 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
|
|||
<div>
|
||||
{site.description && <h6>{site.description}</h6>}
|
||||
{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)}
|
||||
</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) {
|
||||
i.setState({ collapsed: !i.state.collapsed });
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
GetModlog,
|
||||
GetModlogResponse,
|
||||
GetPersonDetails,
|
||||
GetPersonDetailsResponse,
|
||||
ModAddCommunityView,
|
||||
ModAddView,
|
||||
ModBanFromCommunityView,
|
||||
|
@ -74,6 +75,13 @@ type View =
|
|||
| AdminPurgePostView
|
||||
| AdminPurgeCommentView;
|
||||
|
||||
type ModlogData = RouteDataResponse<{
|
||||
res: GetModlogResponse;
|
||||
communityRes: GetCommunityResponse;
|
||||
modUserResponse: GetPersonDetailsResponse;
|
||||
userResponse: GetPersonDetailsResponse;
|
||||
}>;
|
||||
|
||||
interface ModlogType {
|
||||
id: number;
|
||||
type_: ModlogActionType;
|
||||
|
@ -631,7 +639,7 @@ export class Modlog extends Component<
|
|||
RouteComponentProps<{ communityId?: string }>,
|
||||
ModlogState
|
||||
> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<ModlogData>(this.context);
|
||||
|
||||
state: ModlogState = {
|
||||
res: { state: "empty" },
|
||||
|
@ -653,25 +661,26 @@ export class Modlog extends Component<
|
|||
|
||||
// Only fetch the data if coming from another route
|
||||
if (FirstLoadService.isFirstLoad) {
|
||||
const [res, communityRes, filteredModRes, filteredUserRes] =
|
||||
const { res, communityRes, modUserResponse, userResponse } =
|
||||
this.isoData.routeData;
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
res,
|
||||
communityRes,
|
||||
};
|
||||
|
||||
if (filteredModRes.state === "success") {
|
||||
if (modUserResponse.state === "success") {
|
||||
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,
|
||||
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,
|
||||
path,
|
||||
query: { modId: urlModId, page, userId: urlUserId, actionType },
|
||||
auth,
|
||||
site,
|
||||
}: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<
|
||||
RequestState<any>
|
||||
>[] {
|
||||
}: InitialFetchRequest<QueryParams<ModlogProps>>): Promise<ModlogData> {
|
||||
const pathSplit = path.split("/");
|
||||
const promises: Promise<RequestState<any>>[] = [];
|
||||
const communityId = getIdFromString(pathSplit[2]);
|
||||
const modId = !site.site_view.local_site.hide_modlog_mod_names
|
||||
? getIdFromString(urlModId)
|
||||
|
@ -985,40 +991,50 @@ export class Modlog extends Component<
|
|||
auth,
|
||||
};
|
||||
|
||||
promises.push(client.getModlog(modlogForm));
|
||||
let communityResponse: RequestState<GetCommunityResponse> = {
|
||||
state: "empty",
|
||||
};
|
||||
|
||||
if (communityId) {
|
||||
const communityForm: GetCommunity = {
|
||||
id: communityId,
|
||||
auth,
|
||||
};
|
||||
promises.push(client.getCommunity(communityForm));
|
||||
} else {
|
||||
promises.push(Promise.resolve({ state: "empty" }));
|
||||
|
||||
communityResponse = await client.getCommunity(communityForm);
|
||||
}
|
||||
|
||||
let modUserResponse: RequestState<GetPersonDetailsResponse> = {
|
||||
state: "empty",
|
||||
};
|
||||
|
||||
if (modId) {
|
||||
const getPersonForm: GetPersonDetails = {
|
||||
person_id: modId,
|
||||
auth,
|
||||
};
|
||||
|
||||
promises.push(client.getPersonDetails(getPersonForm));
|
||||
} else {
|
||||
promises.push(Promise.resolve({ state: "empty" }));
|
||||
modUserResponse = await client.getPersonDetails(getPersonForm);
|
||||
}
|
||||
|
||||
let userResponse: RequestState<GetPersonDetailsResponse> = {
|
||||
state: "empty",
|
||||
};
|
||||
|
||||
if (userId) {
|
||||
const getPersonForm: GetPersonDetails = {
|
||||
person_id: userId,
|
||||
auth,
|
||||
};
|
||||
|
||||
promises.push(client.getPersonDetails(getPersonForm));
|
||||
} else {
|
||||
promises.push(Promise.resolve({ state: "empty" }));
|
||||
userResponse = await client.getPersonDetails(getPersonForm);
|
||||
}
|
||||
|
||||
return promises;
|
||||
return {
|
||||
res: await client.getModlog(modlogForm),
|
||||
communityRes: communityResponse,
|
||||
modUserResponse,
|
||||
userResponse,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,10 +24,7 @@ import {
|
|||
DistinguishComment,
|
||||
EditComment,
|
||||
EditPrivateMessage,
|
||||
GetPersonMentions,
|
||||
GetPersonMentionsResponse,
|
||||
GetPrivateMessages,
|
||||
GetReplies,
|
||||
GetRepliesResponse,
|
||||
GetSiteResponse,
|
||||
MarkCommentReplyAsRead,
|
||||
|
@ -53,6 +50,7 @@ import { UserService } from "../../services";
|
|||
import { FirstLoadService } from "../../services/FirstLoadService";
|
||||
import { HttpService, RequestState } from "../../services/HttpService";
|
||||
import {
|
||||
RouteDataResponse,
|
||||
commentsToFlatNodes,
|
||||
editCommentReply,
|
||||
editMention,
|
||||
|
@ -92,6 +90,13 @@ enum ReplyEnum {
|
|||
Mention,
|
||||
Message,
|
||||
}
|
||||
|
||||
type InboxData = RouteDataResponse<{
|
||||
repliesRes: GetRepliesResponse;
|
||||
mentionsRes: GetPersonMentionsResponse;
|
||||
messagesRes: PrivateMessagesResponse;
|
||||
}>;
|
||||
|
||||
type ReplyType = {
|
||||
id: number;
|
||||
type_: ReplyEnum;
|
||||
|
@ -114,7 +119,7 @@ interface InboxState {
|
|||
}
|
||||
|
||||
export class Inbox extends Component<any, InboxState> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<InboxData>(this.context);
|
||||
state: InboxState = {
|
||||
unreadOrAll: UnreadOrAll.Unread,
|
||||
messageType: MessageType.All,
|
||||
|
@ -162,7 +167,7 @@ export class Inbox extends Component<any, InboxState> {
|
|||
|
||||
// Only fetch the data if coming from another route
|
||||
if (FirstLoadService.isFirstLoad) {
|
||||
const [repliesRes, mentionsRes, messagesRes] = this.isoData.routeData;
|
||||
const { mentionsRes, messagesRes, repliesRes } = this.isoData.routeData;
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
|
@ -686,50 +691,40 @@ export class Inbox extends Component<any, InboxState> {
|
|||
await i.refetch();
|
||||
}
|
||||
|
||||
static fetchInitialData({
|
||||
static async fetchInitialData({
|
||||
client,
|
||||
auth,
|
||||
}: InitialFetchRequest): Promise<any>[] {
|
||||
const promises: Promise<RequestState<any>>[] = [];
|
||||
|
||||
}: InitialFetchRequest): Promise<InboxData> {
|
||||
const sort: CommentSortType = "New";
|
||||
|
||||
if (auth) {
|
||||
// It can be /u/me, or /username/1
|
||||
const repliesForm: GetReplies = {
|
||||
return {
|
||||
mentionsRes: auth
|
||||
? await client.getPersonMentions({
|
||||
sort,
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: fetchLimit,
|
||||
auth,
|
||||
};
|
||||
promises.push(client.getReplies(repliesForm));
|
||||
|
||||
const personMentionsForm: GetPersonMentions = {
|
||||
})
|
||||
: { state: "empty" },
|
||||
messagesRes: auth
|
||||
? await client.getPrivateMessages({
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: fetchLimit,
|
||||
auth,
|
||||
})
|
||||
: { state: "empty" },
|
||||
repliesRes: auth
|
||||
? await client.getReplies({
|
||||
sort,
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: fetchLimit,
|
||||
auth,
|
||||
})
|
||||
: { state: "empty" },
|
||||
};
|
||||
promises.push(client.getPersonMentions(personMentionsForm));
|
||||
|
||||
const privateMessagesForm: GetPrivateMessages = {
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: fetchLimit,
|
||||
auth,
|
||||
};
|
||||
promises.push(client.getPrivateMessages(privateMessagesForm));
|
||||
} else {
|
||||
promises.push(
|
||||
Promise.resolve({ state: "empty" }),
|
||||
Promise.resolve({ state: "empty" }),
|
||||
Promise.resolve({ state: "empty" })
|
||||
);
|
||||
}
|
||||
|
||||
return promises;
|
||||
}
|
||||
|
||||
async refetch() {
|
||||
|
|
|
@ -107,16 +107,6 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
|
|||
setupTippy();
|
||||
}
|
||||
|
||||
// TODO wut?
|
||||
// componentDidUpdate(lastProps: UserDetailsProps) {
|
||||
// for (const key of Object.keys(lastProps)) {
|
||||
// if (lastProps[key] !== this.props[key]) {
|
||||
// this.fetchUserData();
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
|
|
|
@ -73,10 +73,14 @@ export class PersonListing extends Component<PersonListingProps, any> {
|
|||
const avatar = this.props.person.avatar;
|
||||
return (
|
||||
<>
|
||||
{avatar &&
|
||||
!this.props.hideAvatar &&
|
||||
{!this.props.hideAvatar &&
|
||||
!this.props.person.banned &&
|
||||
showAvatars() && <PictrsImage src={avatar} icon />}
|
||||
showAvatars() && (
|
||||
<PictrsImage
|
||||
src={avatar ?? "/static/assets/icons/icon-96x96.png"}
|
||||
icon
|
||||
/>
|
||||
)}
|
||||
<span>{displayName}</span>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -90,6 +90,10 @@ import { CommunityLink } from "../community/community-link";
|
|||
import { PersonDetails } from "./person-details";
|
||||
import { PersonListing } from "./person-listing";
|
||||
|
||||
type ProfileData = RouteDataResponse<{
|
||||
personResponse: GetPersonDetailsResponse;
|
||||
}>;
|
||||
|
||||
interface ProfileState {
|
||||
personRes: RequestState<GetPersonDetailsResponse>;
|
||||
personBlocked: boolean;
|
||||
|
@ -156,7 +160,7 @@ export class Profile extends Component<
|
|||
RouteComponentProps<{ username: string }>,
|
||||
ProfileState
|
||||
> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<ProfileData>(this.context);
|
||||
state: ProfileState = {
|
||||
personRes: { state: "empty" },
|
||||
personBlocked: false,
|
||||
|
@ -208,7 +212,7 @@ export class Profile extends Component<
|
|||
if (FirstLoadService.isFirstLoad) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
personRes: this.isoData.routeData[0],
|
||||
personRes: this.isoData.routeData.personResponse,
|
||||
isIsomorphic: true,
|
||||
};
|
||||
}
|
||||
|
@ -267,14 +271,12 @@ export class Profile extends Component<
|
|||
}
|
||||
}
|
||||
|
||||
static fetchInitialData({
|
||||
static async fetchInitialData({
|
||||
client,
|
||||
path,
|
||||
query: { page, sort, view: urlView },
|
||||
auth,
|
||||
}: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<
|
||||
RequestState<any>
|
||||
>[] {
|
||||
}: InitialFetchRequest<QueryParams<ProfileProps>>): Promise<ProfileData> {
|
||||
const pathSplit = path.split("/");
|
||||
|
||||
const username = pathSplit[2];
|
||||
|
@ -289,7 +291,9 @@ export class Profile extends Component<
|
|||
auth,
|
||||
};
|
||||
|
||||
return [client.getPersonDetails(form)];
|
||||
return {
|
||||
personResponse: await client.getPersonDetails(form),
|
||||
};
|
||||
}
|
||||
|
||||
get documentTitle(): string {
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Component, linkEvent } from "inferno";
|
|||
import {
|
||||
ApproveRegistrationApplication,
|
||||
GetSiteResponse,
|
||||
ListRegistrationApplications,
|
||||
ListRegistrationApplicationsResponse,
|
||||
RegistrationApplicationView,
|
||||
} from "lemmy-js-client";
|
||||
|
@ -12,6 +11,7 @@ import { UserService } from "../../services";
|
|||
import { FirstLoadService } from "../../services/FirstLoadService";
|
||||
import { HttpService, RequestState } from "../../services/HttpService";
|
||||
import {
|
||||
RouteDataResponse,
|
||||
editRegistrationApplication,
|
||||
fetchLimit,
|
||||
myAuthRequired,
|
||||
|
@ -28,6 +28,10 @@ enum UnreadOrAll {
|
|||
All,
|
||||
}
|
||||
|
||||
type RegistrationApplicationsData = RouteDataResponse<{
|
||||
listRegistrationApplicationsResponse: ListRegistrationApplicationsResponse;
|
||||
}>;
|
||||
|
||||
interface RegistrationApplicationsState {
|
||||
appsRes: RequestState<ListRegistrationApplicationsResponse>;
|
||||
siteRes: GetSiteResponse;
|
||||
|
@ -40,7 +44,7 @@ export class RegistrationApplications extends Component<
|
|||
any,
|
||||
RegistrationApplicationsState
|
||||
> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<RegistrationApplicationsData>(this.context);
|
||||
state: RegistrationApplicationsState = {
|
||||
appsRes: { state: "empty" },
|
||||
siteRes: this.isoData.site_res,
|
||||
|
@ -59,7 +63,7 @@ export class RegistrationApplications extends Component<
|
|||
if (FirstLoadService.isFirstLoad) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
appsRes: this.isoData.routeData[0],
|
||||
appsRes: this.isoData.routeData.listRegistrationApplicationsResponse,
|
||||
isIsomorphic: true,
|
||||
};
|
||||
}
|
||||
|
@ -184,25 +188,20 @@ export class RegistrationApplications extends Component<
|
|||
this.refetch();
|
||||
}
|
||||
|
||||
static fetchInitialData({
|
||||
static async fetchInitialData({
|
||||
auth,
|
||||
client,
|
||||
}: InitialFetchRequest): Promise<any>[] {
|
||||
const promises: Promise<RequestState<any>>[] = [];
|
||||
|
||||
if (auth) {
|
||||
const form: ListRegistrationApplications = {
|
||||
}: InitialFetchRequest): Promise<RegistrationApplicationsData> {
|
||||
return {
|
||||
listRegistrationApplicationsResponse: auth
|
||||
? await client.listRegistrationApplications({
|
||||
unread_only: true,
|
||||
page: 1,
|
||||
limit: fetchLimit,
|
||||
auth,
|
||||
auth: auth as string,
|
||||
})
|
||||
: { state: "empty" },
|
||||
};
|
||||
promises.push(client.listRegistrationApplications(form));
|
||||
} else {
|
||||
promises.push(Promise.resolve({ state: "empty" }));
|
||||
}
|
||||
|
||||
return promises;
|
||||
}
|
||||
|
||||
async refetch() {
|
||||
|
|
|
@ -23,6 +23,7 @@ import { HttpService, UserService } from "../../services";
|
|||
import { FirstLoadService } from "../../services/FirstLoadService";
|
||||
import { RequestState } from "../../services/HttpService";
|
||||
import {
|
||||
RouteDataResponse,
|
||||
editCommentReport,
|
||||
editPostReport,
|
||||
editPrivateMessageReport,
|
||||
|
@ -56,6 +57,12 @@ enum MessageEnum {
|
|||
PrivateMessageReport,
|
||||
}
|
||||
|
||||
type ReportsData = RouteDataResponse<{
|
||||
commentReportsRes: ListCommentReportsResponse;
|
||||
postReportsRes: ListPostReportsResponse;
|
||||
messageReportsRes: ListPrivateMessageReportsResponse;
|
||||
}>;
|
||||
|
||||
type ItemType = {
|
||||
id: number;
|
||||
type_: MessageEnum;
|
||||
|
@ -75,7 +82,7 @@ interface ReportsState {
|
|||
}
|
||||
|
||||
export class Reports extends Component<any, ReportsState> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<ReportsData>(this.context);
|
||||
state: ReportsState = {
|
||||
commentReportsRes: { 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
|
||||
if (FirstLoadService.isFirstLoad) {
|
||||
const [commentReportsRes, postReportsRes, messageReportsRes] =
|
||||
const { commentReportsRes, postReportsRes, messageReportsRes } =
|
||||
this.isoData.routeData;
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
commentReportsRes,
|
||||
|
@ -111,7 +119,7 @@ export class Reports extends Component<any, ReportsState> {
|
|||
if (amAdmin()) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
messageReportsRes,
|
||||
messageReportsRes: messageReportsRes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -481,55 +489,48 @@ export class Reports extends Component<any, ReportsState> {
|
|||
await i.refetch();
|
||||
}
|
||||
|
||||
static fetchInitialData({
|
||||
static async fetchInitialData({
|
||||
auth,
|
||||
client,
|
||||
}: InitialFetchRequest): Promise<any>[] {
|
||||
const promises: Promise<RequestState<any>>[] = [];
|
||||
|
||||
}: InitialFetchRequest): Promise<ReportsData> {
|
||||
const unresolved_only = true;
|
||||
const page = 1;
|
||||
const limit = fetchLimit;
|
||||
|
||||
if (auth) {
|
||||
const commentReportsForm: ListCommentReports = {
|
||||
unresolved_only,
|
||||
page,
|
||||
limit,
|
||||
auth,
|
||||
auth: auth as string,
|
||||
};
|
||||
promises.push(client.listCommentReports(commentReportsForm));
|
||||
|
||||
const postReportsForm: ListPostReports = {
|
||||
unresolved_only,
|
||||
page,
|
||||
limit,
|
||||
auth,
|
||||
auth: auth as string,
|
||||
};
|
||||
|
||||
const data: ReportsData = {
|
||||
commentReportsRes: await client.listCommentReports(commentReportsForm),
|
||||
postReportsRes: await client.listPostReports(postReportsForm),
|
||||
messageReportsRes: { state: "empty" },
|
||||
};
|
||||
promises.push(client.listPostReports(postReportsForm));
|
||||
|
||||
if (amAdmin()) {
|
||||
const privateMessageReportsForm: ListPrivateMessageReports = {
|
||||
unresolved_only,
|
||||
page,
|
||||
limit,
|
||||
auth,
|
||||
auth: auth as string,
|
||||
};
|
||||
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" })
|
||||
|
||||
data.messageReportsRes = await client.listPrivateMessageReports(
|
||||
privateMessageReportsForm
|
||||
);
|
||||
}
|
||||
|
||||
return promises;
|
||||
return data;
|
||||
}
|
||||
|
||||
async refetch() {
|
||||
|
|
|
@ -3,6 +3,7 @@ import { RouteComponentProps } from "inferno-router/dist/Route";
|
|||
import {
|
||||
CreatePost as CreatePostI,
|
||||
GetCommunity,
|
||||
GetCommunityResponse,
|
||||
GetSiteResponse,
|
||||
ListCommunitiesResponse,
|
||||
} from "lemmy-js-client";
|
||||
|
@ -16,6 +17,7 @@ import {
|
|||
} from "../../services/HttpService";
|
||||
import {
|
||||
Choice,
|
||||
RouteDataResponse,
|
||||
enableDownvotes,
|
||||
enableNsfw,
|
||||
getIdFromString,
|
||||
|
@ -32,6 +34,11 @@ export interface CreatePostProps {
|
|||
communityId?: number;
|
||||
}
|
||||
|
||||
type CreatePostData = RouteDataResponse<{
|
||||
communityResponse: GetCommunityResponse;
|
||||
initialCommunitiesRes: ListCommunitiesResponse;
|
||||
}>;
|
||||
|
||||
function getCreatePostQueryParams() {
|
||||
return getQueryParams<CreatePostProps>({
|
||||
communityId: getIdFromString,
|
||||
|
@ -54,7 +61,7 @@ export class CreatePost extends Component<
|
|||
RouteComponentProps<Record<string, never>>,
|
||||
CreatePostState
|
||||
> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<CreatePostData>(this.context);
|
||||
state: CreatePostState = {
|
||||
siteRes: this.isoData.site_res,
|
||||
loading: true,
|
||||
|
@ -71,7 +78,15 @@ export class CreatePost extends Component<
|
|||
|
||||
// Only fetch the data if coming from another route
|
||||
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") {
|
||||
const communityChoice: Choice = {
|
||||
|
@ -84,13 +99,6 @@ export class CreatePost extends Component<
|
|||
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,
|
||||
query: { communityId },
|
||||
auth,
|
||||
}: InitialFetchRequest<QueryParams<CreatePostProps>>): Promise<
|
||||
RequestState<any>
|
||||
>[] {
|
||||
const promises: Promise<RequestState<any>>[] = [];
|
||||
}: InitialFetchRequest<
|
||||
QueryParams<CreatePostProps>
|
||||
>): Promise<CreatePostData> {
|
||||
const data: CreatePostData = {
|
||||
initialCommunitiesRes: await fetchCommunitiesForOptions(client),
|
||||
communityResponse: { state: "empty" },
|
||||
};
|
||||
|
||||
if (communityId) {
|
||||
const form: GetCommunity = {
|
||||
|
@ -249,13 +260,9 @@ export class CreatePost extends Component<
|
|||
id: getIdFromString(communityId),
|
||||
};
|
||||
|
||||
promises.push(client.getCommunity(form));
|
||||
} else {
|
||||
promises.push(Promise.resolve({ state: "empty" }));
|
||||
data.communityResponse = await client.getCommunity(form);
|
||||
}
|
||||
|
||||
promises.push(fetchCommunitiesForOptions(client));
|
||||
|
||||
return promises;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -631,7 +631,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
const post = this.postView.post;
|
||||
|
||||
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}
|
||||
{canShare() && (
|
||||
<button
|
||||
|
|
|
@ -75,6 +75,7 @@ import {
|
|||
isImage,
|
||||
myAuth,
|
||||
restoreScrollPosition,
|
||||
RouteDataResponse,
|
||||
saveScrollPosition,
|
||||
setIsoData,
|
||||
setupTippy,
|
||||
|
@ -93,6 +94,11 @@ import { PostListing } from "./post-listing";
|
|||
|
||||
const commentsShownInterval = 15;
|
||||
|
||||
type PostData = RouteDataResponse<{
|
||||
postRes: GetPostResponse;
|
||||
commentsRes: GetCommentsResponse;
|
||||
}>;
|
||||
|
||||
interface PostState {
|
||||
postId?: number;
|
||||
commentId?: number;
|
||||
|
@ -110,7 +116,7 @@ interface PostState {
|
|||
}
|
||||
|
||||
export class Post extends Component<any, PostState> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<PostData>(this.context);
|
||||
private commentScrollDebounced: () => void;
|
||||
state: PostState = {
|
||||
postRes: { state: "empty" },
|
||||
|
@ -169,7 +175,7 @@ export class Post extends Component<any, PostState> {
|
|||
|
||||
// Only fetch the data if coming from another route
|
||||
if (FirstLoadService.isFirstLoad) {
|
||||
const [postRes, commentsRes] = this.isoData.routeData;
|
||||
const { commentsRes, postRes } = this.isoData.routeData;
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
|
@ -220,13 +226,12 @@ export class Post extends Component<any, PostState> {
|
|||
}
|
||||
}
|
||||
|
||||
static fetchInitialData({
|
||||
auth,
|
||||
static async fetchInitialData({
|
||||
client,
|
||||
path,
|
||||
}: InitialFetchRequest): Promise<any>[] {
|
||||
auth,
|
||||
}: InitialFetchRequest): Promise<PostData> {
|
||||
const pathSplit = path.split("/");
|
||||
const promises: Promise<RequestState<any>>[] = [];
|
||||
|
||||
const pathType = pathSplit.at(1);
|
||||
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;
|
||||
}
|
||||
|
||||
promises.push(client.getPost(postForm));
|
||||
promises.push(client.getComments(commentsForm));
|
||||
|
||||
return promises;
|
||||
return {
|
||||
postRes: await client.getPost(postForm),
|
||||
commentsRes: await client.getComments(commentsForm),
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { InitialFetchRequest } from "../../interfaces";
|
|||
import { FirstLoadService } from "../../services/FirstLoadService";
|
||||
import { HttpService, RequestState } from "../../services/HttpService";
|
||||
import {
|
||||
RouteDataResponse,
|
||||
getRecipientIdFromProps,
|
||||
myAuth,
|
||||
setIsoData,
|
||||
|
@ -19,6 +20,10 @@ import { HtmlTags } from "../common/html-tags";
|
|||
import { Spinner } from "../common/icon";
|
||||
import { PrivateMessageForm } from "./private-message-form";
|
||||
|
||||
type CreatePrivateMessageData = RouteDataResponse<{
|
||||
recipientDetailsResponse: GetPersonDetailsResponse;
|
||||
}>;
|
||||
|
||||
interface CreatePrivateMessageState {
|
||||
siteRes: GetSiteResponse;
|
||||
recipientRes: RequestState<GetPersonDetailsResponse>;
|
||||
|
@ -30,7 +35,7 @@ export class CreatePrivateMessage extends Component<
|
|||
any,
|
||||
CreatePrivateMessageState
|
||||
> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<CreatePrivateMessageData>(this.context);
|
||||
state: CreatePrivateMessageState = {
|
||||
siteRes: this.isoData.site_res,
|
||||
recipientRes: { state: "empty" },
|
||||
|
@ -47,7 +52,7 @@ export class CreatePrivateMessage extends Component<
|
|||
if (FirstLoadService.isFirstLoad) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
recipientRes: this.isoData.routeData[0],
|
||||
recipientRes: this.isoData.routeData.recipientDetailsResponse,
|
||||
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() {
|
||||
this.setState({
|
||||
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 {
|
||||
if (this.state.recipientRes.state == "success") {
|
||||
const name_ = this.state.recipientRes.data.person_view.person.name;
|
||||
|
|
|
@ -26,6 +26,7 @@ import { FirstLoadService } from "../services/FirstLoadService";
|
|||
import { HttpService, RequestState } from "../services/HttpService";
|
||||
import {
|
||||
Choice,
|
||||
RouteDataResponse,
|
||||
capitalizeFirstLetter,
|
||||
commentsToFlatNodes,
|
||||
communityToChoice,
|
||||
|
@ -70,6 +71,14 @@ interface SearchProps {
|
|||
page: number;
|
||||
}
|
||||
|
||||
type SearchData = RouteDataResponse<{
|
||||
communityResponse: GetCommunityResponse;
|
||||
listCommunitiesResponse: ListCommunitiesResponse;
|
||||
creatorDetailsResponse: GetPersonDetailsResponse;
|
||||
searchResponse: SearchResponse;
|
||||
resolveObjectResponse: ResolveObjectResponse;
|
||||
}>;
|
||||
|
||||
type FilterType = "creator" | "community";
|
||||
|
||||
interface SearchState {
|
||||
|
@ -228,7 +237,8 @@ function getListing(
|
|||
}
|
||||
|
||||
export class Search extends Component<any, SearchState> {
|
||||
private isoData = setIsoData(this.context);
|
||||
private isoData = setIsoData<SearchData>(this.context);
|
||||
|
||||
state: SearchState = {
|
||||
resolveObjectRes: { state: "empty" },
|
||||
creatorDetailsRes: { state: "empty" },
|
||||
|
@ -262,44 +272,65 @@ export class Search extends Component<any, SearchState> {
|
|||
|
||||
// Only fetch the data if coming from another route
|
||||
if (FirstLoadService.isFirstLoad) {
|
||||
const [
|
||||
communityRes,
|
||||
communitiesRes,
|
||||
creatorDetailsRes,
|
||||
searchRes,
|
||||
resolveObjectRes,
|
||||
] = this.isoData.routeData;
|
||||
const {
|
||||
communityResponse: communityRes,
|
||||
creatorDetailsResponse: creatorDetailsRes,
|
||||
listCommunitiesResponse: communitiesRes,
|
||||
resolveObjectResponse: resolveObjectRes,
|
||||
searchResponse: searchRes,
|
||||
} = this.isoData.routeData;
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
communitiesRes,
|
||||
communityRes,
|
||||
creatorDetailsRes,
|
||||
creatorSearchOptions:
|
||||
creatorDetailsRes.state == "success"
|
||||
? [personToChoice(creatorDetailsRes.data.person_view)]
|
||||
: [],
|
||||
isIsomorphic: true,
|
||||
};
|
||||
|
||||
if (communityRes.state === "success") {
|
||||
if (creatorDetailsRes?.state === "success") {
|
||||
this.state = {
|
||||
...this.state,
|
||||
communitySearchOptions: [
|
||||
communityToChoice(communityRes.data.community_view),
|
||||
],
|
||||
creatorSearchOptions:
|
||||
creatorDetailsRes?.state === "success"
|
||||
? [personToChoice(creatorDetailsRes.data.person_view)]
|
||||
: [],
|
||||
creatorDetailsRes,
|
||||
};
|
||||
}
|
||||
|
||||
if (q) {
|
||||
if (communitiesRes?.state === "success") {
|
||||
this.state = {
|
||||
...this.state,
|
||||
communitiesRes,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (!this.state.isIsomorphic) {
|
||||
|
@ -328,23 +359,25 @@ export class Search extends Component<any, SearchState> {
|
|||
saveScrollPosition(this.context);
|
||||
}
|
||||
|
||||
static fetchInitialData({
|
||||
static async fetchInitialData({
|
||||
client,
|
||||
auth,
|
||||
query: { communityId, creatorId, q, type, sort, listingType, page },
|
||||
}: InitialFetchRequest<QueryParams<SearchProps>>): Promise<
|
||||
RequestState<any>
|
||||
>[] {
|
||||
const promises: Promise<RequestState<any>>[] = [];
|
||||
|
||||
}: InitialFetchRequest<QueryParams<SearchProps>>): Promise<SearchData> {
|
||||
const community_id = getIdFromString(communityId);
|
||||
let communityResponse: RequestState<GetCommunityResponse> = {
|
||||
state: "empty",
|
||||
};
|
||||
let listCommunitiesResponse: RequestState<ListCommunitiesResponse> = {
|
||||
state: "empty",
|
||||
};
|
||||
if (community_id) {
|
||||
const getCommunityForm: GetCommunity = {
|
||||
id: community_id,
|
||||
auth,
|
||||
};
|
||||
promises.push(client.getCommunity(getCommunityForm));
|
||||
promises.push(Promise.resolve({ state: "empty" }));
|
||||
|
||||
communityResponse = await client.getCommunity(getCommunityForm);
|
||||
} else {
|
||||
const listCommunitiesForm: ListCommunities = {
|
||||
type_: defaultListingType,
|
||||
|
@ -352,23 +385,32 @@ export class Search extends Component<any, SearchState> {
|
|||
limit: fetchLimit,
|
||||
auth,
|
||||
};
|
||||
promises.push(Promise.resolve({ state: "empty" }));
|
||||
promises.push(client.listCommunities(listCommunitiesForm));
|
||||
|
||||
listCommunitiesResponse = await client.listCommunities(
|
||||
listCommunitiesForm
|
||||
);
|
||||
}
|
||||
|
||||
const creator_id = getIdFromString(creatorId);
|
||||
let creatorDetailsResponse: RequestState<GetPersonDetailsResponse> = {
|
||||
state: "empty",
|
||||
};
|
||||
if (creator_id) {
|
||||
const getCreatorForm: GetPersonDetails = {
|
||||
person_id: creator_id,
|
||||
auth,
|
||||
};
|
||||
promises.push(client.getPersonDetails(getCreatorForm));
|
||||
} else {
|
||||
promises.push(Promise.resolve({ state: "empty" }));
|
||||
|
||||
creatorDetailsResponse = await client.getPersonDetails(getCreatorForm);
|
||||
}
|
||||
|
||||
const query = getSearchQueryFromQuery(q);
|
||||
|
||||
let searchResponse: RequestState<SearchResponse> = { state: "empty" };
|
||||
let resolveObjectResponse: RequestState<ResolveObjectResponse> = {
|
||||
state: "empty",
|
||||
};
|
||||
|
||||
if (query) {
|
||||
const form: SearchForm = {
|
||||
q: query,
|
||||
|
@ -383,21 +425,24 @@ export class Search extends Component<any, SearchState> {
|
|||
};
|
||||
|
||||
if (query !== "") {
|
||||
promises.push(client.search(form));
|
||||
searchResponse = await client.search(form);
|
||||
if (auth) {
|
||||
const resolveObjectForm: ResolveObject = {
|
||||
q: query,
|
||||
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 {
|
||||
|
@ -463,7 +508,7 @@ export class Search extends Component<any, SearchState> {
|
|||
minLength={1}
|
||||
/>
|
||||
<button type="submit" className="btn btn-secondary mr-2 mb-2">
|
||||
{this.state.searchRes.state == "loading" ? (
|
||||
{this.state.searchRes.state === "loading" ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<span>{i18n.t("search")}</span>
|
||||
|
|
|
@ -6,15 +6,17 @@ import { ErrorPageData } from "./utils";
|
|||
/**
|
||||
* This contains serialized data, it needs to be deserialized before use.
|
||||
*/
|
||||
export interface IsoData {
|
||||
export interface IsoData<T extends RouteData = any> {
|
||||
path: string;
|
||||
routeData: RequestState<any>[];
|
||||
routeData: T;
|
||||
site_res: GetSiteResponse;
|
||||
errorPageData?: ErrorPageData;
|
||||
}
|
||||
|
||||
export type IsoDataOptionalSite = Partial<IsoData> &
|
||||
Pick<IsoData, Exclude<keyof IsoData, "site_res">>;
|
||||
export type IsoDataOptionalSite<T extends RouteData = any> = Partial<
|
||||
IsoData<T>
|
||||
> &
|
||||
Pick<IsoData<T>, Exclude<keyof IsoData<T>, "site_res">>;
|
||||
|
||||
export interface ILemmyConfig {
|
||||
wsHost?: string;
|
||||
|
@ -80,3 +82,5 @@ export interface CommentNodeI {
|
|||
children: Array<CommentNodeI>;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
export type RouteData = Record<string, RequestState<any>>;
|
||||
|
|
|
@ -21,15 +21,13 @@ import { CreatePost } from "./components/post/create-post";
|
|||
import { Post } from "./components/post/post";
|
||||
import { CreatePrivateMessage } from "./components/private_message/create-private-message";
|
||||
import { Search } from "./components/search";
|
||||
import { InitialFetchRequest } from "./interfaces";
|
||||
import { RequestState } from "./services/HttpService";
|
||||
import { InitialFetchRequest, RouteData } from "./interfaces";
|
||||
|
||||
interface IRoutePropsWithFetch extends IRouteProps {
|
||||
// TODO Make sure this one is good.
|
||||
fetchInitialData?(req: InitialFetchRequest): Promise<RequestState<any>>[];
|
||||
interface IRoutePropsWithFetch<T extends RouteData> extends IRouteProps {
|
||||
fetchInitialData?(req: InitialFetchRequest): Promise<T>;
|
||||
}
|
||||
|
||||
export const routes: IRoutePropsWithFetch[] = [
|
||||
export const routes: IRoutePropsWithFetch<Record<string, any>>[] = [
|
||||
{
|
||||
path: `/`,
|
||||
component: Home,
|
||||
|
|
|
@ -11,7 +11,7 @@ type LoadingRequestState = {
|
|||
state: "loading";
|
||||
};
|
||||
|
||||
type FailedRequestState = {
|
||||
export type FailedRequestState = {
|
||||
state: "failed";
|
||||
msg: string;
|
||||
};
|
||||
|
@ -58,7 +58,7 @@ class WrappedLemmyHttpClient {
|
|||
|
||||
return {
|
||||
data: res,
|
||||
state: "success",
|
||||
state: !(res === undefined || res === null) ? "success" : "empty",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`API error: ${error}`);
|
||||
|
|
|
@ -41,11 +41,18 @@ import tippy from "tippy.js";
|
|||
import Toastify from "toastify-js";
|
||||
import { getHttpBase } from "./env";
|
||||
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 { isBrowser } from "./utils/browser/is-browser";
|
||||
import { debounce } from "./utils/helpers/debounce";
|
||||
import { groupBy } from "./utils/helpers/group-by";
|
||||
import { RequestState } from "./services/HttpService";
|
||||
|
||||
let Tribute: any;
|
||||
if (isBrowser()) {
|
||||
|
@ -242,7 +249,12 @@ export function isVideo(url: string) {
|
|||
}
|
||||
|
||||
export function validURL(str: string) {
|
||||
return !!new URL(str);
|
||||
try {
|
||||
new URL(str);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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 (isBrowser()) {
|
||||
return window.isoData;
|
||||
|
@ -1264,3 +1276,7 @@ export function newVote(voteType: VoteType, myVote?: number): number {
|
|||
return myVote == -1 ? 0 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
export type RouteDataResponse<T extends Record<string, any>> = {
|
||||
[K in keyof T]: RequestState<T[K]>;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue