mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-26 14:21:13 +00:00
Merge branch 'main' into vaporwave
This commit is contained in:
commit
f55b804685
23 changed files with 2160 additions and 2605 deletions
|
@ -2,3 +2,4 @@ src/shared/translations
|
||||||
lemmy-translations
|
lemmy-translations
|
||||||
src/assets/css/themes/*.css
|
src/assets/css/themes/*.css
|
||||||
stats.json
|
stats.json
|
||||||
|
dist
|
||||||
|
|
|
@ -27,7 +27,7 @@ COPY .git .git
|
||||||
RUN echo "export const VERSION = '$(git describe --tag)';" > "src/shared/version.ts"
|
RUN echo "export const VERSION = '$(git describe --tag)';" > "src/shared/version.ts"
|
||||||
|
|
||||||
RUN yarn --production --prefer-offline
|
RUN yarn --production --prefer-offline
|
||||||
RUN yarn build:prod
|
RUN NODE_OPTIONS="--max-old-space-size=8192" yarn build:prod
|
||||||
|
|
||||||
# Prune the image
|
# Prune the image
|
||||||
RUN node-prune /usr/src/app/node_modules
|
RUN node-prune /usr/src/app/node_modules
|
||||||
|
|
|
@ -20,6 +20,7 @@ COPY generate_translations.js \
|
||||||
|
|
||||||
COPY lemmy-translations lemmy-translations
|
COPY lemmy-translations lemmy-translations
|
||||||
COPY src src
|
COPY src src
|
||||||
|
COPY .git .git
|
||||||
|
|
||||||
# Set UI version
|
# Set UI version
|
||||||
RUN echo "export const VERSION = 'dev';" > "src/shared/version.ts"
|
RUN echo "export const VERSION = 'dev';" > "src/shared/version.ts"
|
||||||
|
|
11
package.json
11
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lemmy-ui",
|
"name": "lemmy-ui",
|
||||||
"version": "0.18.1-rc.3",
|
"version": "0.18.1-rc.7",
|
||||||
"description": "An isomorphic UI for lemmy",
|
"description": "An isomorphic UI for lemmy",
|
||||||
"repository": "https://github.com/LemmyNet/lemmy-ui",
|
"repository": "https://github.com/LemmyNet/lemmy-ui",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
|
@ -8,9 +8,9 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "webpack --mode=none",
|
"analyze": "webpack --mode=none",
|
||||||
"prebuild:dev": "yarn clean && node generate_translations.js",
|
"prebuild:dev": "yarn clean && node generate_translations.js",
|
||||||
"build:dev": "webpack --mode=development",
|
"build:dev": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=development",
|
||||||
"prebuild:prod": "yarn clean && node generate_translations.js",
|
"prebuild:prod": "yarn clean && node generate_translations.js",
|
||||||
"build:prod": "webpack --mode=production",
|
"build:prod": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=production",
|
||||||
"clean": "yarn run rimraf dist",
|
"clean": "yarn run rimraf dist",
|
||||||
"dev": "yarn build:dev --watch",
|
"dev": "yarn build:dev --watch",
|
||||||
"lint": "yarn translations:generate && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx \"src/**\" && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"",
|
"lint": "yarn translations:generate && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx \"src/**\" && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"",
|
||||||
|
@ -48,11 +48,11 @@
|
||||||
"check-password-strength": "^2.0.7",
|
"check-password-strength": "^2.0.7",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
|
"cookie": "^0.5.0",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"cross-fetch": "^3.1.5",
|
"cross-fetch": "^3.1.5",
|
||||||
"css-loader": "^6.7.3",
|
"css-loader": "^6.7.3",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"date-fns-tz": "^2.0.0",
|
|
||||||
"emoji-mart": "^5.4.0",
|
"emoji-mart": "^5.4.0",
|
||||||
"emoji-short-name": "^2.0.0",
|
"emoji-short-name": "^2.0.0",
|
||||||
"express": "~4.18.2",
|
"express": "~4.18.2",
|
||||||
|
@ -66,7 +66,6 @@
|
||||||
"inferno-i18next-dess": "0.0.2",
|
"inferno-i18next-dess": "0.0.2",
|
||||||
"inferno-router": "^8.1.1",
|
"inferno-router": "^8.1.1",
|
||||||
"inferno-server": "^8.1.1",
|
"inferno-server": "^8.1.1",
|
||||||
"isomorphic-cookie": "^1.2.4",
|
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lemmy-js-client": "0.18.0-rc.2",
|
"lemmy-js-client": "0.18.0-rc.2",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
|
@ -98,6 +97,7 @@
|
||||||
"@babel/core": "^7.21.8",
|
"@babel/core": "^7.21.8",
|
||||||
"@types/autosize": "^4.0.0",
|
"@types/autosize": "^4.0.0",
|
||||||
"@types/bootstrap": "^5.2.6",
|
"@types/bootstrap": "^5.2.6",
|
||||||
|
"@types/cookie": "^0.5.1",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/html-to-text": "^9.0.0",
|
"@types/html-to-text": "^9.0.0",
|
||||||
"@types/lodash.isequal": "^4.5.6",
|
"@types/lodash.isequal": "^4.5.6",
|
||||||
|
@ -126,6 +126,7 @@
|
||||||
"style-loader": "^3.3.2",
|
"style-loader": "^3.3.2",
|
||||||
"terser": "^5.17.3",
|
"terser": "^5.17.3",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
|
"typescript-language-server": "^3.3.2",
|
||||||
"webpack-bundle-analyzer": "^4.9.0",
|
"webpack-bundle-analyzer": "^4.9.0",
|
||||||
"webpack-dev-server": "4.15.0"
|
"webpack-dev-server": "4.15.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { initializeSite, isAuthPath } from "@utils/app";
|
import { initializeSite, isAuthPath } from "@utils/app";
|
||||||
import { getHttpBaseInternal } from "@utils/env";
|
import { getHttpBaseInternal } from "@utils/env";
|
||||||
import { ErrorPageData } from "@utils/types";
|
import { ErrorPageData } from "@utils/types";
|
||||||
|
import * as cookie from "cookie";
|
||||||
import fetch from "cross-fetch";
|
import fetch from "cross-fetch";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { StaticRouter, matchPath } from "inferno-router";
|
import { StaticRouter, matchPath } from "inferno-router";
|
||||||
import { renderToString } from "inferno-server";
|
import { renderToString } from "inferno-server";
|
||||||
import IsomorphicCookie from "isomorphic-cookie";
|
|
||||||
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
||||||
import { App } from "../../shared/components/app/app";
|
import { App } from "../../shared/components/app/app";
|
||||||
import {
|
import {
|
||||||
|
@ -25,11 +25,15 @@ import { setForwardedHeaders } from "../utils/set-forwarded-headers";
|
||||||
export default async (req: Request, res: Response) => {
|
export default async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const activeRoute = routes.find(route => matchPath(req.path, route));
|
const activeRoute = routes.find(route => matchPath(req.path, route));
|
||||||
let auth: string | undefined = IsomorphicCookie.load("jwt", req);
|
|
||||||
|
let auth = req.headers.cookie
|
||||||
|
? cookie.parse(req.headers.cookie).jwt
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const getSiteForm: GetSite = { auth };
|
const getSiteForm: GetSite = { auth };
|
||||||
|
|
||||||
const headers = setForwardedHeaders(req.headers);
|
const headers = setForwardedHeaders(req.headers);
|
||||||
|
|
||||||
const client = wrapClient(
|
const client = wrapClient(
|
||||||
new LemmyHttp(getHttpBaseInternal(), { fetchFunction: fetch, headers })
|
new LemmyHttp(getHttpBaseInternal(), { fetchFunction: fetch, headers })
|
||||||
);
|
);
|
||||||
|
@ -43,6 +47,7 @@ export default async (req: Request, res: Response) => {
|
||||||
let routeData: RouteData = {};
|
let routeData: RouteData = {};
|
||||||
let errorPageData: ErrorPageData | undefined = undefined;
|
let errorPageData: ErrorPageData | undefined = undefined;
|
||||||
let try_site = await client.getSite(getSiteForm);
|
let try_site = await client.getSite(getSiteForm);
|
||||||
|
|
||||||
if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
|
if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
|
||||||
console.error(
|
console.error(
|
||||||
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
|
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
|
||||||
|
@ -91,6 +96,7 @@ export default async (req: Request, res: Response) => {
|
||||||
// Redirect to the 404 if there's an API error
|
// Redirect to the 404 if there's an API error
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error.msg);
|
console.error(error.msg);
|
||||||
|
|
||||||
if (error.msg === "instance_is_private") {
|
if (error.msg === "instance_is_private") {
|
||||||
return res.redirect(`/signup`);
|
return res.redirect(`/signup`);
|
||||||
} else {
|
} else {
|
||||||
|
@ -119,6 +125,7 @@ export default async (req: Request, res: Response) => {
|
||||||
// If an error is caught here, the error page couldn't even be rendered
|
// If an error is caught here, the error page couldn't even be rendered
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.statusCode = 500;
|
res.statusCode = 500;
|
||||||
|
|
||||||
return res.send(
|
return res.send(
|
||||||
process.env.NODE_ENV === "development" ? err.message : "Server error"
|
process.env.NODE_ENV === "development" ? err.message : "Server error"
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { setupDateFns } from "@utils/app";
|
import { setupDateFns } from "@utils/app";
|
||||||
|
import { getStaticDir } from "@utils/env";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
|
@ -19,7 +20,13 @@ const [hostname, port] = process.env["LEMMY_UI_HOST"]
|
||||||
|
|
||||||
server.use(express.json());
|
server.use(express.json());
|
||||||
server.use(express.urlencoded({ extended: false }));
|
server.use(express.urlencoded({ extended: false }));
|
||||||
server.use("/static", express.static(path.resolve("./dist")));
|
server.use(
|
||||||
|
getStaticDir(),
|
||||||
|
express.static(path.resolve("./dist"), {
|
||||||
|
maxAge: 24 * 60 * 60 * 1000, // 1 day
|
||||||
|
immutable: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
server.use(setCacheControl);
|
server.use(setCacheControl);
|
||||||
|
|
||||||
if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {
|
if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { NextFunction, Response } from "express";
|
import type { NextFunction, Request, Response } from "express";
|
||||||
import { UserService } from "../shared/services";
|
import { hasJwtCookie } from "./utils/has-jwt-cookie";
|
||||||
|
|
||||||
export function setDefaultCsp({
|
export function setDefaultCsp({
|
||||||
res,
|
res,
|
||||||
|
@ -18,24 +18,35 @@ export function setDefaultCsp({
|
||||||
|
|
||||||
// Set cache-control headers. If user is logged in, set `private` to prevent storing data in
|
// Set cache-control headers. If user is logged in, set `private` to prevent storing data in
|
||||||
// shared caches (eg nginx) and leaking of private data. If user is not logged in, allow caching
|
// shared caches (eg nginx) and leaking of private data. If user is not logged in, allow caching
|
||||||
// all responses for 60 seconds to reduce load on backend and database. The specific cache
|
// all responses for 5 seconds to reduce load on backend and database. The specific cache
|
||||||
// interval is rather arbitrary and could be set higher (less server load) or lower (fresher data).
|
// interval is rather arbitrary and could be set higher (less server load) or lower (fresher data).
|
||||||
//
|
//
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
||||||
export function setCacheControl({
|
export function setCacheControl(
|
||||||
res,
|
req: Request,
|
||||||
next,
|
res: Response,
|
||||||
}: {
|
next: NextFunction
|
||||||
res: Response;
|
) {
|
||||||
next: NextFunction;
|
if (process.env.NODE_ENV !== "production") {
|
||||||
}) {
|
return next();
|
||||||
const user = UserService.Instance;
|
|
||||||
let caching: string;
|
|
||||||
if (user.auth()) {
|
|
||||||
caching = "private";
|
|
||||||
} else {
|
|
||||||
caching = "public, max-age=60";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let caching: string;
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.path.match(/\.(js|css|txt|manifest\.webmanifest)\/?$/) ||
|
||||||
|
req.path.includes("/css/themelist")
|
||||||
|
) {
|
||||||
|
// Static content gets cached publicly for a day
|
||||||
|
caching = "public, max-age=86400";
|
||||||
|
} else {
|
||||||
|
if (hasJwtCookie(req)) {
|
||||||
|
caching = "private";
|
||||||
|
} else {
|
||||||
|
caching = "public, max-age=5";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.setHeader("Cache-Control", caching);
|
res.setHeader("Cache-Control", caching);
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getStaticDir } from "@utils/env";
|
||||||
import { Helmet } from "inferno-helmet";
|
import { Helmet } from "inferno-helmet";
|
||||||
import { renderToString } from "inferno-server";
|
import { renderToString } from "inferno-server";
|
||||||
import serialize from "serialize-javascript";
|
import serialize from "serialize-javascript";
|
||||||
|
@ -23,7 +24,7 @@ export async function createSsrHtml(
|
||||||
|
|
||||||
if (!appleTouchIcon) {
|
if (!appleTouchIcon) {
|
||||||
appleTouchIcon = site?.site_view.site.icon
|
appleTouchIcon = site?.site_view.site.icon
|
||||||
? `data:image/png;base64,${sharp(
|
? `data:image/png;base64,${await sharp(
|
||||||
await fetchIconPng(site.site_view.site.icon)
|
await fetchIconPng(site.site_view.site.icon)
|
||||||
)
|
)
|
||||||
.resize(180, 180)
|
.resize(180, 180)
|
||||||
|
@ -87,7 +88,7 @@ export async function createSsrHtml(
|
||||||
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
|
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
|
||||||
|
|
||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
|
<link rel="stylesheet" type="text/css" href="${getStaticDir()}/styles/styles.css" />
|
||||||
|
|
||||||
<!-- Current theme and more -->
|
<!-- Current theme and more -->
|
||||||
${helmet.link.toString() || fallbackTheme}
|
${helmet.link.toString() || fallbackTheme}
|
||||||
|
@ -102,7 +103,7 @@ export async function createSsrHtml(
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
<div id='root'>${root}</div>
|
<div id='root'>${root}</div>
|
||||||
<script defer src='/static/js/client.js'></script>
|
<script defer src='${getStaticDir()}/js/client.js'></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
|
|
6
src/server/utils/has-jwt-cookie.ts
Normal file
6
src/server/utils/has-jwt-cookie.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import * as cookie from "cookie";
|
||||||
|
import type { Request } from "express";
|
||||||
|
|
||||||
|
export function hasJwtCookie(req: Request): boolean {
|
||||||
|
return Boolean(cookie.parse(req.headers.cookie ?? "").jwt?.length);
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getStaticDir } from "@utils/env";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
|
@ -23,7 +24,9 @@ export class Icon extends Component<IconProps, any> {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<use
|
<use
|
||||||
xlinkHref={`/static/assets/symbols.svg#icon-${this.props.icon}`}
|
xlinkHref={`${getStaticDir()}/assets/symbols.svg#icon-${
|
||||||
|
this.props.icon
|
||||||
|
}`}
|
||||||
></use>
|
></use>
|
||||||
<div className="visually-hidden">
|
<div className="visually-hidden">
|
||||||
<title>{this.props.icon}</title>
|
<title>{this.props.icon}</title>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { capitalizeFirstLetter, formatPastDate } from "@utils/helpers";
|
import { capitalizeFirstLetter, formatPastDate } from "@utils/helpers";
|
||||||
import { formatInTimeZone } from "date-fns-tz";
|
import { format } from "date-fns";
|
||||||
import parseISO from "date-fns/parseISO";
|
import parseISO from "date-fns/parseISO";
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
|
@ -13,9 +13,8 @@ interface MomentTimeProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(input: string) {
|
function formatDate(input: string) {
|
||||||
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
||||||
const parsed = parseISO(input + "Z");
|
const parsed = parseISO(input + "Z");
|
||||||
return formatInTimeZone(parsed, tz, "PPPPpppp");
|
return format(parsed, "PPPPpppp");
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MomentTime extends Component<MomentTimeProps, any> {
|
export class MomentTime extends Component<MomentTimeProps, any> {
|
||||||
|
|
|
@ -279,13 +279,15 @@ export class Home extends Component<any, HomeState> {
|
||||||
trendingCommunitiesRes,
|
trendingCommunitiesRes,
|
||||||
commentsRes,
|
commentsRes,
|
||||||
postsRes,
|
postsRes,
|
||||||
tagline: getRandomFromList(this.state?.siteRes?.taglines ?? [])
|
|
||||||
?.content,
|
|
||||||
isIsomorphic: true,
|
isIsomorphic: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
HomeCacheService.postsRes = postsRes;
|
HomeCacheService.postsRes = postsRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.state.tagline = getRandomFromList(
|
||||||
|
this.state?.siteRes?.taglines ?? []
|
||||||
|
)?.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|
|
@ -141,7 +141,7 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
|
||||||
|
|
||||||
handleEditTaglineClick(d: { i: TaglineForm; index: number }, event: any) {
|
handleEditTaglineClick(d: { i: TaglineForm; index: number }, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this.state.editingRow == d.index) {
|
if (d.i.state.editingRow == d.index) {
|
||||||
d.i.setState({ editingRow: undefined });
|
d.i.setState({ editingRow: undefined });
|
||||||
} else {
|
} else {
|
||||||
d.i.setState({ editingRow: d.index });
|
d.i.setState({ editingRow: d.index });
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { showAvatars } from "@utils/app";
|
import { showAvatars } from "@utils/app";
|
||||||
|
import { getStaticDir } from "@utils/env";
|
||||||
import { hostname, isCakeDay } from "@utils/helpers";
|
import { hostname, isCakeDay } from "@utils/helpers";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
|
@ -88,7 +89,7 @@ export class PersonListing extends Component<PersonListingProps, any> {
|
||||||
!this.props.person.banned &&
|
!this.props.person.banned &&
|
||||||
showAvatars() && (
|
showAvatars() && (
|
||||||
<PictrsImage
|
<PictrsImage
|
||||||
src={avatar ?? "/static/assets/icons/icon-96x96.png"}
|
src={avatar ?? `${getStaticDir()}/assets/icons/icon-96x96.png`}
|
||||||
icon
|
icon
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -333,7 +333,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="thumbnail rounded overflow-hidden d-inline-block position-relative mb-2 p-0 border-0"
|
className="thumbnail rounded overflow-hidden d-inline-block position-relative p-0 border-0"
|
||||||
data-tippy-content={I18NextService.i18n.t("expand_here")}
|
data-tippy-content={I18NextService.i18n.t("expand_here")}
|
||||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||||
aria-label={I18NextService.i18n.t("expand_here")}
|
aria-label={I18NextService.i18n.t("expand_here")}
|
||||||
|
@ -348,7 +348,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
} else if (!this.props.hideImage && url && thumbnail && this.imageSrc) {
|
} else if (!this.props.hideImage && url && thumbnail && this.imageSrc) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className="thumbnail rounded bg-light d-flex justify-content-center"
|
className="thumbnail rounded overflow-hidden d-inline-block position-relative p-0 border-0"
|
||||||
href={url}
|
href={url}
|
||||||
rel={relTags}
|
rel={relTags}
|
||||||
title={url}
|
title={url}
|
||||||
|
@ -403,8 +403,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
createdLine() {
|
createdLine() {
|
||||||
const post_view = this.postView;
|
const post_view = this.postView;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="small">
|
<div className="small mb-1 mb-md-0">
|
||||||
<span className="me-1">
|
<span className="me-1">
|
||||||
<PersonListing person={post_view.creator} />
|
<PersonListing person={post_view.creator} />
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
export const favIconUrl = "/static/assets/icons/favicon.svg";
|
import { getStaticDir } from "@utils/env";
|
||||||
export const favIconPngUrl = "/static/assets/icons/apple-touch-icon.png";
|
|
||||||
|
export const favIconUrl = `${getStaticDir()}/assets/icons/favicon.svg`;
|
||||||
|
export const favIconPngUrl = `${getStaticDir()}/assets/icons/apple-touch-icon.png`;
|
||||||
|
|
||||||
export const repoUrl = "https://github.com/LemmyNet";
|
export const repoUrl = "https://github.com/LemmyNet";
|
||||||
export const joinLemmyUrl = "https://join-lemmy.org";
|
export const joinLemmyUrl = "https://join-lemmy.org";
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { isAuthPath } from "@utils/app";
|
import { isAuthPath } from "@utils/app";
|
||||||
import { isBrowser } from "@utils/browser";
|
import { isBrowser } from "@utils/browser";
|
||||||
import { isHttps } from "@utils/env";
|
import { isHttps } from "@utils/env";
|
||||||
import IsomorphicCookie from "isomorphic-cookie";
|
import * as cookie from "cookie";
|
||||||
import jwt_decode from "jwt-decode";
|
import jwt_decode from "jwt-decode";
|
||||||
import { LoginResponse, MyUserInfo } from "lemmy-js-client";
|
import { LoginResponse, MyUserInfo } from "lemmy-js-client";
|
||||||
import { toast } from "../toast";
|
import { toast } from "../toast";
|
||||||
|
@ -31,9 +31,14 @@ export class UserService {
|
||||||
public login(res: LoginResponse) {
|
public login(res: LoginResponse) {
|
||||||
const expires = new Date();
|
const expires = new Date();
|
||||||
expires.setDate(expires.getDate() + 365);
|
expires.setDate(expires.getDate() + 365);
|
||||||
if (res.jwt) {
|
if (isBrowser() && res.jwt) {
|
||||||
toast(I18NextService.i18n.t("logged_in"));
|
toast(I18NextService.i18n.t("logged_in"));
|
||||||
IsomorphicCookie.save("jwt", res.jwt, { expires, secure: isHttps() });
|
document.cookie = cookie.serialize("jwt", res.jwt, {
|
||||||
|
expires,
|
||||||
|
secure: isHttps(),
|
||||||
|
domain: location.hostname,
|
||||||
|
sameSite: true,
|
||||||
|
});
|
||||||
this.#setJwtInfo();
|
this.#setJwtInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,8 +46,14 @@ export class UserService {
|
||||||
public logout() {
|
public logout() {
|
||||||
this.jwtInfo = undefined;
|
this.jwtInfo = undefined;
|
||||||
this.myUserInfo = undefined;
|
this.myUserInfo = undefined;
|
||||||
IsomorphicCookie.remove("jwt"); // TODO is sometimes unreliable for some reason
|
if (isBrowser()) {
|
||||||
document.cookie = "jwt=; Max-Age=0; path=/; domain=" + location.hostname;
|
document.cookie = cookie.serialize("jwt", "", {
|
||||||
|
maxAge: 0,
|
||||||
|
path: "/",
|
||||||
|
domain: location.hostname,
|
||||||
|
sameSite: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (isAuthPath(location.pathname)) {
|
if (isAuthPath(location.pathname)) {
|
||||||
location.replace("/");
|
location.replace("/");
|
||||||
} else {
|
} else {
|
||||||
|
@ -66,10 +77,11 @@ export class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
#setJwtInfo() {
|
#setJwtInfo() {
|
||||||
const jwt: string | undefined = IsomorphicCookie.load("jwt");
|
if (isBrowser()) {
|
||||||
|
const { jwt } = cookie.parse(document.cookie);
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
this.jwtInfo = { jwt, claims: jwt_decode(jwt) };
|
this.jwtInfo = { jwt, claims: jwt_decode(jwt) };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export default function isAuthPath(pathname: string) {
|
export default function isAuthPath(pathname: string) {
|
||||||
return /create_.*|inbox|settings|admin|reports|registration_applications/g.test(
|
return /^\/create_.*|inbox|settings|admin|reports|registration_applications/g.test(
|
||||||
pathname
|
pathname
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,16 @@ export default async function () {
|
||||||
lang = "en-US";
|
lang = "en-US";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if lang and country are the same, then date-fns expects only the lang
|
||||||
|
// eg: instead of "fr-FR", we should import just "fr"
|
||||||
|
|
||||||
|
if (lang.includes("-")) {
|
||||||
|
const parts = lang.split("-");
|
||||||
|
if (parts[0] === parts[1].toLowerCase()) {
|
||||||
|
lang = parts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const locale = (
|
const locale = (
|
||||||
await import(
|
await import(
|
||||||
/* webpackExclude: /\.js\.flow$/ */
|
/* webpackExclude: /\.js\.flow$/ */
|
||||||
|
|
5
src/shared/utils/env/get-static-dir.ts
vendored
Normal file
5
src/shared/utils/env/get-static-dir.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// Returns path to static directory, intended
|
||||||
|
// for cache-busting based on latest commit hash.
|
||||||
|
export default function getStaticDir() {
|
||||||
|
return `/static/${process.env.COMMIT_HASH}`;
|
||||||
|
}
|
2
src/shared/utils/env/index.ts
vendored
2
src/shared/utils/env/index.ts
vendored
|
@ -6,6 +6,7 @@ import getHttpBaseExternal from "./get-http-base-external";
|
||||||
import getHttpBaseInternal from "./get-http-base-internal";
|
import getHttpBaseInternal from "./get-http-base-internal";
|
||||||
import getInternalHost from "./get-internal-host";
|
import getInternalHost from "./get-internal-host";
|
||||||
import getSecure from "./get-secure";
|
import getSecure from "./get-secure";
|
||||||
|
import getStaticDir from "./get-static-dir";
|
||||||
import httpExternalPath from "./http-external-path";
|
import httpExternalPath from "./http-external-path";
|
||||||
import isHttps from "./is-https";
|
import isHttps from "./is-https";
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ export {
|
||||||
getHttpBaseInternal,
|
getHttpBaseInternal,
|
||||||
getInternalHost,
|
getInternalHost,
|
||||||
getSecure,
|
getSecure,
|
||||||
|
getStaticDir,
|
||||||
httpExternalPath,
|
httpExternalPath,
|
||||||
isHttps,
|
isHttps,
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,56 +14,63 @@ const banner = `
|
||||||
@license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL v3.0
|
@license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL v3.0
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const base = {
|
function getBase(env, mode) {
|
||||||
output: {
|
return {
|
||||||
filename: "js/server.js",
|
output: {
|
||||||
publicPath: "/",
|
filename: "js/server.js",
|
||||||
hashFunction: "xxhash64",
|
publicPath: "/",
|
||||||
},
|
hashFunction: "xxhash64",
|
||||||
resolve: {
|
|
||||||
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
|
||||||
alias: {
|
|
||||||
"@": path.resolve(__dirname, "src/"),
|
|
||||||
"@utils": path.resolve(__dirname, "src/shared/utils/"),
|
|
||||||
},
|
},
|
||||||
},
|
resolve: {
|
||||||
performance: {
|
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
||||||
hints: false,
|
alias: {
|
||||||
},
|
"@": path.resolve(__dirname, "src/"),
|
||||||
module: {
|
"@utils": path.resolve(__dirname, "src/shared/utils/"),
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.(scss|css)$/i,
|
|
||||||
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
test: /\.(js|jsx|tsx|ts)$/, // All ts and tsx files will be process by
|
performance: {
|
||||||
exclude: /node_modules/, // ignore node_modules
|
hints: false,
|
||||||
loader: "babel-loader",
|
},
|
||||||
},
|
module: {
|
||||||
// Due to some weird babel issue: https://github.com/webpack/webpack/issues/11467
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.m?js/,
|
test: /\.(scss|css)$/i,
|
||||||
resolve: {
|
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
|
||||||
fullySpecified: false,
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
|
test: /\.(js|jsx|tsx|ts)$/, // All ts and tsx files will be process by
|
||||||
|
exclude: /node_modules/, // ignore node_modules
|
||||||
|
loader: "babel-loader",
|
||||||
|
},
|
||||||
|
// Due to some weird babel issue: https://github.com/webpack/webpack/issues/11467
|
||||||
|
{
|
||||||
|
test: /\.m?js/,
|
||||||
|
resolve: {
|
||||||
|
fullySpecified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
"process.env.COMMIT_HASH": `"${env.COMMIT_HASH}"`,
|
||||||
|
"process.env.NODE_ENV": `"${mode}"`,
|
||||||
|
}),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: "styles/styles.css",
|
||||||
|
}),
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [{ from: "./src/assets", to: "./assets" }],
|
||||||
|
}),
|
||||||
|
new webpack.BannerPlugin({
|
||||||
|
banner,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
},
|
};
|
||||||
plugins: [
|
}
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
filename: "styles/styles.css",
|
|
||||||
}),
|
|
||||||
new CopyPlugin({
|
|
||||||
patterns: [{ from: "./src/assets", to: "./assets" }],
|
|
||||||
}),
|
|
||||||
new webpack.BannerPlugin({
|
|
||||||
banner,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const createServerConfig = (_env, mode) => {
|
const createServerConfig = (env, mode) => {
|
||||||
|
const base = getBase(env, mode);
|
||||||
const config = merge({}, base, {
|
const config = merge({}, base, {
|
||||||
mode,
|
mode,
|
||||||
entry: "./src/server/index.tsx",
|
entry: "./src/server/index.tsx",
|
||||||
|
@ -90,22 +97,20 @@ const createServerConfig = (_env, mode) => {
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createClientConfig = (_env, mode) => {
|
const createClientConfig = (env, mode) => {
|
||||||
|
const base = getBase(env, mode);
|
||||||
const config = merge({}, base, {
|
const config = merge({}, base, {
|
||||||
mode,
|
mode,
|
||||||
entry: "./src/client/index.tsx",
|
entry: "./src/client/index.tsx",
|
||||||
output: {
|
output: {
|
||||||
filename: "js/client.js",
|
filename: "js/client.js",
|
||||||
publicPath: "/static/",
|
publicPath: `/static/${env.COMMIT_HASH}/`,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
...base.plugins,
|
...base.plugins,
|
||||||
new ServiceWorkerPlugin({
|
new ServiceWorkerPlugin({
|
||||||
enableInDevelopment: mode !== "development", // this may seem counterintuitive, but it is correct
|
enableInDevelopment: mode !== "development", // this may seem counterintuitive, but it is correct
|
||||||
workbox: {
|
workbox: {
|
||||||
modifyURLPrefix: {
|
|
||||||
"/": "/static/",
|
|
||||||
},
|
|
||||||
cacheId: "lemmy",
|
cacheId: "lemmy",
|
||||||
include: [/(assets|styles|js)\/.+\..+$/g],
|
include: [/(assets|styles|js)\/.+\..+$/g],
|
||||||
inlineWorkboxRuntime: true,
|
inlineWorkboxRuntime: true,
|
||||||
|
|
Loading…
Reference in a new issue