mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-22 04:11:12 +00:00
Merge branch 'main' into vote_display_mode
This commit is contained in:
commit
54b889a053
25 changed files with 6495 additions and 4522 deletions
|
@ -49,6 +49,7 @@
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error",
|
||||||
"quote-props": 0,
|
"quote-props": 0,
|
||||||
"unicorn/filename-case": 0,
|
"unicorn/filename-case": 0,
|
||||||
"jsx-a11y/media-has-caption": 0
|
"jsx-a11y/media-has-caption": 0,
|
||||||
|
"jsx-a11y/label-has-associated-control": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit e78c744abe5c3824f9ca7de7f1ee086468385ee0
|
Subproject commit c88dd1e3b36ee1617f1b86acf94c1b7946e97cd4
|
|
@ -42,6 +42,7 @@
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"cookie": "^0.6.0",
|
"cookie": "^0.6.0",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
"css-loader": "^6.10.0",
|
"css-loader": "^6.10.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
@ -60,7 +61,7 @@
|
||||||
"inferno-router": "^8.2.3",
|
"inferno-router": "^8.2.3",
|
||||||
"inferno-server": "^8.2.3",
|
"inferno-server": "^8.2.3",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lemmy-js-client": "0.19.4-alpha.16",
|
"lemmy-js-client": "0.19.4-alpha.18",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"markdown-it-bidi": "^0.1.0",
|
"markdown-it-bidi": "^0.1.0",
|
||||||
|
@ -94,6 +95,7 @@
|
||||||
"@types/autosize": "^4.0.3",
|
"@types/autosize": "^4.0.3",
|
||||||
"@types/bootstrap": "^5.2.10",
|
"@types/bootstrap": "^5.2.10",
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie": "^0.6.0",
|
||||||
|
"@types/cookie-parser": "^1.4.7",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/html-to-text": "^9.0.4",
|
"@types/html-to-text": "^9.0.4",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
|
@ -139,7 +141,7 @@
|
||||||
"sortpack"
|
"sortpack"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.14.3",
|
"packageManager": "pnpm@9.0.1+sha256.46d50ee2afecb42b185ebbd662dc7bdd52ef5be56bf035bb615cab81a75345df",
|
||||||
"engineStrict": true,
|
"engineStrict": true,
|
||||||
"importSort": {
|
"importSort": {
|
||||||
".js, .jsx, .ts, .tsx": {
|
".js, .jsx, .ts, .tsx": {
|
||||||
|
|
10388
pnpm-lock.yaml
10388
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -292,5 +292,8 @@
|
||||||
<line x1="16" y1="216" x2="240" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
<line x1="16" y1="216" x2="240" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
||||||
<path d="M152,216V160a8,8,0,0,0-8-8H112a8,8,0,0,0-8,8v56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
<path d="M152,216V160a8,8,0,0,0-8-8H112a8,8,0,0,0-8,8v56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol id="icon-info" fill="currentColor" viewBox="0 0 256 256">
|
||||||
|
<path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z" />
|
||||||
|
</symbol>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 69 KiB |
|
@ -1,7 +1,7 @@
|
||||||
import { initializeSite } from "@utils/app";
|
import { initializeSite } from "@utils/app";
|
||||||
import { hydrate } from "inferno-hydrate";
|
import { hydrate } from "inferno-hydrate";
|
||||||
import { BrowserRouter } from "inferno-router";
|
import { BrowserRouter } from "inferno-router";
|
||||||
import { App } from "../shared/components/app/app";
|
import App from "../shared/components/app/app";
|
||||||
import { lazyHighlightjs } from "../shared/lazy-highlightjs";
|
import { lazyHighlightjs } from "../shared/lazy-highlightjs";
|
||||||
import { loadUserLanguage } from "../shared/services/I18NextService";
|
import { loadUserLanguage } from "../shared/services/I18NextService";
|
||||||
import { verifyDynamicImports } from "../shared/dynamic-imports";
|
import { verifyDynamicImports } from "../shared/dynamic-imports";
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { StaticRouter, matchPath } from "inferno-router";
|
||||||
import { Match } from "inferno-router/dist/Route";
|
import { Match } from "inferno-router/dist/Route";
|
||||||
import { renderToString } from "inferno-server";
|
import { renderToString } from "inferno-server";
|
||||||
import { GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
import { GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
||||||
import { App } from "../../shared/components/app/app";
|
import App from "../../shared/components/app/app";
|
||||||
import {
|
import {
|
||||||
InitialFetchRequest,
|
InitialFetchRequest,
|
||||||
IsoDataOptionalSite,
|
IsoDataOptionalSite,
|
||||||
|
@ -28,6 +28,7 @@ import {
|
||||||
} from "../../shared/services/";
|
} from "../../shared/services/";
|
||||||
import { parsePath } from "history";
|
import { parsePath } from "history";
|
||||||
import { getQueryString } from "@utils/helpers";
|
import { getQueryString } from "@utils/helpers";
|
||||||
|
import { adultConsentCookieKey } from "../../shared/config";
|
||||||
|
|
||||||
export default async (req: Request, res: Response) => {
|
export default async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
@ -142,6 +143,9 @@ export default async (req: Request, res: Response) => {
|
||||||
site_res: site,
|
site_res: site,
|
||||||
routeData,
|
routeData,
|
||||||
errorPageData,
|
errorPageData,
|
||||||
|
showAdultConsentModal:
|
||||||
|
!!site?.site_view.site.content_warning &&
|
||||||
|
!(site.my_user || req.cookies[adultConsentCookieKey]),
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = (
|
const wrapper = (
|
||||||
|
|
|
@ -14,8 +14,10 @@ import ThemesListHandler from "./handlers/themes-list-handler";
|
||||||
import { setCacheControl, setDefaultCsp } from "./middleware";
|
import { setCacheControl, setDefaultCsp } from "./middleware";
|
||||||
import CodeThemeHandler from "./handlers/code-theme-handler";
|
import CodeThemeHandler from "./handlers/code-theme-handler";
|
||||||
import { verifyDynamicImports } from "../shared/dynamic-imports";
|
import { verifyDynamicImports } from "../shared/dynamic-imports";
|
||||||
|
import cookieParser from "cookie-parser";
|
||||||
|
|
||||||
const server = express();
|
const server = express();
|
||||||
|
server.use(cookieParser());
|
||||||
|
|
||||||
const [hostname, port] = process.env["LEMMY_UI_HOST"]
|
const [hostname, port] = process.env["LEMMY_UI_HOST"]
|
||||||
? process.env["LEMMY_UI_HOST"].split(":")
|
? process.env["LEMMY_UI_HOST"].split(":")
|
||||||
|
|
|
@ -80,13 +80,14 @@ export async function createSsrHtml(
|
||||||
<html ${helmet.htmlAttributes.toString()}>
|
<html ${helmet.htmlAttributes.toString()}>
|
||||||
<head>
|
<head>
|
||||||
<script nonce="${cspNonce}">
|
<script nonce="${cspNonce}">
|
||||||
|
window.isoData = ${serialize(isoData)};
|
||||||
|
|
||||||
if (!document.documentElement.hasAttribute("data-bs-theme")) {
|
if (!document.documentElement.hasAttribute("data-bs-theme")) {
|
||||||
const light = window.matchMedia("(prefers-color-scheme: light)").matches;
|
const light = window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||||
document.documentElement.setAttribute("data-bs-theme", light ? "light" : "dark");
|
document.documentElement.setAttribute("data-bs-theme", light ? "light" : "dark");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
${lazyScripts}
|
${lazyScripts}
|
||||||
<script nonce="${cspNonce}">window.isoData = ${serialize(isoData)}</script>
|
|
||||||
|
|
||||||
<!-- A remote debugging utility for mobile -->
|
<!-- A remote debugging utility for mobile -->
|
||||||
${erudaStr}
|
${erudaStr}
|
||||||
|
@ -97,6 +98,16 @@ export async function createSsrHtml(
|
||||||
${helmet.title.toString()}
|
${helmet.title.toString()}
|
||||||
${helmet.meta.toString()}
|
${helmet.meta.toString()}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#app[data-adult-consent] {
|
||||||
|
filter: blur(10px);
|
||||||
|
-webkit-filter: blur(10px);
|
||||||
|
-moz-filter: blur(10px);
|
||||||
|
-o-filter: blur(10px);
|
||||||
|
-ms-filter: blur(10px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- Required meta tags -->
|
<!-- Required meta tags -->
|
||||||
<meta name="Description" content="Lemmy">
|
<meta name="Description" content="Lemmy">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { isAnonymousPath, isAuthPath, setIsoData } from "@utils/app";
|
import { isAnonymousPath, isAuthPath, setIsoData } from "@utils/app";
|
||||||
import { Component, RefObject, createRef, linkEvent } from "inferno";
|
import { Component, createRef, linkEvent } from "inferno";
|
||||||
import { Provider } from "inferno-i18next-dess";
|
import { Provider } from "inferno-i18next-dess";
|
||||||
import { Route, Switch } from "inferno-router";
|
import { Route, Switch } from "inferno-router";
|
||||||
import { IsoDataOptionalSite } from "../../interfaces";
|
import { IsoDataOptionalSite } from "../../interfaces";
|
||||||
|
@ -13,42 +13,39 @@ import { Navbar } from "./navbar";
|
||||||
import "./styles.scss";
|
import "./styles.scss";
|
||||||
import { Theme } from "./theme";
|
import { Theme } from "./theme";
|
||||||
import AnonymousGuard from "../common/anonymous-guard";
|
import AnonymousGuard from "../common/anonymous-guard";
|
||||||
import { destroyTippy, setupTippy } from "../../tippy";
|
import AdultConsentModal from "../common/adult-consent-modal";
|
||||||
|
|
||||||
export class App extends Component<any, any> {
|
function handleJumpToContent(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class App extends Component<any, any> {
|
||||||
private isoData: IsoDataOptionalSite = setIsoData(this.context);
|
private isoData: IsoDataOptionalSite = setIsoData(this.context);
|
||||||
private readonly mainContentRef: RefObject<HTMLElement>;
|
|
||||||
private readonly rootRef = createRef<HTMLDivElement>();
|
private readonly rootRef = createRef<HTMLDivElement>();
|
||||||
constructor(props: any, context: any) {
|
|
||||||
super(props, context);
|
|
||||||
this.mainContentRef = createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
setupTippy(this.rootRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
|
||||||
destroyTippy();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleJumpToContent(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.mainContentRef.current?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const siteRes = this.isoData.site_res;
|
const siteRes = this.isoData.site_res;
|
||||||
const siteView = siteRes?.site_view;
|
const siteView = siteRes?.site_view;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Provider i18next={I18NextService.i18n}>
|
||||||
<Provider i18next={I18NextService.i18n}>
|
{/* This fragment is required to avoid an SSR error*/}
|
||||||
<div id="app" className="lemmy-site" ref={this.rootRef}>
|
<>
|
||||||
|
{this.isoData.showAdultConsentModal && (
|
||||||
|
<AdultConsentModal
|
||||||
|
contentWarning={siteView!.site.content_warning!}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
id="app"
|
||||||
|
className="lemmy-site"
|
||||||
|
ref={this.rootRef}
|
||||||
|
data-adult-consent={this.isoData.showAdultConsentModal || null}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn skip-link bg-light position-absolute start-0 z-3"
|
className="btn skip-link bg-light position-absolute start-0 z-3"
|
||||||
onClick={linkEvent(this, this.handleJumpToContent)}
|
onClick={linkEvent(this, handleJumpToContent)}
|
||||||
>
|
>
|
||||||
{I18NextService.i18n.t("jump_to_content", "Jump to content")}
|
{I18NextService.i18n.t("jump_to_content", "Jump to content")}
|
||||||
</button>
|
</button>
|
||||||
|
@ -115,8 +112,8 @@ export class App extends Component<any, any> {
|
||||||
</div>
|
</div>
|
||||||
<Footer site={siteRes} />
|
<Footer site={siteRes} />
|
||||||
</div>
|
</div>
|
||||||
</Provider>
|
</>
|
||||||
</>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
141
src/shared/components/common/adult-consent-modal.tsx
Normal file
141
src/shared/components/common/adult-consent-modal.tsx
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import { Component, LinkedEvent, createRef, linkEvent } from "inferno";
|
||||||
|
import { modalMixin } from "../mixins/modal-mixin";
|
||||||
|
import { adultConsentCookieKey } from "../../config";
|
||||||
|
import { mdToHtml } from "../../markdown";
|
||||||
|
import { I18NextService } from "../../services";
|
||||||
|
import { isHttps } from "@utils/env";
|
||||||
|
import { IsoData } from "../../interfaces";
|
||||||
|
import { setIsoData } from "@utils/app";
|
||||||
|
|
||||||
|
interface AdultConsentModalProps {
|
||||||
|
contentWarning: string;
|
||||||
|
show: boolean;
|
||||||
|
onContinue: LinkedEvent<any, Event> | null;
|
||||||
|
onBack: LinkedEvent<any, Event> | null;
|
||||||
|
redirectCountdown: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@modalMixin
|
||||||
|
class AdultConsentModalInner extends Component<AdultConsentModalProps, any> {
|
||||||
|
readonly modalDivRef = createRef<HTMLDivElement>();
|
||||||
|
readonly continueButtonRef = createRef<HTMLButtonElement>();
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { contentWarning, onContinue, onBack, redirectCountdown } =
|
||||||
|
this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="modal"
|
||||||
|
id="adultConsentModal"
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-hidden
|
||||||
|
aria-label="Content warning"
|
||||||
|
data-bs-backdrop="static"
|
||||||
|
ref={this.modalDivRef}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="modal-dialog modal-fullscreen-sm-down"
|
||||||
|
data-bs-backdrop="static"
|
||||||
|
>
|
||||||
|
<div className="modal-content">
|
||||||
|
<header className="modal-header justify-content-center">
|
||||||
|
<h3 className="modal-title">
|
||||||
|
{I18NextService.i18n.t("content_warning")}
|
||||||
|
</h3>
|
||||||
|
</header>
|
||||||
|
{redirectCountdown === Infinity ? (
|
||||||
|
<div
|
||||||
|
className="modal-body text-center align-middle text-body"
|
||||||
|
dangerouslySetInnerHTML={mdToHtml(contentWarning, () =>
|
||||||
|
this.forceUpdate(),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="modal-body text-center align-middle text-body">
|
||||||
|
{I18NextService.i18n.t("sending_back_message", {
|
||||||
|
seconds: redirectCountdown,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<footer className="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-success"
|
||||||
|
onClick={onContinue}
|
||||||
|
ref={this.continueButtonRef}
|
||||||
|
>
|
||||||
|
{I18NextService.i18n.t("continue")}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-danger" onClick={onBack}>
|
||||||
|
{I18NextService.i18n.t("go_back")}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShow() {
|
||||||
|
this.continueButtonRef.current?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AdultConsentModalState {
|
||||||
|
show: boolean;
|
||||||
|
redirectCountdown: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAdultConsent(i: AdultConsentModal) {
|
||||||
|
document.cookie = `${adultConsentCookieKey}=true; Path=/; SameSite=Strict${isHttps() ? "; Secure" : ""}`;
|
||||||
|
i.setState({ show: false });
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAdultConsentGoBack(i: AdultConsentModal) {
|
||||||
|
i.setState({ redirectCountdown: 5 });
|
||||||
|
|
||||||
|
i.redirectTimeout = setInterval(() => {
|
||||||
|
i.setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
redirectCountdown: prev.redirectCountdown - 1,
|
||||||
|
}));
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AdultConsentModal extends Component<
|
||||||
|
Pick<AdultConsentModalProps, "contentWarning">,
|
||||||
|
AdultConsentModalState
|
||||||
|
> {
|
||||||
|
private isoData: IsoData = setIsoData(this.context);
|
||||||
|
redirectTimeout: NodeJS.Timeout;
|
||||||
|
state: AdultConsentModalState = {
|
||||||
|
show: this.isoData.showAdultConsentModal,
|
||||||
|
redirectCountdown: Infinity,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (this.state.redirectCountdown === 0) {
|
||||||
|
this.context.router.history.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
clearInterval(this.redirectTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { redirectCountdown, show } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdultConsentModalInner
|
||||||
|
contentWarning={this.props.contentWarning}
|
||||||
|
show={show}
|
||||||
|
redirectCountdown={redirectCountdown}
|
||||||
|
onBack={linkEvent(this, handleAdultConsentGoBack)}
|
||||||
|
onContinue={linkEvent(this, handleAdultConsent)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,7 +36,7 @@ export class ImageUploadForm extends Component<
|
||||||
<form className="image-upload-form d-inline">
|
<form className="image-upload-form d-inline">
|
||||||
{this.props.imageSrc && (
|
{this.props.imageSrc && (
|
||||||
<span className="d-inline-block position-relative mb-2">
|
<span className="d-inline-block position-relative mb-2">
|
||||||
{/* TODO: Create "Current Iamge" translation for alt text */}
|
{/* TODO: Create "Current Image" translation for alt text */}
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
src={this.props.imageSrc}
|
src={this.props.imageSrc}
|
||||||
|
|
106
src/shared/components/common/media-uploads.tsx
Normal file
106
src/shared/components/common/media-uploads.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||||
|
import { ListMediaResponse, LocalImage } from "lemmy-js-client";
|
||||||
|
import { HttpService, I18NextService } from "../../services";
|
||||||
|
import { PersonListing } from "../person/person-listing";
|
||||||
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
import { MomentTime } from "./moment-time";
|
||||||
|
import { PictrsImage } from "./pictrs-image";
|
||||||
|
import { getHttpBase } from "@utils/env";
|
||||||
|
import { toast } from "../../toast";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
uploads: ListMediaResponse;
|
||||||
|
showUploader?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@tippyMixin
|
||||||
|
export class MediaUploads extends Component<Props, any> {
|
||||||
|
constructor(props: any, context: any) {
|
||||||
|
super(props, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(
|
||||||
|
nextProps: Readonly<{ children?: InfernoNode } & Props>,
|
||||||
|
): void {
|
||||||
|
if (this.props !== nextProps) {
|
||||||
|
this.setState({ loading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const images = this.props.uploads.images;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="media-uploads table-responsive">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{this.props.showUploader && (
|
||||||
|
<th>{I18NextService.i18n.t("uploader")}</th>
|
||||||
|
)}
|
||||||
|
<th colSpan={3}>{I18NextService.i18n.t("time")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{images.map(i => (
|
||||||
|
<tr key={i.local_image.pictrs_alias}>
|
||||||
|
{this.props.showUploader && (
|
||||||
|
<td>
|
||||||
|
<PersonListing person={i.person} />
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td>
|
||||||
|
<MomentTime published={i.local_image.published} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<PictrsImage
|
||||||
|
src={buildImageUrl(i.local_image.pictrs_alias)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{this.deleteImageBtn(i.local_image)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteImageBtn(image: LocalImage) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={linkEvent(image, this.handleDeleteImage)}
|
||||||
|
className="btn btn-danger"
|
||||||
|
>
|
||||||
|
{I18NextService.i18n.t("delete")}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDeleteImage(image: LocalImage) {
|
||||||
|
const form = {
|
||||||
|
token: image.pictrs_delete_token,
|
||||||
|
filename: image.pictrs_alias,
|
||||||
|
};
|
||||||
|
const res = await HttpService.client.deleteImage(form);
|
||||||
|
const filename = image.pictrs_alias;
|
||||||
|
if (res.state === "success") {
|
||||||
|
const deletePictureText = I18NextService.i18n.t("picture_deleted", {
|
||||||
|
filename,
|
||||||
|
});
|
||||||
|
toast(deletePictureText);
|
||||||
|
} else if (res.state === "failed") {
|
||||||
|
const failedDeletePictureText = I18NextService.i18n.t(
|
||||||
|
"failed_to_delete_picture",
|
||||||
|
{
|
||||||
|
filename,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
toast(failedDeletePictureText, "danger");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildImageUrl(pictrsAlias: string): string {
|
||||||
|
return `${getHttpBase()}/pictrs/image/${pictrsAlias}`;
|
||||||
|
}
|
|
@ -2,6 +2,8 @@ import classNames from "classnames";
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
|
|
||||||
import { UserService } from "../../services";
|
import { UserService } from "../../services";
|
||||||
|
import { setIsoData } from "@utils/app";
|
||||||
|
import { IsoData } from "../../interfaces";
|
||||||
|
|
||||||
const iconThumbnailSize = 96;
|
const iconThumbnailSize = 96;
|
||||||
const thumbnailSize = 256;
|
const thumbnailSize = 256;
|
||||||
|
@ -19,48 +21,46 @@ interface PictrsImageProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PictrsImage extends Component<PictrsImageProps, any> {
|
export class PictrsImage extends Component<PictrsImageProps, any> {
|
||||||
constructor(props: any, context: any) {
|
private readonly isoData: IsoData = setIsoData(this.context);
|
||||||
super(props, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { src, icon, iconOverlay, banner, thumbnail, nsfw, pushup, cardTop } =
|
const { src, icon, iconOverlay, banner, thumbnail, nsfw, pushup, cardTop } =
|
||||||
this.props;
|
this.props;
|
||||||
let user_blur_nsfw = true;
|
|
||||||
if (UserService.Instance.myUserInfo) {
|
|
||||||
user_blur_nsfw =
|
|
||||||
UserService.Instance.myUserInfo?.local_user_view.local_user.blur_nsfw;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blur_image = nsfw && user_blur_nsfw;
|
const blurImage =
|
||||||
|
nsfw &&
|
||||||
|
(UserService.Instance.myUserInfo?.local_user_view.local_user.blur_nsfw ??
|
||||||
|
true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<picture>
|
!this.isoData.showAdultConsentModal && (
|
||||||
<source srcSet={this.src("webp")} type="image/webp" />
|
<picture>
|
||||||
<source srcSet={src} />
|
<source srcSet={this.src("webp")} type="image/webp" />
|
||||||
<source srcSet={this.src("jpg")} type="image/jpeg" />
|
<source srcSet={src} />
|
||||||
<img
|
<source srcSet={this.src("jpg")} type="image/jpeg" />
|
||||||
src={src}
|
<img
|
||||||
alt={this.alt()}
|
src={src}
|
||||||
title={this.alt()}
|
alt={this.alt()}
|
||||||
loading="lazy"
|
title={this.alt()}
|
||||||
className={classNames("overflow-hidden pictrs-image", {
|
loading="lazy"
|
||||||
"img-fluid": !(icon || iconOverlay),
|
className={classNames("overflow-hidden pictrs-image", {
|
||||||
banner,
|
"img-fluid": !(icon || iconOverlay),
|
||||||
"thumbnail rounded object-fit-cover":
|
banner,
|
||||||
thumbnail && !(icon || banner),
|
"thumbnail rounded object-fit-cover":
|
||||||
"img-expanded slight-radius": !(thumbnail || icon),
|
thumbnail && !(icon || banner),
|
||||||
"img-blur": thumbnail && nsfw,
|
"img-expanded slight-radius": !(thumbnail || icon),
|
||||||
"object-fit-cover img-icon me-1": icon,
|
"img-blur": thumbnail && nsfw,
|
||||||
"img-blur-icon": icon && blur_image,
|
"object-fit-cover img-icon me-1": icon,
|
||||||
"img-blur-thumb": thumbnail && blur_image,
|
"img-blur-icon": icon && blurImage,
|
||||||
"ms-2 mb-0 rounded-circle object-fit-cover avatar-overlay":
|
"img-blur-thumb": thumbnail && blurImage,
|
||||||
iconOverlay,
|
"ms-2 mb-0 rounded-circle object-fit-cover avatar-overlay":
|
||||||
"avatar-pushup": pushup,
|
iconOverlay,
|
||||||
"card-img-top": cardTop,
|
"avatar-pushup": pushup,
|
||||||
})}
|
"card-img-top": cardTop,
|
||||||
/>
|
})}
|
||||||
</picture>
|
/>
|
||||||
|
</picture>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
|
||||||
return this.props.src;
|
return this.props.src;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If theres no match, then its not a pictrs image
|
// If there's no match, then it's not a pictrs image
|
||||||
if (!url.pathname.includes("/pictrs/image/")) {
|
if (!url.pathname.includes("/pictrs/image/")) {
|
||||||
return this.props.src;
|
return this.props.src;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,6 @@ import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import { I18NextService } from "../../services/I18NextService";
|
import { I18NextService } from "../../services/I18NextService";
|
||||||
|
|
||||||
// Need to disable this rule because ESLint flat out lies about labels not
|
|
||||||
// having an associated control in this component
|
|
||||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
|
||||||
interface PostHiddenSelectProps {
|
interface PostHiddenSelectProps {
|
||||||
showHidden?: StringBoolean;
|
showHidden?: StringBoolean;
|
||||||
onShowHiddenChange: (hidden?: StringBoolean) => void;
|
onShowHiddenChange: (hidden?: StringBoolean) => void;
|
||||||
|
|
|
@ -20,7 +20,7 @@ function handleSwitchTab({ ctx, tab }: { ctx: Tabs; tab: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Tabs extends Component<TabsProps, TabsState> {
|
export default class Tabs extends Component<TabsProps, TabsState> {
|
||||||
constructor(props: TabsProps, context) {
|
constructor(props: TabsProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
GetFederatedInstancesResponse,
|
GetFederatedInstancesResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
LemmyHttp,
|
LemmyHttp,
|
||||||
|
ListMediaResponse,
|
||||||
PersonView,
|
PersonView,
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { InitialFetchRequest } from "../../interfaces";
|
import { InitialFetchRequest } from "../../interfaces";
|
||||||
|
@ -37,10 +38,14 @@ import { TaglineForm } from "./tagline-form";
|
||||||
import { getHttpBaseInternal } from "../../utils/env";
|
import { getHttpBaseInternal } from "../../utils/env";
|
||||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
import { IRoutePropsWithFetch } from "../../routes";
|
import { IRoutePropsWithFetch } from "../../routes";
|
||||||
|
import { MediaUploads } from "../common/media-uploads";
|
||||||
|
import { Paginator } from "../common/paginator";
|
||||||
|
import { snapToTop } from "@utils/browser";
|
||||||
|
|
||||||
type AdminSettingsData = RouteDataResponse<{
|
type AdminSettingsData = RouteDataResponse<{
|
||||||
bannedRes: BannedPersonsResponse;
|
bannedRes: BannedPersonsResponse;
|
||||||
instancesRes: GetFederatedInstancesResponse;
|
instancesRes: GetFederatedInstancesResponse;
|
||||||
|
uploadsRes: ListMediaResponse;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
interface AdminSettingsState {
|
interface AdminSettingsState {
|
||||||
|
@ -50,6 +55,8 @@ interface AdminSettingsState {
|
||||||
instancesRes: RequestState<GetFederatedInstancesResponse>;
|
instancesRes: RequestState<GetFederatedInstancesResponse>;
|
||||||
bannedRes: RequestState<BannedPersonsResponse>;
|
bannedRes: RequestState<BannedPersonsResponse>;
|
||||||
leaveAdminTeamRes: RequestState<GetSiteResponse>;
|
leaveAdminTeamRes: RequestState<GetSiteResponse>;
|
||||||
|
uploadsRes: RequestState<ListMediaResponse>;
|
||||||
|
uploadsPage: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
themeList: string[];
|
themeList: string[];
|
||||||
isIsomorphic: boolean;
|
isIsomorphic: boolean;
|
||||||
|
@ -76,13 +83,19 @@ export class AdminSettings extends Component<
|
||||||
bannedRes: EMPTY_REQUEST,
|
bannedRes: EMPTY_REQUEST,
|
||||||
instancesRes: EMPTY_REQUEST,
|
instancesRes: EMPTY_REQUEST,
|
||||||
leaveAdminTeamRes: EMPTY_REQUEST,
|
leaveAdminTeamRes: EMPTY_REQUEST,
|
||||||
|
uploadsRes: EMPTY_REQUEST,
|
||||||
|
uploadsPage: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
themeList: [],
|
themeList: [],
|
||||||
isIsomorphic: false,
|
isIsomorphic: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
loadingSettled() {
|
loadingSettled() {
|
||||||
return resourcesSettled([this.state.bannedRes, this.state.instancesRes]);
|
return resourcesSettled([
|
||||||
|
this.state.bannedRes,
|
||||||
|
this.state.instancesRes,
|
||||||
|
this.state.uploadsRes,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -92,15 +105,17 @@ export class AdminSettings extends Component<
|
||||||
this.handleEditEmoji = this.handleEditEmoji.bind(this);
|
this.handleEditEmoji = this.handleEditEmoji.bind(this);
|
||||||
this.handleDeleteEmoji = this.handleDeleteEmoji.bind(this);
|
this.handleDeleteEmoji = this.handleDeleteEmoji.bind(this);
|
||||||
this.handleCreateEmoji = this.handleCreateEmoji.bind(this);
|
this.handleCreateEmoji = this.handleCreateEmoji.bind(this);
|
||||||
|
this.handleUploadsPageChange = this.handleUploadsPageChange.bind(this);
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// Only fetch the data if coming from another route
|
||||||
if (FirstLoadService.isFirstLoad) {
|
if (FirstLoadService.isFirstLoad) {
|
||||||
const { bannedRes, instancesRes } = this.isoData.routeData;
|
const { bannedRes, instancesRes, uploadsRes } = this.isoData.routeData;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
bannedRes,
|
bannedRes,
|
||||||
instancesRes,
|
instancesRes,
|
||||||
|
uploadsRes,
|
||||||
isIsomorphic: true,
|
isIsomorphic: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -115,6 +130,7 @@ export class AdminSettings extends Component<
|
||||||
return {
|
return {
|
||||||
bannedRes: await client.getBannedPersons(),
|
bannedRes: await client.getBannedPersons(),
|
||||||
instancesRes: await client.getFederatedInstances(),
|
instancesRes: await client.getFederatedInstances(),
|
||||||
|
uploadsRes: await client.listAllMedia(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,6 +272,21 @@ export class AdminSettings extends Component<
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "uploads",
|
||||||
|
label: I18NextService.i18n.t("uploads"),
|
||||||
|
getNode: isSelected => (
|
||||||
|
<div
|
||||||
|
className={classNames("tab-pane", {
|
||||||
|
active: isSelected,
|
||||||
|
})}
|
||||||
|
role="tabpanel"
|
||||||
|
id="uploads-tab-pane"
|
||||||
|
>
|
||||||
|
{this.uploads()}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -266,22 +297,34 @@ export class AdminSettings extends Component<
|
||||||
this.setState({
|
this.setState({
|
||||||
bannedRes: LOADING_REQUEST,
|
bannedRes: LOADING_REQUEST,
|
||||||
instancesRes: LOADING_REQUEST,
|
instancesRes: LOADING_REQUEST,
|
||||||
|
uploadsRes: LOADING_REQUEST,
|
||||||
themeList: [],
|
themeList: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [bannedRes, instancesRes, themeList] = await Promise.all([
|
const [bannedRes, instancesRes, uploadsRes, themeList] = await Promise.all([
|
||||||
HttpService.client.getBannedPersons(),
|
HttpService.client.getBannedPersons(),
|
||||||
HttpService.client.getFederatedInstances(),
|
HttpService.client.getFederatedInstances(),
|
||||||
|
HttpService.client.listAllMedia({
|
||||||
|
page: this.state.uploadsPage,
|
||||||
|
}),
|
||||||
fetchThemeList(),
|
fetchThemeList(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
bannedRes,
|
bannedRes,
|
||||||
instancesRes,
|
instancesRes,
|
||||||
|
uploadsRes,
|
||||||
themeList,
|
themeList,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchUploadsOnly() {
|
||||||
|
const uploadsRes = await HttpService.client.listAllMedia({
|
||||||
|
page: this.state.uploadsPage,
|
||||||
|
});
|
||||||
|
this.setState({ uploadsRes });
|
||||||
|
}
|
||||||
|
|
||||||
admins() {
|
admins() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -341,6 +384,30 @@ export class AdminSettings extends Component<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploads() {
|
||||||
|
switch (this.state.uploadsRes.state) {
|
||||||
|
case "loading":
|
||||||
|
return (
|
||||||
|
<h5>
|
||||||
|
<Spinner large />
|
||||||
|
</h5>
|
||||||
|
);
|
||||||
|
case "success": {
|
||||||
|
const uploadsRes = this.state.uploadsRes.data;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MediaUploads showUploader uploads={uploadsRes} />
|
||||||
|
<Paginator
|
||||||
|
page={this.state.uploadsPage}
|
||||||
|
onChange={this.handleUploadsPageChange}
|
||||||
|
nextDisabled={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async handleEditSite(form: EditSite) {
|
async handleEditSite(form: EditSite) {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
|
|
||||||
|
@ -397,4 +464,10 @@ export class AdminSettings extends Component<
|
||||||
updateEmojiDataModel(res.data.custom_emoji);
|
updateEmojiDataModel(res.data.custom_emoji);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleUploadsPageChange(val: number) {
|
||||||
|
this.setState({ uploadsPage: val });
|
||||||
|
snapToTop();
|
||||||
|
await this.fetchUploadsOnly();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ export class Signup extends Component<
|
||||||
registerRes: EMPTY_REQUEST,
|
registerRes: EMPTY_REQUEST,
|
||||||
captchaRes: EMPTY_REQUEST,
|
captchaRes: EMPTY_REQUEST,
|
||||||
form: {
|
form: {
|
||||||
show_nsfw: false,
|
show_nsfw: !!this.isoData.site_res.site_view.site.content_warning,
|
||||||
},
|
},
|
||||||
captchaPlaying: false,
|
captchaPlaying: false,
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
|
|
|
@ -86,6 +86,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
allowed_instances: this.props.allowedInstances?.map(i => i.domain),
|
allowed_instances: this.props.allowedInstances?.map(i => i.domain),
|
||||||
blocked_instances: this.props.blockedInstances?.map(i => i.domain),
|
blocked_instances: this.props.blockedInstances?.map(i => i.domain),
|
||||||
blocked_urls: this.props.siteRes.blocked_urls.map(u => u.url),
|
blocked_urls: this.props.siteRes.blocked_urls.map(u => u.url),
|
||||||
|
content_warning: this.props.siteRes.site_view.site.content_warning,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,6 +117,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
this.handleInstanceTextChange = this.handleInstanceTextChange.bind(this);
|
this.handleInstanceTextChange = this.handleInstanceTextChange.bind(this);
|
||||||
|
|
||||||
this.handleBlockedUrlsUpdate = this.handleBlockedUrlsUpdate.bind(this);
|
this.handleBlockedUrlsUpdate = this.handleBlockedUrlsUpdate.bind(this);
|
||||||
|
this.handleSiteContentWarningChange =
|
||||||
|
this.handleSiteContentWarningChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -269,6 +272,26 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{this.state.siteForm.enable_nsfw && (
|
||||||
|
<div className="mb-3 row">
|
||||||
|
<div className="alert small alert-info" role="alert">
|
||||||
|
<Icon icon="info" classes="icon-inline me-2" />
|
||||||
|
{I18NextService.i18n.t("content_warning_setting_blurb")}
|
||||||
|
</div>
|
||||||
|
<label className="col-12 col-form-label">
|
||||||
|
{I18NextService.i18n.t("content_warning")}
|
||||||
|
</label>
|
||||||
|
<div className="col-12">
|
||||||
|
<MarkdownTextArea
|
||||||
|
initialContent={this.state.siteForm.content_warning}
|
||||||
|
onContentChange={this.handleSiteContentWarningChange}
|
||||||
|
hideNavigationWarnings
|
||||||
|
allLanguages={[]}
|
||||||
|
siteLanguages={[]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="mb-3 row">
|
<div className="mb-3 row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<label
|
<label
|
||||||
|
@ -1012,4 +1035,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSiteContentWarningChange(val: string) {
|
||||||
|
this.setState(s => ((s.siteForm.content_warning = val), s));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,8 @@ import {
|
||||||
GetPersonDetailsResponse,
|
GetPersonDetailsResponse,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
LemmyHttp,
|
LemmyHttp,
|
||||||
|
ListMedia,
|
||||||
|
ListMediaResponse,
|
||||||
LockPost,
|
LockPost,
|
||||||
MarkCommentReplyAsRead,
|
MarkCommentReplyAsRead,
|
||||||
MarkPersonMentionAsRead,
|
MarkPersonMentionAsRead,
|
||||||
|
@ -96,13 +98,16 @@ import { PersonDetails } from "./person-details";
|
||||||
import { PersonListing } from "./person-listing";
|
import { PersonListing } from "./person-listing";
|
||||||
import { getHttpBaseInternal } from "../../utils/env";
|
import { getHttpBaseInternal } from "../../utils/env";
|
||||||
import { IRoutePropsWithFetch } from "../../routes";
|
import { IRoutePropsWithFetch } from "../../routes";
|
||||||
|
import { MediaUploads } from "../common/media-uploads";
|
||||||
|
|
||||||
type ProfileData = RouteDataResponse<{
|
type ProfileData = RouteDataResponse<{
|
||||||
personResponse: GetPersonDetailsResponse;
|
personRes: GetPersonDetailsResponse;
|
||||||
|
uploadsRes: ListMediaResponse;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
interface ProfileState {
|
interface ProfileState {
|
||||||
personRes: RequestState<GetPersonDetailsResponse>;
|
personRes: RequestState<GetPersonDetailsResponse>;
|
||||||
|
uploadsRes: RequestState<ListMediaResponse>;
|
||||||
personBlocked: boolean;
|
personBlocked: boolean;
|
||||||
banReason?: string;
|
banReason?: string;
|
||||||
banExpireDays?: number;
|
banExpireDays?: number;
|
||||||
|
@ -189,6 +194,7 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||||
private isoData = setIsoData<ProfileData>(this.context);
|
private isoData = setIsoData<ProfileData>(this.context);
|
||||||
state: ProfileState = {
|
state: ProfileState = {
|
||||||
personRes: EMPTY_REQUEST,
|
personRes: EMPTY_REQUEST,
|
||||||
|
uploadsRes: EMPTY_REQUEST,
|
||||||
personBlocked: false,
|
personBlocked: false,
|
||||||
siteRes: this.isoData.site_res,
|
siteRes: this.isoData.site_res,
|
||||||
showBanDialog: false,
|
showBanDialog: false,
|
||||||
|
@ -241,10 +247,12 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// Only fetch the data if coming from another route
|
||||||
if (FirstLoadService.isFirstLoad) {
|
if (FirstLoadService.isFirstLoad) {
|
||||||
const personRes = this.isoData.routeData.personResponse;
|
const personRes = this.isoData.routeData.personRes;
|
||||||
|
const uploadsRes = this.isoData.routeData.uploadsRes;
|
||||||
this.state = {
|
this.state = {
|
||||||
...this.state,
|
...this.state,
|
||||||
personRes,
|
personRes,
|
||||||
|
uploadsRes,
|
||||||
isIsomorphic: true,
|
isIsomorphic: true,
|
||||||
personBlocked: isPersonBlocked(personRes),
|
personBlocked: isPersonBlocked(personRes),
|
||||||
};
|
};
|
||||||
|
@ -268,10 +276,21 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||||
page,
|
page,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
personRes,
|
personRes,
|
||||||
personBlocked: isPersonBlocked(personRes),
|
personBlocked: isPersonBlocked(personRes),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (view === PersonDetailsView.Uploads) {
|
||||||
|
this.setState({ uploadsRes: LOADING_REQUEST });
|
||||||
|
const form: ListMedia = {
|
||||||
|
page,
|
||||||
|
limit: fetchLimit,
|
||||||
|
};
|
||||||
|
const uploadsRes = await HttpService.client.listMedia(form);
|
||||||
|
this.setState({ uploadsRes });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get amCurrentUser() {
|
get amCurrentUser() {
|
||||||
|
@ -299,6 +318,16 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||||
new LemmyHttp(getHttpBaseInternal(), { headers }),
|
new LemmyHttp(getHttpBaseInternal(), { headers }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let uploadsRes: RequestState<ListMediaResponse> = EMPTY_REQUEST;
|
||||||
|
|
||||||
|
if (view === PersonDetailsView.Uploads) {
|
||||||
|
const form: ListMedia = {
|
||||||
|
page,
|
||||||
|
limit: fetchLimit,
|
||||||
|
};
|
||||||
|
uploadsRes = await client.listMedia(form);
|
||||||
|
}
|
||||||
|
|
||||||
const form: GetPersonDetails = {
|
const form: GetPersonDetails = {
|
||||||
username: username,
|
username: username,
|
||||||
sort,
|
sort,
|
||||||
|
@ -306,9 +335,11 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||||
page,
|
page,
|
||||||
limit: fetchLimit,
|
limit: fetchLimit,
|
||||||
};
|
};
|
||||||
|
const personRes = await client.getPersonDetails(form);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
personResponse: await client.getPersonDetails(form),
|
personRes,
|
||||||
|
uploadsRes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,6 +351,25 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||||
: siteName;
|
: siteName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderUploadsRes() {
|
||||||
|
switch (this.state.uploadsRes.state) {
|
||||||
|
case "loading":
|
||||||
|
return (
|
||||||
|
<h5>
|
||||||
|
<Spinner large />
|
||||||
|
</h5>
|
||||||
|
);
|
||||||
|
case "success": {
|
||||||
|
const uploadsRes = this.state.uploadsRes.data;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MediaUploads uploads={uploadsRes} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderPersonRes() {
|
renderPersonRes() {
|
||||||
switch (this.state.personRes.state) {
|
switch (this.state.personRes.state) {
|
||||||
case "loading":
|
case "loading":
|
||||||
|
@ -350,6 +400,8 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||||
|
|
||||||
{this.selects}
|
{this.selects}
|
||||||
|
|
||||||
|
{this.renderUploadsRes()}
|
||||||
|
|
||||||
<PersonDetails
|
<PersonDetails
|
||||||
personRes={personRes}
|
personRes={personRes}
|
||||||
admins={siteRes.admins}
|
admins={siteRes.admins}
|
||||||
|
@ -416,11 +468,12 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||||
|
|
||||||
get viewRadios() {
|
get viewRadios() {
|
||||||
return (
|
return (
|
||||||
<div className="btn-group btn-group-toggle flex-wrap mb-2" role="group">
|
<div className="btn-group btn-group-toggle flex-wrap" role="group">
|
||||||
{this.getRadio(PersonDetailsView.Overview)}
|
{this.getRadio(PersonDetailsView.Overview)}
|
||||||
{this.getRadio(PersonDetailsView.Comments)}
|
{this.getRadio(PersonDetailsView.Comments)}
|
||||||
{this.getRadio(PersonDetailsView.Posts)}
|
{this.getRadio(PersonDetailsView.Posts)}
|
||||||
{this.amCurrentUser && this.getRadio(PersonDetailsView.Saved)}
|
{this.amCurrentUser && this.getRadio(PersonDetailsView.Saved)}
|
||||||
|
{this.getRadio(PersonDetailsView.Uploads)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -459,18 +512,22 @@ export class Profile extends Component<ProfileRouteProps, ProfileState> {
|
||||||
const profileRss = `/feeds/u/${username}.xml${getQueryString({ sort })}`;
|
const profileRss = `/feeds/u/${username}.xml${getQueryString({ sort })}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="row align-items-center mb-3 g-3">
|
||||||
<span className="me-3">{this.viewRadios}</span>
|
<div className="col-auto">{this.viewRadios}</div>
|
||||||
<SortSelect
|
<div className="col-auto">
|
||||||
sort={sort}
|
<SortSelect
|
||||||
onChange={this.handleSortChange}
|
sort={sort}
|
||||||
hideHot
|
onChange={this.handleSortChange}
|
||||||
hideMostComments
|
hideHot
|
||||||
/>
|
hideMostComments
|
||||||
<a href={profileRss} rel={relTags} title="RSS">
|
/>
|
||||||
<Icon icon="rss" classes="text-muted small mx-2" />
|
</div>
|
||||||
</a>
|
<div className="col-auto">
|
||||||
<link rel="alternate" type="application/atom+xml" href={profileRss} />
|
<a href={profileRss} rel={relTags} title="RSS">
|
||||||
|
<Icon icon="rss" classes="text-muted small ps-0" />
|
||||||
|
</a>
|
||||||
|
<link rel="alternate" type="application/atom+xml" href={profileRss} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -931,7 +931,11 @@ export class Settings extends Component<SettingsRouteProps, SettingsState> {
|
||||||
className="form-check-input"
|
className="form-check-input"
|
||||||
id="user-blur-nsfw"
|
id="user-blur-nsfw"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={this.state.saveUserSettingsForm.blur_nsfw}
|
disabled={!this.state.saveUserSettingsForm.show_nsfw}
|
||||||
|
checked={
|
||||||
|
this.state.saveUserSettingsForm.blur_nsfw &&
|
||||||
|
this.state.saveUserSettingsForm.show_nsfw
|
||||||
|
}
|
||||||
onChange={linkEvent(this, this.handleBlurNsfwChange)}
|
onChange={linkEvent(this, this.handleBlurNsfwChange)}
|
||||||
/>
|
/>
|
||||||
<label className="form-check-label" htmlFor="user-blur-nsfw">
|
<label className="form-check-label" htmlFor="user-blur-nsfw">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { myAuth } from "@utils/app";
|
import { myAuth, setIsoData } from "@utils/app";
|
||||||
import { canShare, share } from "@utils/browser";
|
import { canShare, share } from "@utils/browser";
|
||||||
import { getExternalHost, getHttpBase } from "@utils/env";
|
import { getExternalHost, getHttpBase } from "@utils/env";
|
||||||
import { futureDaysToUnixTime, hostname } from "@utils/helpers";
|
import { futureDaysToUnixTime, hostname } from "@utils/helpers";
|
||||||
|
@ -34,7 +34,7 @@ import {
|
||||||
TransferCommunity,
|
TransferCommunity,
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { relTags } from "../../config";
|
import { relTags } from "../../config";
|
||||||
import { VoteContentType } from "../../interfaces";
|
import { IsoDataOptionalSite, VoteContentType } from "../../interfaces";
|
||||||
import { mdToHtml, mdToHtmlInline } from "../../markdown";
|
import { mdToHtml, mdToHtmlInline } from "../../markdown";
|
||||||
import { I18NextService, UserService } from "../../services";
|
import { I18NextService, UserService } from "../../services";
|
||||||
import { tippyMixin } from "../mixins/tippy-mixin";
|
import { tippyMixin } from "../mixins/tippy-mixin";
|
||||||
|
@ -100,9 +100,10 @@ interface PostListingProps {
|
||||||
|
|
||||||
@tippyMixin
|
@tippyMixin
|
||||||
export class PostListing extends Component<PostListingProps, PostListingState> {
|
export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
private readonly isoData: IsoDataOptionalSite = setIsoData(this.context);
|
||||||
state: PostListingState = {
|
state: PostListingState = {
|
||||||
showEdit: false,
|
showEdit: false,
|
||||||
imageExpanded: false,
|
imageExpanded: !!this.isoData.site_res?.site_view.site.content_warning,
|
||||||
viewSource: false,
|
viewSource: false,
|
||||||
showAdvanced: false,
|
showAdvanced: false,
|
||||||
showBody: false,
|
showBody: false,
|
||||||
|
|
|
@ -28,6 +28,7 @@ export const fetchLimit = 20;
|
||||||
export const relTags = "noopener nofollow";
|
export const relTags = "noopener nofollow";
|
||||||
export const emDash = "\u2014";
|
export const emDash = "\u2014";
|
||||||
export const authCookieName = "jwt";
|
export const authCookieName = "jwt";
|
||||||
|
export const adultConsentCookieKey = "adultConsent";
|
||||||
|
|
||||||
// No. of max displayed communities per
|
// No. of max displayed communities per
|
||||||
// page on route "/communities"
|
// page on route "/communities"
|
||||||
|
|
|
@ -16,6 +16,7 @@ export interface IsoData<T extends RouteData = any> {
|
||||||
routeData: T;
|
routeData: T;
|
||||||
site_res: GetSiteResponse;
|
site_res: GetSiteResponse;
|
||||||
errorPageData?: ErrorPageData;
|
errorPageData?: ErrorPageData;
|
||||||
|
showAdultConsentModal: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IsoDataOptionalSite<T extends RouteData = any> = Partial<
|
export type IsoDataOptionalSite<T extends RouteData = any> = Partial<
|
||||||
|
@ -67,6 +68,7 @@ export enum PersonDetailsView {
|
||||||
Comments = "Comments",
|
Comments = "Comments",
|
||||||
Posts = "Posts",
|
Posts = "Posts",
|
||||||
Saved = "Saved",
|
Saved = "Saved",
|
||||||
|
Uploads = "Uploads",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PurgeType {
|
export enum PurgeType {
|
||||||
|
|
|
@ -168,7 +168,7 @@ function bestDateFns(
|
||||||
return locale;
|
return locale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback to base langauge first, to avoid mixing languages.
|
// Fallback to base language first, to avoid mixing languages.
|
||||||
return langToLocale(base_lang) ?? localeByCode[EN_US];
|
return langToLocale(base_lang) ?? localeByCode[EN_US];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue