Compare commits
35 commits
tweak/thum
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b6415f828e | ||
![]() |
cc184a86c8 | ||
![]() |
2d88e42cab | ||
![]() |
9e7fec772d | ||
![]() |
df39e0fe5d | ||
![]() |
fa41117320 | ||
![]() |
d8ee0ec78a | ||
![]() |
fead020bdc | ||
![]() |
339cefa2b0 | ||
![]() |
08370d4c4e | ||
![]() |
b73cb808e4 | ||
![]() |
80d9aac1ca | ||
![]() |
751495702c | ||
![]() |
ad6db69dda | ||
![]() |
6bf159261a | ||
![]() |
73be96880a | ||
![]() |
dc439b8dee | ||
![]() |
ec649d1911 | ||
![]() |
eee1f443a8 | ||
![]() |
67365d5878 | ||
![]() |
d9d4a3195f | ||
![]() |
005135d4f2 | ||
![]() |
75c2e803c9 | ||
![]() |
500003f310 | ||
![]() |
6b1ea447e5 | ||
![]() |
083e468df5 | ||
![]() |
6305f593d0 | ||
![]() |
92f6fe914f | ||
![]() |
9a04d0c58e | ||
![]() |
a2716d5f48 | ||
![]() |
bca438b2c6 | ||
![]() |
0764b75dc2 | ||
![]() |
a077924f38 | ||
![]() |
38a109ba92 | ||
![]() |
a30c112048 |
29 changed files with 310 additions and 256 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"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lemmy-ui",
|
"name": "lemmy-ui",
|
||||||
"version": "0.18.1-rc.2",
|
"version": "0.18.1-rc.5",
|
||||||
"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}\"",
|
||||||
|
|
|
@ -198,9 +198,9 @@ blockquote {
|
||||||
|
|
||||||
.thumbnail {
|
.thumbnail {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
aspect-ratio: 4/3;
|
aspect-ratio: 1/1;
|
||||||
width: 100%;
|
width: 5rem;
|
||||||
max-height: 6rem;
|
height: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail svg {
|
.thumbnail svg {
|
||||||
|
|
17
src/server/handlers/security-handler.ts
Normal file
17
src/server/handlers/security-handler.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import type { Response } from "express";
|
||||||
|
|
||||||
|
export default async ({ res }: { res: Response }) => {
|
||||||
|
res.setHeader("content-type", "text/plain; charset=utf-8");
|
||||||
|
|
||||||
|
res.send(
|
||||||
|
`Contact: mailto:security@lemmy.ml
|
||||||
|
Contact: mailto:admin@` +
|
||||||
|
process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST +
|
||||||
|
`
|
||||||
|
Contact: mailto:security@` +
|
||||||
|
process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST +
|
||||||
|
`
|
||||||
|
Expires: 2024-01-01T04:59:00.000Z
|
||||||
|
`
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,10 +1,12 @@
|
||||||
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";
|
||||||
import CatchAllHandler from "./handlers/catch-all-handler";
|
import CatchAllHandler from "./handlers/catch-all-handler";
|
||||||
import ManifestHandler from "./handlers/manifest-handler";
|
import ManifestHandler from "./handlers/manifest-handler";
|
||||||
import RobotsHandler from "./handlers/robots-handler";
|
import RobotsHandler from "./handlers/robots-handler";
|
||||||
|
import SecurityHandler from "./handlers/security-handler";
|
||||||
import ServiceWorkerHandler from "./handlers/service-worker-handler";
|
import ServiceWorkerHandler from "./handlers/service-worker-handler";
|
||||||
import ThemeHandler from "./handlers/theme-handler";
|
import ThemeHandler from "./handlers/theme-handler";
|
||||||
import ThemesListHandler from "./handlers/themes-list-handler";
|
import ThemesListHandler from "./handlers/themes-list-handler";
|
||||||
|
@ -18,13 +20,20 @@ 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"]) {
|
||||||
server.use(setDefaultCsp);
|
server.use(setDefaultCsp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server.get("/.well-known/security.txt", SecurityHandler);
|
||||||
server.get("/robots.txt", RobotsHandler);
|
server.get("/robots.txt", RobotsHandler);
|
||||||
server.get("/service-worker.js", ServiceWorkerHandler);
|
server.get("/service-worker.js", ServiceWorkerHandler);
|
||||||
server.get("/manifest.webmanifest", ManifestHandler);
|
server.get("/manifest.webmanifest", ManifestHandler);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { NextFunction, Response } from "express";
|
import type { NextFunction, Request, Response } from "express";
|
||||||
import { UserService } from "../shared/services";
|
import { UserService } from "../shared/services";
|
||||||
|
|
||||||
export function setDefaultCsp({
|
export function setDefaultCsp({
|
||||||
|
@ -10,7 +10,7 @@ export function setDefaultCsp({
|
||||||
}) {
|
}) {
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
"Content-Security-Policy",
|
"Content-Security-Policy",
|
||||||
`default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src *`
|
`default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src * data:`
|
||||||
);
|
);
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
@ -18,24 +18,33 @@ 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;
|
|
||||||
}) {
|
|
||||||
const user = UserService.Instance;
|
const user = UserService.Instance;
|
||||||
let caching;
|
let caching: string;
|
||||||
if (user.auth()) {
|
|
||||||
caching = "private";
|
if (
|
||||||
|
process.env.NODE_ENV === "production" &&
|
||||||
|
(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 {
|
} else {
|
||||||
caching = "public, max-age=60";
|
if (user.auth()) {
|
||||||
|
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>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -84,6 +84,8 @@ export class ImageUploadForm extends Component<
|
||||||
if (res.state === "success") {
|
if (res.state === "success") {
|
||||||
if (res.data.msg === "ok") {
|
if (res.data.msg === "ok") {
|
||||||
i.props.onUpload(res.data.url as string);
|
i.props.onUpload(res.data.url as string);
|
||||||
|
} else if (res.data.msg === "too_large") {
|
||||||
|
toast(I18NextService.i18n.t("upload_too_large"), "danger");
|
||||||
} else {
|
} else {
|
||||||
toast(JSON.stringify(res), "danger");
|
toast(JSON.stringify(res), "danger");
|
||||||
}
|
}
|
||||||
|
|
|
@ -443,6 +443,10 @@ export class MarkdownTextArea extends Component<
|
||||||
const textarea: any = document.getElementById(i.id);
|
const textarea: any = document.getElementById(i.id);
|
||||||
autosize.update(textarea);
|
autosize.update(textarea);
|
||||||
pictrsDeleteToast(image.name, res.data.delete_url as string);
|
pictrsDeleteToast(image.name, res.data.delete_url as string);
|
||||||
|
} else if (res.data.msg === "too_large") {
|
||||||
|
toast(I18NextService.i18n.t("upload_too_large"), "danger");
|
||||||
|
i.setState({ imageUploadStatus: undefined });
|
||||||
|
throw JSON.stringify(res.data);
|
||||||
} else {
|
} else {
|
||||||
throw JSON.stringify(res.data);
|
throw JSON.stringify(res.data);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { capitalizeFirstLetter, formatPastDate } from "@utils/helpers";
|
import { capitalizeFirstLetter, formatPastDate } from "@utils/helpers";
|
||||||
import format from "date-fns/format";
|
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,7 +13,8 @@ interface MomentTimeProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(input: string) {
|
function formatDate(input: string) {
|
||||||
return format(parseISO(input), "PPPPpppp");
|
const parsed = parseISO(input + "Z");
|
||||||
|
return format(parsed, "PPPPpppp");
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MomentTime extends Component<MomentTimeProps, any> {
|
export class MomentTime extends Component<MomentTimeProps, any> {
|
||||||
|
|
|
@ -284,7 +284,9 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
handleSearchSubmit(i: Communities, event: any) {
|
handleSearchSubmit(i: Communities, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const searchParamEncoded = encodeURIComponent(i.state.searchText);
|
const searchParamEncoded = encodeURIComponent(i.state.searchText);
|
||||||
i.context.router.history.push(`/search?q=${searchParamEncoded}`);
|
i.context.router.history.push(
|
||||||
|
`/search?q=${searchParamEncoded}&type=Communities`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fetchInitialData({
|
static async fetchInitialData({
|
||||||
|
|
|
@ -508,6 +508,8 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
|
||||||
{ form: form, index: index, overrideValue: res.data.url as string },
|
{ form: form, index: index, overrideValue: res.data.url as string },
|
||||||
event
|
event
|
||||||
);
|
);
|
||||||
|
} else if (res.data.msg === "too_large") {
|
||||||
|
toast(I18NextService.i18n.t("upload_too_large"), "danger");
|
||||||
} else {
|
} else {
|
||||||
toast(JSON.stringify(res), "danger");
|
toast(JSON.stringify(res), "danger");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -205,9 +205,7 @@ export class Setup extends Component<any, State> {
|
||||||
const data = i.state.registerRes.data;
|
const data = i.state.registerRes.data;
|
||||||
|
|
||||||
UserService.Instance.login(data);
|
UserService.Instance.login(data);
|
||||||
if (UserService.Instance.jwtInfo) {
|
i.setState({ doneRegisteringUser: true });
|
||||||
i.setState({ doneRegisteringUser: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -187,6 +187,8 @@ function handleImageUpload(i: PostForm, event: any) {
|
||||||
imageLoading: false,
|
imageLoading: false,
|
||||||
imageDeleteUrl: res.data.delete_url as string,
|
imageDeleteUrl: res.data.delete_url as string,
|
||||||
});
|
});
|
||||||
|
} else if (res.data.msg === "too_large") {
|
||||||
|
toast(I18NextService.i18n.t("upload_too_large"), "danger");
|
||||||
} else {
|
} else {
|
||||||
toast(JSON.stringify(res), "danger");
|
toast(JSON.stringify(res), "danger");
|
||||||
}
|
}
|
||||||
|
|
|
@ -333,7 +333,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="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="text-body d-inline-block position-relative mb-2"
|
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>
|
||||||
|
@ -702,6 +703,50 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{(this.canMod_ || this.canAdmin_) && (
|
{(this.canMod_ || this.canAdmin_) && (
|
||||||
<li>{this.modRemoveButton}</li>
|
<li>{this.modRemoveButton}</li>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{this.canMod_ && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<hr className="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
{!this.creatorIsMod_ &&
|
||||||
|
(!post_view.creator_banned_from_community ? (
|
||||||
|
<li>{this.modBanFromCommunityButton}</li>
|
||||||
|
) : (
|
||||||
|
<li>{this.modUnbanFromCommunityButton}</li>
|
||||||
|
))}
|
||||||
|
{!post_view.creator_banned_from_community && (
|
||||||
|
<li>{this.addModToCommunityButton}</li>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(amCommunityCreator(post_view.creator.id, this.props.moderators) ||
|
||||||
|
this.canAdmin_) &&
|
||||||
|
this.creatorIsMod_ && <li>{this.transferCommunityButton}</li>}
|
||||||
|
|
||||||
|
{/* Admins can ban from all, and appoint other admins */}
|
||||||
|
{this.canAdmin_ && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<hr className="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
{!this.creatorIsAdmin_ && (
|
||||||
|
<>
|
||||||
|
{!isBanned(post_view.creator) ? (
|
||||||
|
<li>{this.modBanButton}</li>
|
||||||
|
) : (
|
||||||
|
<li>{this.modUnbanButton}</li>
|
||||||
|
)}
|
||||||
|
<li>{this.purgePersonButton}</li>
|
||||||
|
<li>{this.purgePostButton}</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isBanned(post_view.creator) && post_view.creator.local && (
|
||||||
|
<li>{this.toggleAdminButton}</li>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -969,9 +1014,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get modBanFromCommunityButton() {
|
get modBanFromCommunityButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handleModBanFromCommunityShow)}
|
onClick={linkEvent(this, this.handleModBanFromCommunityShow)}
|
||||||
aria-label={I18NextService.i18n.t("ban_from_community")}
|
|
||||||
>
|
>
|
||||||
{I18NextService.i18n.t("ban_from_community")}
|
{I18NextService.i18n.t("ban_from_community")}
|
||||||
</button>
|
</button>
|
||||||
|
@ -981,9 +1025,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get modUnbanFromCommunityButton() {
|
get modUnbanFromCommunityButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}
|
onClick={linkEvent(this, this.handleModBanFromCommunitySubmit)}
|
||||||
aria-label={I18NextService.i18n.t("unban")}
|
|
||||||
>
|
>
|
||||||
{this.state.banLoading ? <Spinner /> : I18NextService.i18n.t("unban")}
|
{this.state.banLoading ? <Spinner /> : I18NextService.i18n.t("unban")}
|
||||||
</button>
|
</button>
|
||||||
|
@ -993,20 +1036,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get addModToCommunityButton() {
|
get addModToCommunityButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handleAddModToCommunity)}
|
onClick={linkEvent(this, this.handleAddModToCommunity)}
|
||||||
aria-label={
|
|
||||||
this.creatorIsMod_
|
|
||||||
? I18NextService.i18n.t("remove_as_mod")
|
|
||||||
: I18NextService.i18n.t("appoint_as_mod")
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{this.state.addModLoading ? (
|
{this.state.addModLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : this.creatorIsMod_ ? (
|
) : this.creatorIsMod_ ? (
|
||||||
I18NextService.i18n.t("remove_as_mod")
|
capitalizeFirstLetter(I18NextService.i18n.t("remove_as_mod"))
|
||||||
) : (
|
) : (
|
||||||
I18NextService.i18n.t("appoint_as_mod")
|
capitalizeFirstLetter(I18NextService.i18n.t("appoint_as_mod"))
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -1015,11 +1053,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get modBanButton() {
|
get modBanButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handleModBanShow)}
|
onClick={linkEvent(this, this.handleModBanShow)}
|
||||||
aria-label={I18NextService.i18n.t("ban_from_site")}
|
|
||||||
>
|
>
|
||||||
{I18NextService.i18n.t("ban_from_site")}
|
{capitalizeFirstLetter(I18NextService.i18n.t("ban_from_site"))}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1027,14 +1064,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get modUnbanButton() {
|
get modUnbanButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handleModBanSubmit)}
|
onClick={linkEvent(this, this.handleModBanSubmit)}
|
||||||
aria-label={I18NextService.i18n.t("unban_from_site")}
|
|
||||||
>
|
>
|
||||||
{this.state.banLoading ? (
|
{this.state.banLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
I18NextService.i18n.t("unban_from_site")
|
capitalizeFirstLetter(I18NextService.i18n.t("unban_from_site"))
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -1043,11 +1079,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get purgePersonButton() {
|
get purgePersonButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handlePurgePersonShow)}
|
onClick={linkEvent(this, this.handlePurgePersonShow)}
|
||||||
aria-label={I18NextService.i18n.t("purge_user")}
|
|
||||||
>
|
>
|
||||||
{I18NextService.i18n.t("purge_user")}
|
{capitalizeFirstLetter(I18NextService.i18n.t("purge_user"))}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1055,11 +1090,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get purgePostButton() {
|
get purgePostButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handlePurgePostShow)}
|
onClick={linkEvent(this, this.handlePurgePostShow)}
|
||||||
aria-label={I18NextService.i18n.t("purge_post")}
|
|
||||||
>
|
>
|
||||||
{I18NextService.i18n.t("purge_post")}
|
{capitalizeFirstLetter(I18NextService.i18n.t("purge_post"))}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1067,20 +1101,31 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get toggleAdminButton() {
|
get toggleAdminButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handleAddAdmin)}
|
onClick={linkEvent(this, this.handleAddAdmin)}
|
||||||
>
|
>
|
||||||
{this.state.addAdminLoading ? (
|
{this.state.addAdminLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : this.creatorIsAdmin_ ? (
|
) : this.creatorIsAdmin_ ? (
|
||||||
I18NextService.i18n.t("remove_as_admin")
|
capitalizeFirstLetter(I18NextService.i18n.t("remove_as_admin"))
|
||||||
) : (
|
) : (
|
||||||
I18NextService.i18n.t("appoint_as_admin")
|
capitalizeFirstLetter(I18NextService.i18n.t("appoint_as_admin"))
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get transferCommunityButton() {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
|
onClick={linkEvent(this, this.handleShowConfirmTransferCommunity)}
|
||||||
|
>
|
||||||
|
{capitalizeFirstLetter(I18NextService.i18n.t("transfer_community"))}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get modRemoveButton() {
|
get modRemoveButton() {
|
||||||
const removed = this.postView.post.removed;
|
const removed = this.postView.post.removed;
|
||||||
return (
|
return (
|
||||||
|
@ -1095,102 +1140,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{this.state.removeLoading ? (
|
{this.state.removeLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : !removed ? (
|
) : !removed ? (
|
||||||
I18NextService.i18n.t("remove")
|
capitalizeFirstLetter(I18NextService.i18n.t("remove_post"))
|
||||||
) : (
|
) : (
|
||||||
I18NextService.i18n.t("restore")
|
<>
|
||||||
|
{capitalizeFirstLetter(I18NextService.i18n.t("restore"))}{" "}
|
||||||
|
{I18NextService.i18n.t("post")}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mod/Admin actions to be taken against the author.
|
|
||||||
*/
|
|
||||||
userActionsLine() {
|
|
||||||
// TODO: make nicer
|
|
||||||
const post_view = this.postView;
|
|
||||||
return (
|
|
||||||
this.state.showAdvanced && (
|
|
||||||
<div className="mt-3">
|
|
||||||
{this.canMod_ && (
|
|
||||||
<>
|
|
||||||
{!this.creatorIsMod_ &&
|
|
||||||
(!post_view.creator_banned_from_community
|
|
||||||
? this.modBanFromCommunityButton
|
|
||||||
: this.modUnbanFromCommunityButton)}
|
|
||||||
{!post_view.creator_banned_from_community &&
|
|
||||||
this.addModToCommunityButton}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Community creators and admins can transfer community to another mod */}
|
|
||||||
{(amCommunityCreator(post_view.creator.id, this.props.moderators) ||
|
|
||||||
this.canAdmin_) &&
|
|
||||||
this.creatorIsMod_ &&
|
|
||||||
(!this.state.showConfirmTransferCommunity ? (
|
|
||||||
<button
|
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
|
||||||
onClick={linkEvent(
|
|
||||||
this,
|
|
||||||
this.handleShowConfirmTransferCommunity
|
|
||||||
)}
|
|
||||||
aria-label={I18NextService.i18n.t("transfer_community")}
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("transfer_community")}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="d-inline-block me-1 btn btn-link btn-animate text-muted py-0"
|
|
||||||
aria-label={I18NextService.i18n.t("are_you_sure")}
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("are_you_sure")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-link btn-animate text-muted py-0 d-inline-block me-1"
|
|
||||||
aria-label={I18NextService.i18n.t("yes")}
|
|
||||||
onClick={linkEvent(this, this.handleTransferCommunity)}
|
|
||||||
>
|
|
||||||
{this.state.transferLoading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
I18NextService.i18n.t("yes")
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-link btn-animate text-muted py-0 d-inline-block"
|
|
||||||
onClick={linkEvent(
|
|
||||||
this,
|
|
||||||
this.handleCancelShowConfirmTransferCommunity
|
|
||||||
)}
|
|
||||||
aria-label={I18NextService.i18n.t("no")}
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("no")}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
{/* Admins can ban from all, and appoint other admins */}
|
|
||||||
{this.canAdmin_ && (
|
|
||||||
<>
|
|
||||||
{!this.creatorIsAdmin_ && (
|
|
||||||
<>
|
|
||||||
{!isBanned(post_view.creator)
|
|
||||||
? this.modBanButton
|
|
||||||
: this.modUnbanButton}
|
|
||||||
{this.purgePersonButton}
|
|
||||||
{this.purgePostButton}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!isBanned(post_view.creator) &&
|
|
||||||
post_view.creator.local &&
|
|
||||||
this.toggleAdminButton}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAndBanDialogs() {
|
removeAndBanDialogs() {
|
||||||
const post = this.postView;
|
const post = this.postView;
|
||||||
const purgeTypeText =
|
const purgeTypeText =
|
||||||
|
@ -1218,11 +1178,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
value={this.state.removeReason}
|
value={this.state.removeReason}
|
||||||
onInput={linkEvent(this, this.handleModRemoveReasonChange)}
|
onInput={linkEvent(this, this.handleModRemoveReasonChange)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="submit" className="btn btn-secondary">
|
||||||
type="submit"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
aria-label={I18NextService.i18n.t("remove_post")}
|
|
||||||
>
|
|
||||||
{this.state.removeLoading ? (
|
{this.state.removeLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
|
@ -1231,6 +1187,33 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
{this.state.showConfirmTransferCommunity && (
|
||||||
|
<>
|
||||||
|
<button className="d-inline-block me-1 btn btn-link btn-animate text-muted py-0">
|
||||||
|
{I18NextService.i18n.t("are_you_sure")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-link btn-animate text-muted py-0 d-inline-block me-1"
|
||||||
|
onClick={linkEvent(this, this.handleTransferCommunity)}
|
||||||
|
>
|
||||||
|
{this.state.transferLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
I18NextService.i18n.t("yes")
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-link btn-animate text-muted py-0 d-inline-block"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleCancelShowConfirmTransferCommunity
|
||||||
|
)}
|
||||||
|
aria-label={I18NextService.i18n.t("no")}
|
||||||
|
>
|
||||||
|
{I18NextService.i18n.t("no")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{this.state.showBanDialog && (
|
{this.state.showBanDialog && (
|
||||||
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
|
||||||
<div className="mb-3 row col-12">
|
<div className="mb-3 row col-12">
|
||||||
|
@ -1284,11 +1267,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{/* <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
|
{/* <input type="date" class="form-control me-2" placeholder={I18NextService.i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
<div className="mb-3 row">
|
<div className="mb-3 row">
|
||||||
<button
|
<button type="submit" className="btn btn-secondary">
|
||||||
type="submit"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
aria-label={I18NextService.i18n.t("ban")}
|
|
||||||
>
|
|
||||||
{this.state.banLoading ? (
|
{this.state.banLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
|
@ -1317,11 +1296,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
value={this.state.reportReason}
|
value={this.state.reportReason}
|
||||||
onInput={linkEvent(this, this.handleReportReasonChange)}
|
onInput={linkEvent(this, this.handleReportReasonChange)}
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="submit" className="btn btn-secondary">
|
||||||
type="submit"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
aria-label={I18NextService.i18n.t("create_report")}
|
|
||||||
>
|
|
||||||
{this.state.reportLoading ? (
|
{this.state.reportLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
|
@ -1350,11 +1325,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{this.state.purgeLoading ? (
|
{this.state.purgeLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button type="submit" className="btn btn-secondary">
|
||||||
type="submit"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
aria-label={purgeTypeText}
|
|
||||||
>
|
|
||||||
{this.state.purgeLoading ? <Spinner /> : { purgeTypeText }}
|
{this.state.purgeLoading ? <Spinner /> : { purgeTypeText }}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
@ -1409,7 +1380,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{this.mobileThumbnail()}
|
{this.mobileThumbnail()}
|
||||||
|
|
||||||
{this.commentsLine(true)}
|
{this.commentsLine(true)}
|
||||||
{this.userActionsLine()}
|
|
||||||
{this.duplicatesLine()}
|
{this.duplicatesLine()}
|
||||||
{this.removeAndBanDialogs()}
|
{this.removeAndBanDialogs()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1433,15 +1403,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
)}
|
)}
|
||||||
<div className="col flex-grow-1">
|
<div className="col flex-grow-1">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-sm-3 col-lg-2 pe-0 post-media">
|
<div className="col flex-grow-0 px-0">
|
||||||
<div className="">{this.thumbnail()}</div>
|
<div className="">{this.thumbnail()}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-sm-9 col-lg-10">
|
<div className="col flex-grow-1">
|
||||||
{this.postTitleLine()}
|
{this.postTitleLine()}
|
||||||
{this.createdLine()}
|
{this.createdLine()}
|
||||||
{this.commentsLine()}
|
{this.commentsLine()}
|
||||||
{this.duplicatesLine()}
|
{this.duplicatesLine()}
|
||||||
{this.userActionsLine()}
|
|
||||||
{this.removeAndBanDialogs()}
|
{this.removeAndBanDialogs()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -332,9 +332,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
if (
|
if (!this.state.isIsomorphic) {
|
||||||
!(this.state.isIsomorphic || this.props.history.location.state?.searched)
|
|
||||||
) {
|
|
||||||
const promises = [this.fetchCommunities()];
|
const promises = [this.fetchCommunities()];
|
||||||
if (this.state.searchText) {
|
if (this.state.searchText) {
|
||||||
promises.push(this.search());
|
promises.push(this.search());
|
||||||
|
@ -432,7 +430,15 @@ export class Search extends Component<any, SearchState> {
|
||||||
q: query,
|
q: query,
|
||||||
auth,
|
auth,
|
||||||
};
|
};
|
||||||
resolveObjectResponse = await client.resolveObject(resolveObjectForm);
|
resolveObjectResponse = await HttpService.silent_client.resolveObject(
|
||||||
|
resolveObjectForm
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we return this object with a state of failed, the catch-all-handler will redirect
|
||||||
|
// to an error page, so we ignore it by covering up the error with the empty state.
|
||||||
|
if (resolveObjectResponse.state === "failed") {
|
||||||
|
resolveObjectResponse = { state: "empty" };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -950,7 +956,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
if (auth) {
|
if (auth) {
|
||||||
this.setState({ resolveObjectRes: { state: "loading" } });
|
this.setState({ resolveObjectRes: { state: "loading" } });
|
||||||
this.setState({
|
this.setState({
|
||||||
resolveObjectRes: await HttpService.client.resolveObject({
|
resolveObjectRes: await HttpService.silent_client.resolveObject({
|
||||||
q,
|
q,
|
||||||
auth,
|
auth,
|
||||||
}),
|
}),
|
||||||
|
@ -1097,10 +1103,6 @@ export class Search extends Component<any, SearchState> {
|
||||||
sort: sort ?? urlSort,
|
sort: sort ?? urlSort,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.props.history.push(`/search${getQueryString(queryParams)}`, {
|
this.props.history.push(`/search${getQueryString(queryParams)}`);
|
||||||
searched: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.search();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { getHttpBase } from "@utils/env";
|
import { getHttpBase } from "@utils/env";
|
||||||
import { LemmyHttp } from "lemmy-js-client";
|
import { LemmyHttp } from "lemmy-js-client";
|
||||||
import { toast } from "../../shared/toast";
|
import { toast } from "../toast";
|
||||||
import { I18NextService } from "./I18NextService";
|
import { I18NextService } from "./I18NextService";
|
||||||
|
|
||||||
type EmptyRequestState = {
|
export type EmptyRequestState = {
|
||||||
state: "empty";
|
state: "empty";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ export type WrappedLemmyHttp = {
|
||||||
class WrappedLemmyHttpClient {
|
class WrappedLemmyHttpClient {
|
||||||
#client: LemmyHttp;
|
#client: LemmyHttp;
|
||||||
|
|
||||||
constructor(client: LemmyHttp) {
|
constructor(client: LemmyHttp, silent = false) {
|
||||||
this.#client = client;
|
this.#client = client;
|
||||||
|
|
||||||
for (const key of Object.getOwnPropertyNames(
|
for (const key of Object.getOwnPropertyNames(
|
||||||
|
@ -61,8 +61,10 @@ class WrappedLemmyHttpClient {
|
||||||
state: !(res === undefined || res === null) ? "success" : "empty",
|
state: !(res === undefined || res === null) ? "success" : "empty",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`API error: ${error}`);
|
if (!silent) {
|
||||||
toast(I18NextService.i18n.t(error), "danger");
|
console.error(`API error: ${error}`);
|
||||||
|
toast(I18NextService.i18n.t(error), "danger");
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
state: "failed",
|
state: "failed",
|
||||||
msg: error,
|
msg: error,
|
||||||
|
@ -74,16 +76,23 @@ class WrappedLemmyHttpClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wrapClient(client: LemmyHttp) {
|
export function wrapClient(client: LemmyHttp, silent = false) {
|
||||||
return new WrappedLemmyHttpClient(client) as unknown as WrappedLemmyHttp; // unfortunately, this verbose cast is necessary
|
// unfortunately, this verbose cast is necessary
|
||||||
|
return new WrappedLemmyHttpClient(
|
||||||
|
client,
|
||||||
|
silent
|
||||||
|
) as unknown as WrappedLemmyHttp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HttpService {
|
export class HttpService {
|
||||||
static #_instance: HttpService;
|
static #_instance: HttpService;
|
||||||
|
#silent_client: WrappedLemmyHttp;
|
||||||
#client: WrappedLemmyHttp;
|
#client: WrappedLemmyHttp;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.#client = wrapClient(new LemmyHttp(getHttpBase()));
|
const lemmyHttp = new LemmyHttp(getHttpBase());
|
||||||
|
this.#client = wrapClient(lemmyHttp);
|
||||||
|
this.#silent_client = wrapClient(lemmyHttp, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get #Instance() {
|
static get #Instance() {
|
||||||
|
@ -93,4 +102,8 @@ export class HttpService {
|
||||||
public static get client() {
|
public static get client() {
|
||||||
return this.#Instance.#client;
|
return this.#Instance.#client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static get silent_client() {
|
||||||
|
return this.#Instance.#silent_client;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,11 +2,8 @@ import formatDistanceStrict from "date-fns/formatDistanceStrict";
|
||||||
import parseISO from "date-fns/parseISO";
|
import parseISO from "date-fns/parseISO";
|
||||||
|
|
||||||
export default function (dateString?: string) {
|
export default function (dateString?: string) {
|
||||||
return formatDistanceStrict(
|
const parsed = parseISO((dateString ?? Date.now().toString()) + "Z");
|
||||||
parseISO(dateString ?? Date.now().toString()),
|
return formatDistanceStrict(parsed, new Date(), {
|
||||||
new Date(),
|
addSuffix: true,
|
||||||
{
|
});
|
||||||
addSuffix: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,12 +97,14 @@ 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/${env.COMMIT_HASH}/`,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
...base.plugins,
|
...base.plugins,
|
||||||
|
@ -103,10 +112,10 @@ const createClientConfig = (_env, mode) => {
|
||||||
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: {
|
modifyURLPrefix: {
|
||||||
"/": "/static/",
|
"/": `/static/${env.COMMIT_HASH}/`,
|
||||||
},
|
},
|
||||||
cacheId: "lemmy",
|
cacheId: "lemmy",
|
||||||
include: [/(assets|styles)\/.+\..+|client\.js$/g],
|
include: [/(assets|styles|js)\/.+\..+$/g],
|
||||||
inlineWorkboxRuntime: true,
|
inlineWorkboxRuntime: true,
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue