diff --git a/.eslintrc.json b/.eslintrc.json
index 651ba006b..f289dccbe 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -49,6 +49,7 @@
"prettier/prettier": "error",
"quote-props": 0,
"unicorn/filename-case": 0,
- "jsx-a11y/media-has-caption": 0
+ "jsx-a11y/media-has-caption": 0,
+ "jsx-a11y/label-has-associated-control": 0
}
}
diff --git a/lemmy-translations b/lemmy-translations
index f9783d686..c88dd1e3b 160000
--- a/lemmy-translations
+++ b/lemmy-translations
@@ -1 +1 @@
-Subproject commit f9783d686637197a389b8f10a907e0533c55b688
+Subproject commit c88dd1e3b36ee1617f1b86acf94c1b7946e97cd4
diff --git a/package.json b/package.json
index 4c36ed405..f1d3c2bf9 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"classnames": "^2.5.1",
"clean-webpack-plugin": "^4.0.0",
"cookie": "^0.6.0",
+ "cookie-parser": "^1.4.6",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^6.10.0",
"date-fns": "^3.6.0",
@@ -94,6 +95,7 @@
"@types/autosize": "^4.0.3",
"@types/bootstrap": "^5.2.10",
"@types/cookie": "^0.6.0",
+ "@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.21",
"@types/html-to-text": "^9.0.4",
"@types/lodash.isequal": "^4.5.8",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0ad393bd2..98b531367 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -59,6 +59,9 @@ importers:
cookie:
specifier: ^0.6.0
version: 0.6.0
+ cookie-parser:
+ specifier: ^1.4.6
+ version: 1.4.6
copy-webpack-plugin:
specifier: ^12.0.2
version: 12.0.2(webpack@5.91.0(webpack-cli@5.1.4))
@@ -210,6 +213,9 @@ importers:
'@types/cookie':
specifier: ^0.6.0
version: 0.6.0
+ '@types/cookie-parser':
+ specifier: ^1.4.7
+ version: 1.4.7
'@types/express':
specifier: ^4.17.21
version: 4.17.21
@@ -1207,6 +1213,9 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+ '@types/cookie-parser@1.4.7':
+ resolution: {integrity: sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==}
+
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
@@ -1856,9 +1865,17 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie-parser@1.4.6:
+ resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==}
+ engines: {node: '>= 0.8.0'}
+
cookie-signature@1.0.6:
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
+ cookie@0.4.1:
+ resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==}
+ engines: {node: '>= 0.6'}
+
cookie@0.6.0:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
@@ -5742,6 +5759,10 @@ snapshots:
dependencies:
'@types/node': 20.11.30
+ '@types/cookie-parser@1.4.7':
+ dependencies:
+ '@types/express': 4.17.21
+
'@types/cookie@0.6.0': {}
'@types/eslint-scope@3.7.7':
@@ -6479,8 +6500,15 @@ snapshots:
convert-source-map@2.0.0: {}
+ cookie-parser@1.4.6:
+ dependencies:
+ cookie: 0.4.1
+ cookie-signature: 1.0.6
+
cookie-signature@1.0.6: {}
+ cookie@0.4.1: {}
+
cookie@0.6.0: {}
copy-webpack-plugin@11.0.0(webpack@5.91.0(webpack-cli@5.1.4)):
diff --git a/src/assets/symbols.svg b/src/assets/symbols.svg
index 64002a32f..87d633617 100644
--- a/src/assets/symbols.svg
+++ b/src/assets/symbols.svg
@@ -292,5 +292,8 @@
+
+
+
diff --git a/src/client/index.tsx b/src/client/index.tsx
index db0a9ef76..270311898 100644
--- a/src/client/index.tsx
+++ b/src/client/index.tsx
@@ -1,7 +1,7 @@
import { initializeSite } from "@utils/app";
import { hydrate } from "inferno-hydrate";
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 { loadUserLanguage } from "../shared/services/I18NextService";
import { verifyDynamicImports } from "../shared/dynamic-imports";
diff --git a/src/server/handlers/catch-all-handler.tsx b/src/server/handlers/catch-all-handler.tsx
index 33d4d7a6e..88b06bdfd 100644
--- a/src/server/handlers/catch-all-handler.tsx
+++ b/src/server/handlers/catch-all-handler.tsx
@@ -6,7 +6,7 @@ import { StaticRouter, matchPath } from "inferno-router";
import { Match } from "inferno-router/dist/Route";
import { renderToString } from "inferno-server";
import { GetSiteResponse, LemmyHttp } from "lemmy-js-client";
-import { App } from "../../shared/components/app/app";
+import App from "../../shared/components/app/app";
import {
InitialFetchRequest,
IsoDataOptionalSite,
@@ -28,6 +28,7 @@ import {
} from "../../shared/services/";
import { parsePath } from "history";
import { getQueryString } from "@utils/helpers";
+import { adultConsentCookieKey } from "../../shared/config";
export default async (req: Request, res: Response) => {
try {
@@ -142,6 +143,9 @@ export default async (req: Request, res: Response) => {
site_res: site,
routeData,
errorPageData,
+ showAdultConsentModal:
+ !!site?.site_view.site.content_warning &&
+ !(site.my_user || req.cookies[adultConsentCookieKey]),
};
const wrapper = (
diff --git a/src/server/index.tsx b/src/server/index.tsx
index 34fc9cccf..cbbd027b8 100644
--- a/src/server/index.tsx
+++ b/src/server/index.tsx
@@ -14,8 +14,10 @@ import ThemesListHandler from "./handlers/themes-list-handler";
import { setCacheControl, setDefaultCsp } from "./middleware";
import CodeThemeHandler from "./handlers/code-theme-handler";
import { verifyDynamicImports } from "../shared/dynamic-imports";
+import cookieParser from "cookie-parser";
const server = express();
+server.use(cookieParser());
const [hostname, port] = process.env["LEMMY_UI_HOST"]
? process.env["LEMMY_UI_HOST"].split(":")
diff --git a/src/server/utils/create-ssr-html.tsx b/src/server/utils/create-ssr-html.tsx
index c47e1dae2..cb0ec615d 100644
--- a/src/server/utils/create-ssr-html.tsx
+++ b/src/server/utils/create-ssr-html.tsx
@@ -80,13 +80,14 @@ export async function createSsrHtml(
${lazyScripts}
-
${erudaStr}
@@ -97,6 +98,16 @@ export async function createSsrHtml(
${helmet.title.toString()}
${helmet.meta.toString()}
+
+
diff --git a/src/shared/components/app/app.tsx b/src/shared/components/app/app.tsx
index 1538b0450..4d1cbea26 100644
--- a/src/shared/components/app/app.tsx
+++ b/src/shared/components/app/app.tsx
@@ -1,5 +1,5 @@
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 { Route, Switch } from "inferno-router";
import { IsoDataOptionalSite } from "../../interfaces";
@@ -13,42 +13,39 @@ import { Navbar } from "./navbar";
import "./styles.scss";
import { Theme } from "./theme";
import AnonymousGuard from "../common/anonymous-guard";
-import { destroyTippy, setupTippy } from "../../tippy";
+import AdultConsentModal from "../common/adult-consent-modal";
-export class App extends Component {
+function handleJumpToContent(event) {
+ event.preventDefault();
+}
+
+export default class App extends Component {
private isoData: IsoDataOptionalSite = setIsoData(this.context);
- private readonly mainContentRef: RefObject;
private readonly rootRef = createRef();
- 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() {
const siteRes = this.isoData.site_res;
const siteView = siteRes?.site_view;
return (
- <>
-
-
+
+ {/* This fragment is required to avoid an SSR error*/}
+ <>
+ {this.isoData.showAdultConsentModal && (
+
+ )}
+
@@ -115,8 +112,8 @@ export class App extends Component
{
-
- >
+ >
+
);
}
}
diff --git a/src/shared/components/common/adult-consent-modal.tsx b/src/shared/components/common/adult-consent-modal.tsx
new file mode 100644
index 000000000..ca73e895e
--- /dev/null
+++ b/src/shared/components/common/adult-consent-modal.tsx
@@ -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 | null;
+ onBack: LinkedEvent | null;
+ redirectCountdown: number;
+}
+
+@modalMixin
+class AdultConsentModalInner extends Component {
+ readonly modalDivRef = createRef();
+ readonly continueButtonRef = createRef();
+
+ render() {
+ const { contentWarning, onContinue, onBack, redirectCountdown } =
+ this.props;
+
+ return (
+
+
+
+
+
+ {I18NextService.i18n.t("content_warning")}
+
+
+ {redirectCountdown === Infinity ? (
+
+ this.forceUpdate(),
+ )}
+ />
+ ) : (
+
+ {I18NextService.i18n.t("sending_back_message", {
+ seconds: redirectCountdown,
+ })}
+
+ )}
+
+
+
+
+ );
+ }
+
+ 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
,
+ 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 (
+
+ );
+ }
+}
diff --git a/src/shared/components/common/pictrs-image.tsx b/src/shared/components/common/pictrs-image.tsx
index 261b6ee68..9a5f450e7 100644
--- a/src/shared/components/common/pictrs-image.tsx
+++ b/src/shared/components/common/pictrs-image.tsx
@@ -2,6 +2,8 @@ import classNames from "classnames";
import { Component } from "inferno";
import { UserService } from "../../services";
+import { setIsoData } from "@utils/app";
+import { IsoData } from "../../interfaces";
const iconThumbnailSize = 96;
const thumbnailSize = 256;
@@ -19,48 +21,46 @@ interface PictrsImageProps {
}
export class PictrsImage extends Component {
- constructor(props: any, context: any) {
- super(props, context);
- }
+ private readonly isoData: IsoData = setIsoData(this.context);
render() {
const { src, icon, iconOverlay, banner, thumbnail, nsfw, pushup, cardTop } =
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 (
-
+ !this.isoData.showAdultConsentModal && (
+
+ )
);
}
diff --git a/src/shared/components/common/post-hidden-select.tsx b/src/shared/components/common/post-hidden-select.tsx
index 6bb9e3230..e59069e1e 100644
--- a/src/shared/components/common/post-hidden-select.tsx
+++ b/src/shared/components/common/post-hidden-select.tsx
@@ -5,9 +5,6 @@ import { tippyMixin } from "../mixins/tippy-mixin";
import { Component, linkEvent } from "inferno";
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 {
showHidden?: StringBoolean;
onShowHiddenChange: (hidden?: StringBoolean) => void;
diff --git a/src/shared/components/home/signup.tsx b/src/shared/components/home/signup.tsx
index b0398ef97..a6705bdcd 100644
--- a/src/shared/components/home/signup.tsx
+++ b/src/shared/components/home/signup.tsx
@@ -57,7 +57,7 @@ export class Signup extends Component<
registerRes: EMPTY_REQUEST,
captchaRes: EMPTY_REQUEST,
form: {
- show_nsfw: false,
+ show_nsfw: !!this.isoData.site_res.site_view.site.content_warning,
},
captchaPlaying: false,
siteRes: this.isoData.site_res,
diff --git a/src/shared/components/home/site-form.tsx b/src/shared/components/home/site-form.tsx
index 18af5fa21..430365598 100644
--- a/src/shared/components/home/site-form.tsx
+++ b/src/shared/components/home/site-form.tsx
@@ -86,6 +86,7 @@ export class SiteForm extends Component {
allowed_instances: this.props.allowedInstances?.map(i => i.domain),
blocked_instances: this.props.blockedInstances?.map(i => i.domain),
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 {
this.handleInstanceTextChange = this.handleInstanceTextChange.bind(this);
this.handleBlockedUrlsUpdate = this.handleBlockedUrlsUpdate.bind(this);
+ this.handleSiteContentWarningChange =
+ this.handleSiteContentWarningChange.bind(this);
}
render() {
@@ -269,6 +272,26 @@ export class SiteForm extends Component {
+ {this.state.siteForm.enable_nsfw && (
+
+
+
+ {I18NextService.i18n.t("content_warning_setting_blurb")}
+
+
+
+
+
+
+ )}