mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-23 12:51:13 +00:00
Merge branch 'main' into remote-follow
This commit is contained in:
commit
89c366963e
34 changed files with 501 additions and 361 deletions
|
@ -42,6 +42,9 @@ FROM node:alpine as runner
|
||||||
COPY --from=builder /usr/src/app/dist /app/dist
|
COPY --from=builder /usr/src/app/dist /app/dist
|
||||||
COPY --from=builder /usr/src/app/node_modules /app/node_modules
|
COPY --from=builder /usr/src/app/node_modules /app/node_modules
|
||||||
|
|
||||||
|
RUN chown -R node:node /app
|
||||||
|
|
||||||
|
USER node
|
||||||
EXPOSE 1234
|
EXPOSE 1234
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
CMD node dist/js/server.js
|
CMD node dist/js/server.js
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 713ceed9c7ef84deaa222e68361e670e0763cd83
|
Subproject commit a1a19aea1ad7d91195775a5ccea62ccc9076a2c7
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lemmy-ui",
|
"name": "lemmy-ui",
|
||||||
"version": "0.18.1",
|
"version": "0.18.2-rc.1",
|
||||||
"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",
|
||||||
|
@ -69,7 +69,6 @@
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lemmy-js-client": "0.18.1",
|
"lemmy-js-client": "0.18.1",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.merge": "^4.6.2",
|
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"markdown-it-container": "^3.0.0",
|
"markdown-it-container": "^3.0.0",
|
||||||
"markdown-it-emoji": "^2.0.2",
|
"markdown-it-emoji": "^2.0.2",
|
||||||
|
|
|
@ -258,5 +258,12 @@
|
||||||
<path d="M8.72046 10.6397L14.9999 7.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M8.72046 10.6397L14.9999 7.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M8.70605 13.353L15 16.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M8.70605 13.353L15 16.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol id="icon-eye" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-eye-slash" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||||
|
</symbol>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
|
@ -120,7 +120,7 @@ export default async (req: Request, res: Response) => {
|
||||||
|
|
||||||
const root = renderToString(wrapper);
|
const root = renderToString(wrapper);
|
||||||
|
|
||||||
res.send(await createSsrHtml(root, isoData));
|
res.send(await createSsrHtml(root, isoData, res.locals.cspNonce));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 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);
|
||||||
|
|
|
@ -15,5 +15,6 @@ export default async ({ res }: { res: Response }) => {
|
||||||
Disallow: /admin
|
Disallow: /admin
|
||||||
Disallow: /password_change
|
Disallow: /password_change
|
||||||
Disallow: /search/
|
Disallow: /search/
|
||||||
|
Disallow: /modlog
|
||||||
`);
|
`);
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,7 +29,11 @@ server.use(
|
||||||
);
|
);
|
||||||
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"] &&
|
||||||
|
process.env["NODE_ENV"] !== "development"
|
||||||
|
) {
|
||||||
server.use(setDefaultCsp);
|
server.use(setDefaultCsp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import * as crypto from "crypto";
|
||||||
import type { NextFunction, Request, Response } from "express";
|
import type { NextFunction, Request, Response } from "express";
|
||||||
import { hasJwtCookie } from "./utils/has-jwt-cookie";
|
import { hasJwtCookie } from "./utils/has-jwt-cookie";
|
||||||
|
|
||||||
|
@ -8,9 +9,20 @@ export function setDefaultCsp({
|
||||||
res: Response;
|
res: Response;
|
||||||
next: NextFunction;
|
next: NextFunction;
|
||||||
}) {
|
}) {
|
||||||
|
res.locals.cspNonce = crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
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 * data:`
|
`default-src 'self';
|
||||||
|
manifest-src *;
|
||||||
|
connect-src *;
|
||||||
|
img-src * data:;
|
||||||
|
script-src 'self' 'nonce-${res.locals.cspNonce}';
|
||||||
|
style-src 'self' 'unsafe-inline';
|
||||||
|
form-action 'self';
|
||||||
|
base-uri 'self';
|
||||||
|
frame-src *;
|
||||||
|
media-src * data:`.replace(/\s+/g, " ")
|
||||||
);
|
);
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { renderToString } from "inferno-server";
|
||||||
import serialize from "serialize-javascript";
|
import serialize from "serialize-javascript";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { favIconPngUrl, favIconUrl } from "../../shared/config";
|
import { favIconPngUrl, favIconUrl } from "../../shared/config";
|
||||||
import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces";
|
import { IsoDataOptionalSite } from "../../shared/interfaces";
|
||||||
import { buildThemeList } from "./build-themes-list";
|
import { buildThemeList } from "./build-themes-list";
|
||||||
import { fetchIconPng } from "./fetch-icon-png";
|
import { fetchIconPng } from "./fetch-icon-png";
|
||||||
|
|
||||||
|
@ -14,7 +14,8 @@ let appleTouchIcon: string | undefined = undefined;
|
||||||
|
|
||||||
export async function createSsrHtml(
|
export async function createSsrHtml(
|
||||||
root: string,
|
root: string,
|
||||||
isoData: IsoDataOptionalSite
|
isoData: IsoDataOptionalSite,
|
||||||
|
cspNonce: string
|
||||||
) {
|
) {
|
||||||
const site = isoData.site_res;
|
const site = isoData.site_res;
|
||||||
|
|
||||||
|
@ -22,6 +23,12 @@ export async function createSsrHtml(
|
||||||
(await buildThemeList())[0]
|
(await buildThemeList())[0]
|
||||||
}.css" />`;
|
}.css" />`;
|
||||||
|
|
||||||
|
const customHtmlHeaderScriptTag = new RegExp("<script", "g");
|
||||||
|
const customHtmlHeaderWithNonce = customHtmlHeader.replace(
|
||||||
|
customHtmlHeaderScriptTag,
|
||||||
|
`<script nonce="${cspNonce}"`
|
||||||
|
);
|
||||||
|
|
||||||
if (!appleTouchIcon) {
|
if (!appleTouchIcon) {
|
||||||
appleTouchIcon = site?.site_view.site.icon
|
appleTouchIcon = site?.site_view.site.icon
|
||||||
? `data:image/png;base64,${await sharp(
|
? `data:image/png;base64,${await sharp(
|
||||||
|
@ -45,28 +52,28 @@ export async function createSsrHtml(
|
||||||
process.env["LEMMY_UI_DEBUG"] === "true"
|
process.env["LEMMY_UI_DEBUG"] === "true"
|
||||||
? renderToString(
|
? renderToString(
|
||||||
<>
|
<>
|
||||||
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
<script
|
||||||
<script>eruda.init();</script>
|
nonce={cspNonce}
|
||||||
|
src="//cdn.jsdelivr.net/npm/eruda"
|
||||||
|
></script>
|
||||||
|
<script nonce={cspNonce}>eruda.init();</script>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const helmet = Helmet.renderStatic();
|
const helmet = Helmet.renderStatic();
|
||||||
|
|
||||||
const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST };
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html ${helmet.htmlAttributes.toString()}>
|
<html ${helmet.htmlAttributes.toString()}>
|
||||||
<head>
|
<head>
|
||||||
<script>window.isoData = ${serialize(isoData)}</script>
|
<script nonce="${cspNonce}">window.isoData = ${serialize(isoData)}</script>
|
||||||
<script>window.lemmyConfig = ${serialize(config)}</script>
|
|
||||||
|
|
||||||
<!-- A remote debugging utility for mobile -->
|
<!-- A remote debugging utility for mobile -->
|
||||||
${erudaStr}
|
${erudaStr}
|
||||||
|
|
||||||
<!-- Custom injected script -->
|
<!-- Custom injected script -->
|
||||||
${customHtmlHeader}
|
${customHtmlHeaderWithNonce}
|
||||||
|
|
||||||
${helmet.title.toString()}
|
${helmet.title.toString()}
|
||||||
${helmet.meta.toString()}
|
${helmet.meta.toString()}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { getHttpBaseExternal } from "@utils/env";
|
|
||||||
import { readFile } from "fs/promises";
|
import { readFile } from "fs/promises";
|
||||||
import { GetSiteResponse } from "lemmy-js-client";
|
import { GetSiteResponse } from "lemmy-js-client";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
@ -21,15 +20,13 @@ export default async function ({
|
||||||
local_site: { community_creation_admin_only },
|
local_site: { community_creation_admin_only },
|
||||||
},
|
},
|
||||||
}: GetSiteResponse) {
|
}: GetSiteResponse) {
|
||||||
const url = getHttpBaseExternal();
|
|
||||||
|
|
||||||
const icon = site.icon ? await fetchIconPng(site.icon) : null;
|
const icon = site.icon ? await fetchIconPng(site.icon) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: site.name,
|
name: site.name,
|
||||||
description: site.description ?? "A link aggregator for the fediverse",
|
description: site.description ?? "A link aggregator for the fediverse",
|
||||||
start_url: url,
|
start_url: "/",
|
||||||
scope: url,
|
scope: "/",
|
||||||
display: "standalone",
|
display: "standalone",
|
||||||
id: "/",
|
id: "/",
|
||||||
background_color: "#222222",
|
background_color: "#222222",
|
||||||
|
|
|
@ -21,7 +21,10 @@ export class Theme extends Component<Props> {
|
||||||
/>
|
/>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
);
|
);
|
||||||
} else if (this.props.defaultTheme != "browser") {
|
} else if (
|
||||||
|
this.props.defaultTheme != "browser" &&
|
||||||
|
this.props.defaultTheme != "browser-compact"
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<link
|
<link
|
||||||
|
@ -31,6 +34,25 @@ export class Theme extends Component<Props> {
|
||||||
/>
|
/>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
);
|
);
|
||||||
|
} else if (this.props.defaultTheme == "browser-compact") {
|
||||||
|
return (
|
||||||
|
<Helmet>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
type="text/css"
|
||||||
|
href="/css/themes/litely-compact.css"
|
||||||
|
id="default-light"
|
||||||
|
media="(prefers-color-scheme: light)"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
type="text/css"
|
||||||
|
href="/css/themes/darkly-compact.css"
|
||||||
|
id="default-dark"
|
||||||
|
media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)"
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|
|
@ -114,7 +114,7 @@ interface CommentNodeProps {
|
||||||
moderators?: CommunityModeratorView[];
|
moderators?: CommunityModeratorView[];
|
||||||
admins?: PersonView[];
|
admins?: PersonView[];
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
noIndent?: boolean;
|
isTopLevel?: boolean;
|
||||||
viewOnly?: boolean;
|
viewOnly?: boolean;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
markable?: boolean;
|
markable?: boolean;
|
||||||
|
@ -292,11 +292,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
mark: this.isCommentNew || this.commentView.comment.distinguished,
|
mark: this.isCommentNew || this.commentView.comment.distinguished,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div className="ms-2">
|
||||||
className={classNames({
|
|
||||||
"ms-2": !this.props.noIndent,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="d-flex flex-wrap align-items-center text-muted small">
|
<div className="d-flex flex-wrap align-items-center text-muted small">
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-link text-muted me-2"
|
className="btn btn-sm btn-link text-muted me-2"
|
||||||
|
@ -1136,7 +1132,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
allLanguages={this.props.allLanguages}
|
allLanguages={this.props.allLanguages}
|
||||||
siteLanguages={this.props.siteLanguages}
|
siteLanguages={this.props.siteLanguages}
|
||||||
hideImages={this.props.hideImages}
|
hideImages={this.props.hideImages}
|
||||||
isChild={!this.props.noIndent}
|
isChild={!this.props.isTopLevel}
|
||||||
depth={this.props.node.depth + 1}
|
depth={this.props.node.depth + 1}
|
||||||
finished={this.props.finished}
|
finished={this.props.finished}
|
||||||
onCommentReplyRead={this.props.onCommentReplyRead}
|
onCommentReplyRead={this.props.onCommentReplyRead}
|
||||||
|
|
|
@ -35,7 +35,7 @@ interface CommentNodesProps {
|
||||||
admins?: PersonView[];
|
admins?: PersonView[];
|
||||||
maxCommentsShown?: number;
|
maxCommentsShown?: number;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
noIndent?: boolean;
|
isTopLevel?: boolean;
|
||||||
viewOnly?: boolean;
|
viewOnly?: boolean;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
markable?: boolean;
|
markable?: boolean;
|
||||||
|
@ -86,7 +86,7 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
|
||||||
this.props.nodes.length > 0 && (
|
this.props.nodes.length > 0 && (
|
||||||
<ul
|
<ul
|
||||||
className={classNames("comments", {
|
className={classNames("comments", {
|
||||||
"ms-1": !!this.props.isChild,
|
"ms-1": this.props.depth && this.props.depth > 1,
|
||||||
"border-top border-light": !this.props.noBorder,
|
"border-top border-light": !this.props.noBorder,
|
||||||
})}
|
})}
|
||||||
style={
|
style={
|
||||||
|
@ -100,7 +100,7 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
|
||||||
key={node.comment_view.comment.id}
|
key={node.comment_view.comment.id}
|
||||||
node={node}
|
node={node}
|
||||||
noBorder={this.props.noBorder}
|
noBorder={this.props.noBorder}
|
||||||
noIndent={this.props.noIndent}
|
isTopLevel={this.props.isTopLevel}
|
||||||
viewOnly={this.props.viewOnly}
|
viewOnly={this.props.viewOnly}
|
||||||
locked={this.props.locked}
|
locked={this.props.locked}
|
||||||
moderators={this.props.moderators}
|
moderators={this.props.moderators}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { I18NextService } from "../../services";
|
||||||
interface HtmlTagsProps {
|
interface HtmlTagsProps {
|
||||||
title: string;
|
title: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
canonicalPath?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
}
|
}
|
||||||
|
@ -16,6 +17,8 @@ interface HtmlTagsProps {
|
||||||
export class HtmlTags extends Component<HtmlTagsProps, any> {
|
export class HtmlTags extends Component<HtmlTagsProps, any> {
|
||||||
render() {
|
render() {
|
||||||
const url = httpExternalPath(this.props.path);
|
const url = httpExternalPath(this.props.path);
|
||||||
|
const canonicalUrl =
|
||||||
|
this.props.canonicalPath ?? httpExternalPath(this.props.path);
|
||||||
const desc = this.props.description;
|
const desc = this.props.description;
|
||||||
const image = this.props.image;
|
const image = this.props.image;
|
||||||
|
|
||||||
|
@ -30,6 +33,8 @@ export class HtmlTags extends Component<HtmlTagsProps, any> {
|
||||||
<meta key={u} property={u} content={url} />
|
<meta key={u} property={u} content={url} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<link rel="canonical" href={canonicalUrl} />
|
||||||
|
|
||||||
{/* Open Graph / Facebook */}
|
{/* Open Graph / Facebook */}
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
|
|
||||||
|
|
157
src/shared/components/common/password-input.tsx
Normal file
157
src/shared/components/common/password-input.tsx
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
import { Options, passwordStrength } from "check-password-strength";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { NoOptionI18nKeys } from "i18next";
|
||||||
|
import { Component, FormEventHandler, linkEvent } from "inferno";
|
||||||
|
import { NavLink } from "inferno-router";
|
||||||
|
import { I18NextService } from "../../services";
|
||||||
|
import { Icon } from "./icon";
|
||||||
|
|
||||||
|
interface PasswordInputProps {
|
||||||
|
id: string;
|
||||||
|
value?: string;
|
||||||
|
onInput: FormEventHandler<HTMLInputElement>;
|
||||||
|
className?: string;
|
||||||
|
showStrength?: boolean;
|
||||||
|
label?: string | null;
|
||||||
|
showForgotLink?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PasswordInputState {
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordStrengthOptions: Options<string> = [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
value: "very_weak",
|
||||||
|
minDiversity: 0,
|
||||||
|
minLength: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
value: "weak",
|
||||||
|
minDiversity: 2,
|
||||||
|
minLength: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
value: "medium",
|
||||||
|
minDiversity: 3,
|
||||||
|
minLength: 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
value: "strong",
|
||||||
|
minDiversity: 4,
|
||||||
|
minLength: 14,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleToggleShow(i: PasswordInput) {
|
||||||
|
i.setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
show: !prev.show,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasswordInput extends Component<PasswordInputProps, PasswordInputState> {
|
||||||
|
state: PasswordInputState = {
|
||||||
|
show: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: PasswordInputProps, context: any) {
|
||||||
|
super(props, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
props: {
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
onInput,
|
||||||
|
className,
|
||||||
|
showStrength,
|
||||||
|
label,
|
||||||
|
showForgotLink,
|
||||||
|
},
|
||||||
|
state: { show },
|
||||||
|
} = this;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classNames("row", className)}>
|
||||||
|
{label && (
|
||||||
|
<label className="col-sm-2 col-form-label" htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<div className={`col-sm-${label ? 10 : 12}`}>
|
||||||
|
<div className="input-group">
|
||||||
|
<input
|
||||||
|
type={show ? "text" : "password"}
|
||||||
|
className="form-control"
|
||||||
|
aria-describedby={id}
|
||||||
|
autoComplete="on"
|
||||||
|
onInput={onInput}
|
||||||
|
value={value}
|
||||||
|
required
|
||||||
|
maxLength={60}
|
||||||
|
minLength={10}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-dark"
|
||||||
|
type="button"
|
||||||
|
id={id}
|
||||||
|
onClick={linkEvent(this, handleToggleShow)}
|
||||||
|
aria-label={I18NextService.i18n.t(
|
||||||
|
`${show ? "show" : "hide"}_password`
|
||||||
|
)}
|
||||||
|
data-tippy-content={I18NextService.i18n.t(
|
||||||
|
`${show ? "show" : "hide"}_password`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon icon={`eye${show ? "-slash" : ""}`} inline />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{showStrength && value && (
|
||||||
|
<div className={this.passwordColorClass}>
|
||||||
|
{I18NextService.i18n.t(
|
||||||
|
this.passwordStrength as NoOptionI18nKeys
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showForgotLink && (
|
||||||
|
<NavLink
|
||||||
|
className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold pointer-events not-allowed"
|
||||||
|
to="/login_reset"
|
||||||
|
>
|
||||||
|
{I18NextService.i18n.t("forgot_password")}
|
||||||
|
</NavLink>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get passwordStrength(): string | undefined {
|
||||||
|
const password = this.props.value;
|
||||||
|
return password
|
||||||
|
? passwordStrength(password, passwordStrengthOptions).value
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
get passwordColorClass(): string {
|
||||||
|
const strength = this.passwordStrength;
|
||||||
|
|
||||||
|
if (strength && ["weak", "medium"].includes(strength)) {
|
||||||
|
return "text-warning";
|
||||||
|
} else if (strength == "strong") {
|
||||||
|
return "text-success";
|
||||||
|
} else {
|
||||||
|
return "text-danger";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PasswordInput;
|
|
@ -20,6 +20,7 @@ import {
|
||||||
ListCommunities,
|
ListCommunities,
|
||||||
ListCommunitiesResponse,
|
ListCommunitiesResponse,
|
||||||
ListingType,
|
ListingType,
|
||||||
|
SortType,
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import { InitialFetchRequest } from "../../interfaces";
|
import { InitialFetchRequest } from "../../interfaces";
|
||||||
import { FirstLoadService, I18NextService } from "../../services";
|
import { FirstLoadService, I18NextService } from "../../services";
|
||||||
|
@ -28,6 +29,7 @@ import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
import { ListingTypeSelect } from "../common/listing-type-select";
|
import { ListingTypeSelect } from "../common/listing-type-select";
|
||||||
import { Paginator } from "../common/paginator";
|
import { Paginator } from "../common/paginator";
|
||||||
|
import { SortSelect } from "../common/sort-select";
|
||||||
import { CommunityLink } from "./community-link";
|
import { CommunityLink } from "./community-link";
|
||||||
|
|
||||||
const communityLimit = 50;
|
const communityLimit = 50;
|
||||||
|
@ -45,6 +47,7 @@ interface CommunitiesState {
|
||||||
|
|
||||||
interface CommunitiesProps {
|
interface CommunitiesProps {
|
||||||
listingType: ListingType;
|
listingType: ListingType;
|
||||||
|
sort: SortType;
|
||||||
page: number;
|
page: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +55,10 @@ function getListingTypeFromQuery(listingType?: string): ListingType {
|
||||||
return listingType ? (listingType as ListingType) : "Local";
|
return listingType ? (listingType as ListingType) : "Local";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSortTypeFromQuery(type?: string): SortType {
|
||||||
|
return type ? (type as SortType) : "TopMonth";
|
||||||
|
}
|
||||||
|
|
||||||
export class Communities extends Component<any, CommunitiesState> {
|
export class Communities extends Component<any, CommunitiesState> {
|
||||||
private isoData = setIsoData<CommunitiesData>(this.context);
|
private isoData = setIsoData<CommunitiesData>(this.context);
|
||||||
state: CommunitiesState = {
|
state: CommunitiesState = {
|
||||||
|
@ -64,6 +71,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.handlePageChange = this.handlePageChange.bind(this);
|
this.handlePageChange = this.handlePageChange.bind(this);
|
||||||
|
this.handleSortChange = this.handleSortChange.bind(this);
|
||||||
this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
|
this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
|
||||||
|
|
||||||
// Only fetch the data if coming from another route
|
// Only fetch the data if coming from another route
|
||||||
|
@ -99,13 +107,13 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
</h5>
|
</h5>
|
||||||
);
|
);
|
||||||
case "success": {
|
case "success": {
|
||||||
const { listingType, page } = this.getCommunitiesQueryParams();
|
const { listingType, sort, page } = this.getCommunitiesQueryParams();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="h4 mb-4">
|
<h1 className="h4 mb-4">
|
||||||
{I18NextService.i18n.t("list_of_communities")}
|
{I18NextService.i18n.t("list_of_communities")}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="row g-2 justify-content-between">
|
<div className="row g-3 align-items-center mb-2">
|
||||||
<div className="col-auto">
|
<div className="col-auto">
|
||||||
<ListingTypeSelect
|
<ListingTypeSelect
|
||||||
type_={listingType}
|
type_={listingType}
|
||||||
|
@ -114,6 +122,9 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
onChange={this.handleListingTypeChange}
|
onChange={this.handleListingTypeChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-auto me-auto">
|
||||||
|
<SortSelect sort={sort} onChange={this.handleSortChange} />
|
||||||
|
</div>
|
||||||
<div className="col-auto">{this.searchForm()}</div>
|
<div className="col-auto">{this.searchForm()}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -224,10 +235,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
|
|
||||||
searchForm() {
|
searchForm() {
|
||||||
return (
|
return (
|
||||||
<form
|
<form className="row" onSubmit={linkEvent(this, this.handleSearchSubmit)}>
|
||||||
className="row mb-2"
|
|
||||||
onSubmit={linkEvent(this, this.handleSearchSubmit)}
|
|
||||||
>
|
|
||||||
<div className="col-auto">
|
<div className="col-auto">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -252,12 +260,16 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUrl({ listingType, page }: Partial<CommunitiesProps>) {
|
async updateUrl({ listingType, sort, page }: Partial<CommunitiesProps>) {
|
||||||
const { listingType: urlListingType, page: urlPage } =
|
const {
|
||||||
this.getCommunitiesQueryParams();
|
listingType: urlListingType,
|
||||||
|
sort: urlSort,
|
||||||
|
page: urlPage,
|
||||||
|
} = this.getCommunitiesQueryParams();
|
||||||
|
|
||||||
const queryParams: QueryParams<CommunitiesProps> = {
|
const queryParams: QueryParams<CommunitiesProps> = {
|
||||||
listingType: listingType ?? urlListingType,
|
listingType: listingType ?? urlListingType,
|
||||||
|
sort: sort ?? urlSort,
|
||||||
page: (page ?? urlPage)?.toString(),
|
page: (page ?? urlPage)?.toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -270,6 +282,10 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
this.updateUrl({ page });
|
this.updateUrl({ page });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSortChange(val: SortType) {
|
||||||
|
this.updateUrl({ sort: val, page: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
handleListingTypeChange(val: ListingType) {
|
handleListingTypeChange(val: ListingType) {
|
||||||
this.updateUrl({
|
this.updateUrl({
|
||||||
listingType: val,
|
listingType: val,
|
||||||
|
@ -290,7 +306,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fetchInitialData({
|
static async fetchInitialData({
|
||||||
query: { listingType, page },
|
query: { listingType, sort, page },
|
||||||
client,
|
client,
|
||||||
auth,
|
auth,
|
||||||
}: InitialFetchRequest<
|
}: InitialFetchRequest<
|
||||||
|
@ -298,7 +314,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
>): Promise<CommunitiesData> {
|
>): Promise<CommunitiesData> {
|
||||||
const listCommunitiesForm: ListCommunities = {
|
const listCommunitiesForm: ListCommunities = {
|
||||||
type_: getListingTypeFromQuery(listingType),
|
type_: getListingTypeFromQuery(listingType),
|
||||||
sort: "TopMonth",
|
sort: getSortTypeFromQuery(sort),
|
||||||
limit: communityLimit,
|
limit: communityLimit,
|
||||||
page: getPageFromString(page),
|
page: getPageFromString(page),
|
||||||
auth: auth,
|
auth: auth,
|
||||||
|
@ -314,6 +330,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
getCommunitiesQueryParams() {
|
getCommunitiesQueryParams() {
|
||||||
return getQueryParams<CommunitiesProps>({
|
return getQueryParams<CommunitiesProps>({
|
||||||
listingType: getListingTypeFromQuery,
|
listingType: getListingTypeFromQuery,
|
||||||
|
sort: getSortTypeFromQuery,
|
||||||
page: getPageFromString,
|
page: getPageFromString,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -334,12 +351,12 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
async refetch() {
|
async refetch() {
|
||||||
this.setState({ listCommunitiesResponse: { state: "loading" } });
|
this.setState({ listCommunitiesResponse: { state: "loading" } });
|
||||||
|
|
||||||
const { listingType, page } = this.getCommunitiesQueryParams();
|
const { listingType, sort, page } = this.getCommunitiesQueryParams();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
listCommunitiesResponse: await HttpService.client.listCommunities({
|
listCommunitiesResponse: await HttpService.client.listCommunities({
|
||||||
type_: listingType,
|
type_: listingType,
|
||||||
sort: "TopMonth",
|
sort: sort,
|
||||||
limit: communityLimit,
|
limit: communityLimit,
|
||||||
page,
|
page,
|
||||||
auth: myAuth(),
|
auth: myAuth(),
|
||||||
|
|
|
@ -312,6 +312,7 @@ export class Community extends Component<
|
||||||
<HtmlTags
|
<HtmlTags
|
||||||
title={this.documentTitle}
|
title={this.documentTitle}
|
||||||
path={this.context.router.route.match.url}
|
path={this.context.router.route.match.url}
|
||||||
|
canonicalPath={res.community_view.community.actor_id}
|
||||||
description={res.community_view.community.description}
|
description={res.community_view.community.description}
|
||||||
image={res.community_view.community.icon}
|
image={res.community_view.community.icon}
|
||||||
/>
|
/>
|
||||||
|
@ -447,7 +448,7 @@ export class Community extends Component<
|
||||||
nodes={commentsToFlatNodes(this.state.commentsRes.data.comments)}
|
nodes={commentsToFlatNodes(this.state.commentsRes.data.comments)}
|
||||||
viewType={CommentViewType.Flat}
|
viewType={CommentViewType.Flat}
|
||||||
finished={this.state.finished}
|
finished={this.state.finished}
|
||||||
noIndent
|
isTopLevel
|
||||||
showContext
|
showContext
|
||||||
enableDownvotes={enableDownvotes(site_res)}
|
enableDownvotes={enableDownvotes(site_res)}
|
||||||
moderators={communityRes.moderators}
|
moderators={communityRes.moderators}
|
||||||
|
|
|
@ -636,7 +636,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
i.setState({ leaveModTeamLoading: true });
|
i.setState({ leaveModTeamLoading: true });
|
||||||
i.props.onLeaveModTeam({
|
i.props.onLeaveModTeam({
|
||||||
community_id: i.props.community_view.community.id,
|
community_id: i.props.community_view.community.id,
|
||||||
person_id: 92,
|
person_id: myId,
|
||||||
added: false,
|
added: false,
|
||||||
auth: myAuthRequired(),
|
auth: myAuthRequired(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -718,7 +718,7 @@ export class Home extends Component<any, HomeState> {
|
||||||
nodes={commentsToFlatNodes(comments)}
|
nodes={commentsToFlatNodes(comments)}
|
||||||
viewType={CommentViewType.Flat}
|
viewType={CommentViewType.Flat}
|
||||||
finished={this.state.finished}
|
finished={this.state.finished}
|
||||||
noIndent
|
isTopLevel
|
||||||
showCommunity
|
showCommunity
|
||||||
showContext
|
showContext
|
||||||
enableDownvotes={enableDownvotes(siteRes)}
|
enableDownvotes={enableDownvotes(siteRes)}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { myAuth, setIsoData } from "@utils/app";
|
||||||
import { isBrowser } from "@utils/browser";
|
import { isBrowser } from "@utils/browser";
|
||||||
import { Location } from "history";
|
import { Location } from "history";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import { NavLink } from "inferno-router";
|
|
||||||
import { RouteComponentProps } from "inferno-router/dist/Route";
|
import { RouteComponentProps } from "inferno-router/dist/Route";
|
||||||
import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
|
import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
|
||||||
import { I18NextService, UserService } from "../../services";
|
import { I18NextService, UserService } from "../../services";
|
||||||
|
@ -10,6 +9,7 @@ import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
import { toast } from "../../toast";
|
import { toast } from "../../toast";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
|
import PasswordInput from "../common/password-input";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
loginRes: RequestState<LoginResponse>;
|
loginRes: RequestState<LoginResponse>;
|
||||||
|
@ -163,28 +163,14 @@ export class Login extends Component<
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3 row">
|
<div className="mb-3">
|
||||||
<label className="col-sm-2 col-form-label" htmlFor="login-password">
|
<PasswordInput
|
||||||
{I18NextService.i18n.t("password")}
|
|
||||||
</label>
|
|
||||||
<div className="col-sm-10">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="login-password"
|
id="login-password"
|
||||||
value={this.state.form.password}
|
value={this.state.form.password}
|
||||||
onInput={linkEvent(this, handleLoginPasswordChange)}
|
onInput={linkEvent(this, this.handleLoginPasswordChange)}
|
||||||
className="form-control"
|
label={I18NextService.i18n.t("password")}
|
||||||
autoComplete="current-password"
|
showForgotLink
|
||||||
required
|
|
||||||
maxLength={60}
|
|
||||||
/>
|
/>
|
||||||
<NavLink
|
|
||||||
className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold pointer-events not-allowed"
|
|
||||||
to="/login_reset"
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("forgot_password")}
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{this.state.showTotp && (
|
{this.state.showTotp && (
|
||||||
<div className="mb-3 row">
|
<div className="mb-3 row">
|
||||||
|
@ -223,4 +209,67 @@ export class Login extends Component<
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleLoginSubmit(i: Login, event: any) {
|
||||||
|
event.preventDefault();
|
||||||
|
const { password, totp_2fa_token, username_or_email } = i.state.form;
|
||||||
|
|
||||||
|
if (username_or_email && password) {
|
||||||
|
i.setState({ loginRes: { state: "loading" } });
|
||||||
|
|
||||||
|
const loginRes = await HttpService.client.login({
|
||||||
|
username_or_email,
|
||||||
|
password,
|
||||||
|
totp_2fa_token,
|
||||||
|
});
|
||||||
|
switch (loginRes.state) {
|
||||||
|
case "failed": {
|
||||||
|
if (loginRes.msg === "missing_totp_token") {
|
||||||
|
i.setState({ showTotp: true });
|
||||||
|
toast(I18NextService.i18n.t("enter_two_factor_code"), "info");
|
||||||
|
}
|
||||||
|
if (loginRes.msg === "incorrect_login") {
|
||||||
|
toast(I18NextService.i18n.t("incorrect_login"), "danger");
|
||||||
|
}
|
||||||
|
|
||||||
|
i.setState({ loginRes: { state: "failed", msg: loginRes.msg } });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "success": {
|
||||||
|
UserService.Instance.login({
|
||||||
|
res: loginRes.data,
|
||||||
|
});
|
||||||
|
const site = await HttpService.client.getSite({
|
||||||
|
auth: myAuth(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (site.state === "success") {
|
||||||
|
UserService.Instance.myUserInfo = site.data.my_user;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.props.history.action === "PUSH"
|
||||||
|
? i.props.history.back()
|
||||||
|
: i.props.history.replace("/");
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoginUsernameChange(i: Login, event: any) {
|
||||||
|
i.state.form.username_or_email = event.target.value.trim();
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoginTotpChange(i: Login, event: any) {
|
||||||
|
i.state.form.totp_2fa_token = event.target.value;
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoginPasswordChange(i: Login, event: any) {
|
||||||
|
i.state.form.password = event.target.value;
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import { I18NextService, UserService } from "../../services";
|
import { I18NextService, UserService } from "../../services";
|
||||||
import { HttpService, RequestState } from "../../services/HttpService";
|
import { HttpService, RequestState } from "../../services/HttpService";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
|
import PasswordInput from "../common/password-input";
|
||||||
import { SiteForm } from "./site-form";
|
import { SiteForm } from "./site-form";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -121,42 +122,22 @@ export class Setup extends Component<any, State> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3 row">
|
<div className="mb-3">
|
||||||
<label className="col-sm-2 col-form-label" htmlFor="password">
|
<PasswordInput
|
||||||
{I18NextService.i18n.t("password")}
|
|
||||||
</label>
|
|
||||||
<div className="col-sm-10">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
id="password"
|
||||||
value={this.state.form.password}
|
value={this.state.form.password}
|
||||||
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
|
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
|
||||||
className="form-control"
|
label={I18NextService.i18n.t("password")}
|
||||||
required
|
|
||||||
autoComplete="new-password"
|
|
||||||
minLength={10}
|
|
||||||
maxLength={60}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mb-3">
|
||||||
<div className="mb-3 row">
|
<PasswordInput
|
||||||
<label className="col-sm-2 col-form-label" htmlFor="verify-password">
|
|
||||||
{I18NextService.i18n.t("verify_password")}
|
|
||||||
</label>
|
|
||||||
<div className="col-sm-10">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="verify-password"
|
id="verify-password"
|
||||||
value={this.state.form.password_verify}
|
value={this.state.form.password_verify}
|
||||||
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
|
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
|
||||||
className="form-control"
|
label={I18NextService.i18n.t("verify_password")}
|
||||||
required
|
|
||||||
autoComplete="new-password"
|
|
||||||
minLength={10}
|
|
||||||
maxLength={60}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="mb-3 row">
|
<div className="mb-3 row">
|
||||||
<div className="col-sm-10">
|
<div className="col-sm-10">
|
||||||
<button type="submit" className="btn btn-secondary">
|
<button type="submit" className="btn btn-secondary">
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { myAuth, setIsoData } from "@utils/app";
|
import { myAuth, setIsoData } from "@utils/app";
|
||||||
import { isBrowser } from "@utils/browser";
|
import { isBrowser } from "@utils/browser";
|
||||||
import { validEmail } from "@utils/helpers";
|
import { validEmail } from "@utils/helpers";
|
||||||
import { Options, passwordStrength } from "check-password-strength";
|
|
||||||
import { NoOptionI18nKeys } from "i18next";
|
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import { T } from "inferno-i18next-dess";
|
import { T } from "inferno-i18next-dess";
|
||||||
import {
|
import {
|
||||||
|
@ -20,33 +18,7 @@ import { toast } from "../../toast";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Icon, Spinner } from "../common/icon";
|
import { Icon, Spinner } from "../common/icon";
|
||||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||||
|
import PasswordInput from "../common/password-input";
|
||||||
const passwordStrengthOptions: Options<string> = [
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
value: "very_weak",
|
|
||||||
minDiversity: 0,
|
|
||||||
minLength: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
value: "weak",
|
|
||||||
minDiversity: 2,
|
|
||||||
minLength: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
value: "medium",
|
|
||||||
minDiversity: 3,
|
|
||||||
minLength: 12,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
value: "strong",
|
|
||||||
minDiversity: 4,
|
|
||||||
minLength: 14,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
registerRes: RequestState<LoginResponse>;
|
registerRes: RequestState<LoginResponse>;
|
||||||
|
@ -210,57 +182,26 @@ export class Signup extends Component<any, State> {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-3 row">
|
<div className="mb-3">
|
||||||
<label
|
<PasswordInput
|
||||||
className="col-sm-2 col-form-label"
|
|
||||||
htmlFor="register-password"
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("password")}
|
|
||||||
</label>
|
|
||||||
<div className="col-sm-10">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="register-password"
|
id="register-password"
|
||||||
value={this.state.form.password}
|
value={this.state.form.password}
|
||||||
autoComplete="new-password"
|
|
||||||
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
|
onInput={linkEvent(this, this.handleRegisterPasswordChange)}
|
||||||
minLength={10}
|
showStrength
|
||||||
maxLength={60}
|
label={I18NextService.i18n.t("password")}
|
||||||
className="form-control"
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
{this.state.form.password && (
|
|
||||||
<div className={this.passwordColorClass}>
|
|
||||||
{I18NextService.i18n.t(
|
|
||||||
this.passwordStrength as NoOptionI18nKeys
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-3 row">
|
<div className="mb-3">
|
||||||
<label
|
<PasswordInput
|
||||||
className="col-sm-2 col-form-label"
|
|
||||||
htmlFor="register-verify-password"
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("verify_password")}
|
|
||||||
</label>
|
|
||||||
<div className="col-sm-10">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="register-verify-password"
|
id="register-verify-password"
|
||||||
value={this.state.form.password_verify}
|
value={this.state.form.password_verify}
|
||||||
autoComplete="new-password"
|
|
||||||
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
|
onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
|
||||||
maxLength={60}
|
label={I18NextService.i18n.t("verify_password")}
|
||||||
className="form-control"
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{siteView.local_site.registration_mode == "RequireApplication" && (
|
{siteView.local_site.registration_mode === "RequireApplication" && (
|
||||||
<>
|
<>
|
||||||
<div className="mb-3 row">
|
<div className="mb-3 row">
|
||||||
<div className="offset-sm-2 col-sm-10">
|
<div className="offset-sm-2 col-sm-10">
|
||||||
|
@ -411,25 +352,6 @@ export class Signup extends Component<any, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get passwordStrength(): string | undefined {
|
|
||||||
const password = this.state.form.password;
|
|
||||||
return password
|
|
||||||
? passwordStrength(password, passwordStrengthOptions).value
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
get passwordColorClass(): string {
|
|
||||||
const strength = this.passwordStrength;
|
|
||||||
|
|
||||||
if (strength && ["weak", "medium"].includes(strength)) {
|
|
||||||
return "text-warning";
|
|
||||||
} else if (strength == "strong") {
|
|
||||||
return "text-success";
|
|
||||||
} else {
|
|
||||||
return "text-danger";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleRegisterSubmit(i: Signup, event: any) {
|
async handleRegisterSubmit(i: Signup, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -411,6 +411,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
<option value="browser">
|
<option value="browser">
|
||||||
{I18NextService.i18n.t("browser_default")}
|
{I18NextService.i18n.t("browser_default")}
|
||||||
</option>
|
</option>
|
||||||
|
<option value="browser-compact">
|
||||||
|
{I18NextService.i18n.t("browser_default_compact")}
|
||||||
|
</option>
|
||||||
{this.props.themeList?.map(theme => (
|
{this.props.themeList?.map(theme => (
|
||||||
<option key={theme} value={theme}>
|
<option key={theme} value={theme}>
|
||||||
{theme}
|
{theme}
|
||||||
|
|
|
@ -312,6 +312,7 @@ function renderModlogType({ type_, view }: ModlogType) {
|
||||||
const {
|
const {
|
||||||
mod_feature_post: { featured, is_featured_community },
|
mod_feature_post: { featured, is_featured_community },
|
||||||
post: { id, name },
|
post: { id, name },
|
||||||
|
community,
|
||||||
} = view as ModFeaturePostView;
|
} = view as ModFeaturePostView;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -320,7 +321,12 @@ function renderModlogType({ type_, view }: ModlogType) {
|
||||||
<span>
|
<span>
|
||||||
Post <Link to={`/post/${id}`}>{name}</Link>
|
Post <Link to={`/post/${id}`}>{name}</Link>
|
||||||
</span>
|
</span>
|
||||||
<span>{is_featured_community ? " In Community" : " In Local"}</span>
|
<span>
|
||||||
|
{is_featured_community
|
||||||
|
? " in community "
|
||||||
|
: " in Local, from community "}
|
||||||
|
</span>
|
||||||
|
<CommunityLink community={community} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -532,7 +538,7 @@ function renderModlogType({ type_, view }: ModlogType) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span>Purged a Post from from </span>
|
<span>Purged a Post from </span>
|
||||||
<CommunityLink community={community} />
|
<CommunityLink community={community} />
|
||||||
{reason && (
|
{reason && (
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { HttpService, I18NextService, UserService } from "../../services";
|
||||||
import { RequestState } from "../../services/HttpService";
|
import { RequestState } from "../../services/HttpService";
|
||||||
import { HtmlTags } from "../common/html-tags";
|
import { HtmlTags } from "../common/html-tags";
|
||||||
import { Spinner } from "../common/icon";
|
import { Spinner } from "../common/icon";
|
||||||
|
import PasswordInput from "../common/password-input";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
passwordChangeRes: RequestState<LoginResponse>;
|
passwordChangeRes: RequestState<LoginResponse>;
|
||||||
|
@ -60,38 +61,23 @@ export class PasswordChange extends Component<any, State> {
|
||||||
passwordChangeForm() {
|
passwordChangeForm() {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={linkEvent(this, this.handlePasswordChangeSubmit)}>
|
<form onSubmit={linkEvent(this, this.handlePasswordChangeSubmit)}>
|
||||||
<div className="mb-3 row">
|
<div className="mb-3">
|
||||||
<label className="col-sm-2 col-form-label" htmlFor="new-password">
|
<PasswordInput
|
||||||
{I18NextService.i18n.t("new_password")}
|
|
||||||
</label>
|
|
||||||
<div className="col-sm-10">
|
|
||||||
<input
|
|
||||||
id="new-password"
|
id="new-password"
|
||||||
type="password"
|
|
||||||
value={this.state.form.password}
|
value={this.state.form.password}
|
||||||
onInput={linkEvent(this, this.handlePasswordChange)}
|
onInput={linkEvent(this, this.handlePasswordChange)}
|
||||||
className="form-control"
|
showStrength
|
||||||
required
|
label={I18NextService.i18n.t("new_password")}
|
||||||
maxLength={60}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mb-3">
|
||||||
<div className="mb-3 row">
|
<PasswordInput
|
||||||
<label className="col-sm-2 col-form-label" htmlFor="verify-password">
|
id="password"
|
||||||
{I18NextService.i18n.t("verify_password")}
|
|
||||||
</label>
|
|
||||||
<div className="col-sm-10">
|
|
||||||
<input
|
|
||||||
id="verify-password"
|
|
||||||
type="password"
|
|
||||||
value={this.state.form.password_verify}
|
value={this.state.form.password_verify}
|
||||||
onInput={linkEvent(this, this.handleVerifyPasswordChange)}
|
onInput={linkEvent(this, this.handleVerifyPasswordChange)}
|
||||||
className="form-control"
|
label={I18NextService.i18n.t("verify_password")}
|
||||||
required
|
|
||||||
maxLength={60}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="mb-3 row">
|
<div className="mb-3 row">
|
||||||
<div className="col-sm-10">
|
<div className="col-sm-10">
|
||||||
<button type="submit" className="btn btn-secondary">
|
<button type="submit" className="btn btn-secondary">
|
||||||
|
|
|
@ -252,7 +252,7 @@ export class PersonDetails extends Component<PersonDetailsProps, any> {
|
||||||
viewType={CommentViewType.Flat}
|
viewType={CommentViewType.Flat}
|
||||||
admins={this.props.admins}
|
admins={this.props.admins}
|
||||||
finished={this.props.finished}
|
finished={this.props.finished}
|
||||||
noIndent
|
isTopLevel
|
||||||
showCommunity
|
showCommunity
|
||||||
showContext
|
showContext
|
||||||
enableDownvotes={this.props.enableDownvotes}
|
enableDownvotes={this.props.enableDownvotes}
|
||||||
|
|
|
@ -324,6 +324,7 @@ export class Profile extends Component<
|
||||||
<HtmlTags
|
<HtmlTags
|
||||||
title={this.documentTitle}
|
title={this.documentTitle}
|
||||||
path={this.context.router.route.match.url}
|
path={this.context.router.route.match.url}
|
||||||
|
canonicalPath={personRes.person_view.person.actor_id}
|
||||||
description={personRes.person_view.person.bio}
|
description={personRes.person_view.person.bio}
|
||||||
image={personRes.person_view.person.avatar}
|
image={personRes.person_view.person.avatar}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -40,6 +40,7 @@ import { ImageUploadForm } from "../common/image-upload-form";
|
||||||
import { LanguageSelect } from "../common/language-select";
|
import { LanguageSelect } from "../common/language-select";
|
||||||
import { ListingTypeSelect } from "../common/listing-type-select";
|
import { ListingTypeSelect } from "../common/listing-type-select";
|
||||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||||
|
import PasswordInput from "../common/password-input";
|
||||||
import { SearchableSelect } from "../common/searchable-select";
|
import { SearchableSelect } from "../common/searchable-select";
|
||||||
import { SortSelect } from "../common/sort-select";
|
import { SortSelect } from "../common/sort-select";
|
||||||
import Tabs from "../common/tabs";
|
import Tabs from "../common/tabs";
|
||||||
|
@ -318,60 +319,31 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
<>
|
<>
|
||||||
<h2 className="h5">{I18NextService.i18n.t("change_password")}</h2>
|
<h2 className="h5">{I18NextService.i18n.t("change_password")}</h2>
|
||||||
<form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleChangePasswordSubmit)}>
|
||||||
<div className="mb-3 row">
|
<div className="mb-3">
|
||||||
<label className="col-sm-5 col-form-label" htmlFor="user-password">
|
<PasswordInput
|
||||||
{I18NextService.i18n.t("new_password")}
|
id="new-password"
|
||||||
</label>
|
|
||||||
<div className="col-sm-7">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="user-password"
|
|
||||||
className="form-control"
|
|
||||||
value={this.state.changePasswordForm.new_password}
|
value={this.state.changePasswordForm.new_password}
|
||||||
autoComplete="new-password"
|
|
||||||
maxLength={60}
|
|
||||||
onInput={linkEvent(this, this.handleNewPasswordChange)}
|
onInput={linkEvent(this, this.handleNewPasswordChange)}
|
||||||
|
showStrength
|
||||||
|
label={I18NextService.i18n.t("new_password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mb-3">
|
||||||
<div className="mb-3 row">
|
<PasswordInput
|
||||||
<label
|
id="verify-new-password"
|
||||||
className="col-sm-5 col-form-label"
|
|
||||||
htmlFor="user-verify-password"
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("verify_password")}
|
|
||||||
</label>
|
|
||||||
<div className="col-sm-7">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="user-verify-password"
|
|
||||||
className="form-control"
|
|
||||||
value={this.state.changePasswordForm.new_password_verify}
|
value={this.state.changePasswordForm.new_password_verify}
|
||||||
autoComplete="new-password"
|
|
||||||
maxLength={60}
|
|
||||||
onInput={linkEvent(this, this.handleNewPasswordVerifyChange)}
|
onInput={linkEvent(this, this.handleNewPasswordVerifyChange)}
|
||||||
|
label={I18NextService.i18n.t("verify_password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mb-3">
|
||||||
<div className="mb-3 row">
|
<PasswordInput
|
||||||
<label
|
|
||||||
className="col-sm-5 col-form-label"
|
|
||||||
htmlFor="user-old-password"
|
|
||||||
>
|
|
||||||
{I18NextService.i18n.t("old_password")}
|
|
||||||
</label>
|
|
||||||
<div className="col-sm-7">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="user-old-password"
|
id="user-old-password"
|
||||||
className="form-control"
|
|
||||||
value={this.state.changePasswordForm.old_password}
|
value={this.state.changePasswordForm.old_password}
|
||||||
autoComplete="new-password"
|
|
||||||
maxLength={60}
|
|
||||||
onInput={linkEvent(this, this.handleOldPasswordChange)}
|
onInput={linkEvent(this, this.handleOldPasswordChange)}
|
||||||
|
label={I18NextService.i18n.t("old_password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="input-group mb-3">
|
<div className="input-group mb-3">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -635,6 +607,9 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
<option value="browser">
|
<option value="browser">
|
||||||
{I18NextService.i18n.t("browser_default")}
|
{I18NextService.i18n.t("browser_default")}
|
||||||
</option>
|
</option>
|
||||||
|
<option value="browser-compact">
|
||||||
|
{I18NextService.i18n.t("browser_default_compact")}
|
||||||
|
</option>
|
||||||
{this.state.themeList.map(theme => (
|
{this.state.themeList.map(theme => (
|
||||||
<option key={theme} value={theme}>
|
<option key={theme} value={theme}>
|
||||||
{theme}
|
{theme}
|
||||||
|
@ -813,8 +788,12 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div className="input-group mb-3">
|
<form
|
||||||
|
className="mb-3"
|
||||||
|
onSubmit={linkEvent(this, this.handleDeleteAccount)}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="btn d-block btn-danger"
|
className="btn d-block btn-danger"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(
|
||||||
this,
|
this,
|
||||||
|
@ -825,24 +804,26 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
</button>
|
</button>
|
||||||
{this.state.deleteAccountShowConfirm && (
|
{this.state.deleteAccountShowConfirm && (
|
||||||
<>
|
<>
|
||||||
<div className="my-2 alert alert-danger" role="alert">
|
<label
|
||||||
|
className="my-2 alert alert-danger d-block"
|
||||||
|
role="alert"
|
||||||
|
htmlFor="password-delete-account"
|
||||||
|
>
|
||||||
{I18NextService.i18n.t("delete_account_confirm")}
|
{I18NextService.i18n.t("delete_account_confirm")}
|
||||||
</div>
|
</label>
|
||||||
<input
|
<PasswordInput
|
||||||
type="password"
|
id="password-delete-account"
|
||||||
value={this.state.deleteAccountForm.password}
|
value={this.state.deleteAccountForm.password}
|
||||||
autoComplete="new-password"
|
|
||||||
maxLength={60}
|
|
||||||
onInput={linkEvent(
|
onInput={linkEvent(
|
||||||
this,
|
this,
|
||||||
this.handleDeleteAccountPasswordChange
|
this.handleDeleteAccountPasswordChange
|
||||||
)}
|
)}
|
||||||
className="form-control my-2"
|
className="my-2"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
type="submit"
|
||||||
className="btn btn-danger me-4"
|
className="btn btn-danger me-4"
|
||||||
disabled={!this.state.deleteAccountForm.password}
|
disabled={!this.state.deleteAccountForm.password}
|
||||||
onClick={linkEvent(this, this.handleDeleteAccount)}
|
|
||||||
>
|
>
|
||||||
{this.state.deleteAccountRes.state === "loading" ? (
|
{this.state.deleteAccountRes.state === "loading" ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -852,6 +833,7 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
|
type="button"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(
|
||||||
this,
|
this,
|
||||||
this.handleDeleteAccountShowConfirmToggle
|
this.handleDeleteAccountShowConfirmToggle
|
||||||
|
@ -861,7 +843,7 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</form>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1222,7 +1204,8 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
i.setState(s => ((s.deleteAccountForm.password = event.target.value), s));
|
i.setState(s => ((s.deleteAccountForm.password = event.target.value), s));
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDeleteAccount(i: Settings) {
|
async handleDeleteAccount(i: Settings, event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
const password = i.state.deleteAccountForm.password;
|
const password = i.state.deleteAccountForm.password;
|
||||||
if (password) {
|
if (password) {
|
||||||
i.setState({ deleteAccountRes: { state: "loading" } });
|
i.setState({ deleteAccountRes: { state: "loading" } });
|
||||||
|
|
|
@ -310,16 +310,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
const url = post.url;
|
const url = post.url;
|
||||||
const thumbnail = post.thumbnail_url;
|
const thumbnail = post.thumbnail_url;
|
||||||
|
|
||||||
if (url && isImage(url)) {
|
if (thumbnail) {
|
||||||
if (url.includes("pictrs")) {
|
|
||||||
return url;
|
|
||||||
} else if (thumbnail) {
|
|
||||||
return thumbnail;
|
return thumbnail;
|
||||||
} else {
|
} else if (url && isImage(url)) {
|
||||||
return url;
|
return url;
|
||||||
}
|
|
||||||
} else if (thumbnail) {
|
|
||||||
return thumbnail;
|
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -353,6 +353,7 @@ export class Post extends Component<any, PostState> {
|
||||||
<HtmlTags
|
<HtmlTags
|
||||||
title={this.documentTitle}
|
title={this.documentTitle}
|
||||||
path={this.context.router.route.match.url}
|
path={this.context.router.route.match.url}
|
||||||
|
canonicalPath={res.post_view.post.ap_id}
|
||||||
image={this.imageTag}
|
image={this.imageTag}
|
||||||
description={res.post_view.post.body}
|
description={res.post_view.post.body}
|
||||||
/>
|
/>
|
||||||
|
@ -541,7 +542,7 @@ export class Post extends Component<any, PostState> {
|
||||||
nodes={commentsToFlatNodes(commentsRes.data.comments)}
|
nodes={commentsToFlatNodes(commentsRes.data.comments)}
|
||||||
viewType={this.state.commentViewType}
|
viewType={this.state.commentViewType}
|
||||||
maxCommentsShown={this.state.maxCommentsShown}
|
maxCommentsShown={this.state.maxCommentsShown}
|
||||||
noIndent
|
isTopLevel
|
||||||
locked={postRes.data.post_view.post.locked}
|
locked={postRes.data.post_view.post.locked}
|
||||||
moderators={postRes.data.moderators}
|
moderators={postRes.data.moderators}
|
||||||
admins={this.state.siteRes.admins}
|
admins={this.state.siteRes.admins}
|
||||||
|
|
|
@ -466,6 +466,10 @@ export class Search extends Component<any, SearchState> {
|
||||||
<HtmlTags
|
<HtmlTags
|
||||||
title={this.documentTitle}
|
title={this.documentTitle}
|
||||||
path={this.context.router.route.match.url}
|
path={this.context.router.route.match.url}
|
||||||
|
canonicalPath={
|
||||||
|
this.context.router.route.match.url +
|
||||||
|
this.context.router.route.location.search
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<h1 className="h4 mb-4">{I18NextService.i18n.t("search")}</h1>
|
<h1 className="h4 mb-4">{I18NextService.i18n.t("search")}</h1>
|
||||||
{this.selects}
|
{this.selects}
|
||||||
|
@ -717,7 +721,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
viewType={CommentViewType.Flat}
|
viewType={CommentViewType.Flat}
|
||||||
viewOnly
|
viewOnly
|
||||||
locked
|
locked
|
||||||
noIndent
|
isTopLevel
|
||||||
enableDownvotes={enableDownvotes(this.state.siteRes)}
|
enableDownvotes={enableDownvotes(this.state.siteRes)}
|
||||||
allLanguages={this.state.siteRes.all_languages}
|
allLanguages={this.state.siteRes.all_languages}
|
||||||
siteLanguages={this.state.siteRes.discussion_languages}
|
siteLanguages={this.state.siteRes.discussion_languages}
|
||||||
|
@ -778,7 +782,7 @@ export class Search extends Component<any, SearchState> {
|
||||||
viewType={CommentViewType.Flat}
|
viewType={CommentViewType.Flat}
|
||||||
viewOnly
|
viewOnly
|
||||||
locked
|
locked
|
||||||
noIndent
|
isTopLevel
|
||||||
enableDownvotes={enableDownvotes(siteRes)}
|
enableDownvotes={enableDownvotes(siteRes)}
|
||||||
allLanguages={siteRes.all_languages}
|
allLanguages={siteRes.all_languages}
|
||||||
siteLanguages={siteRes.discussion_languages}
|
siteLanguages={siteRes.discussion_languages}
|
||||||
|
|
|
@ -18,14 +18,9 @@ export type IsoDataOptionalSite<T extends RouteData = any> = Partial<
|
||||||
> &
|
> &
|
||||||
Pick<IsoData<T>, Exclude<keyof IsoData<T>, "site_res">>;
|
Pick<IsoData<T>, Exclude<keyof IsoData<T>, "site_res">>;
|
||||||
|
|
||||||
export interface ILemmyConfig {
|
|
||||||
wsHost?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
isoData: IsoData;
|
isoData: IsoData;
|
||||||
lemmyConfig?: ILemmyConfig;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -188,13 +188,16 @@ export function setupMarkdown() {
|
||||||
//Provide custom renderer for our emojis to allow us to add a css class and force size dimensions on them.
|
//Provide custom renderer for our emojis to allow us to add a css class and force size dimensions on them.
|
||||||
const item = tokens[idx] as any;
|
const item = tokens[idx] as any;
|
||||||
const title = item.attrs.length >= 3 ? item.attrs[2][1] : "";
|
const title = item.attrs.length >= 3 ? item.attrs[2][1] : "";
|
||||||
const src: string = item.attrs[0][1];
|
const customEmoji = customEmojisLookup.get(title);
|
||||||
const isCustomEmoji = customEmojisLookup.get(title) != undefined;
|
const isCustomEmoji = customEmoji != undefined;
|
||||||
if (!isCustomEmoji) {
|
if (!isCustomEmoji) {
|
||||||
return defaultRenderer?.(tokens, idx, options, env, self) ?? "";
|
return defaultRenderer?.(tokens, idx, options, env, self) ?? "";
|
||||||
}
|
}
|
||||||
const alt_text = item.content;
|
return `<img class="icon icon-emoji" src="${
|
||||||
return `<img class="icon icon-emoji" src="${src}" title="${title}" alt="${alt_text}"/>`;
|
customEmoji!.custom_emoji.image_url
|
||||||
|
}" title="${customEmoji!.custom_emoji.shortcode}" alt="${
|
||||||
|
customEmoji!.custom_emoji.alt_text
|
||||||
|
}"/>`;
|
||||||
};
|
};
|
||||||
md.renderer.rules.table_open = function () {
|
md.renderer.rules.table_open = function () {
|
||||||
return '<table class="table">';
|
return '<table class="table">';
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const path = require("path");
|
const { resolve } = require("path");
|
||||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
const nodeExternals = require("webpack-node-externals");
|
const nodeExternals = require("webpack-node-externals");
|
||||||
const CopyPlugin = require("copy-webpack-plugin");
|
const CopyPlugin = require("copy-webpack-plugin");
|
||||||
const RunNodeWebpackPlugin = require("run-node-webpack-plugin");
|
|
||||||
const merge = require("lodash.merge");
|
|
||||||
const { ServiceWorkerPlugin } = require("service-worker-webpack");
|
const { ServiceWorkerPlugin } = require("service-worker-webpack");
|
||||||
|
|
||||||
const banner = `
|
const banner = `
|
||||||
|
@ -14,18 +12,18 @@ 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
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function getBase(env, mode) {
|
module.exports = (env, argv) => {
|
||||||
return {
|
const mode = argv.mode;
|
||||||
|
|
||||||
|
const base = {
|
||||||
output: {
|
output: {
|
||||||
filename: "js/server.js",
|
|
||||||
publicPath: "/",
|
|
||||||
hashFunction: "xxhash64",
|
hashFunction: "xxhash64",
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "src/"),
|
"@": resolve(__dirname, "src/"),
|
||||||
"@utils": path.resolve(__dirname, "src/shared/utils/"),
|
"@utils": resolve(__dirname, "src/shared/utils/"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
performance: {
|
performance: {
|
||||||
|
@ -67,42 +65,24 @@ function getBase(env, mode) {
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const createServerConfig = (env, mode) => {
|
const serverConfig = {
|
||||||
const base = getBase(env, mode);
|
...base,
|
||||||
const config = merge({}, base, {
|
|
||||||
mode,
|
|
||||||
entry: "./src/server/index.tsx",
|
entry: "./src/server/index.tsx",
|
||||||
output: {
|
output: {
|
||||||
|
...base.output,
|
||||||
filename: "js/server.js",
|
filename: "js/server.js",
|
||||||
|
publicPath: "/",
|
||||||
},
|
},
|
||||||
target: "node",
|
target: "node",
|
||||||
externals: [nodeExternals(), "inferno-helmet"],
|
externals: [nodeExternals(), "inferno-helmet"],
|
||||||
});
|
};
|
||||||
|
|
||||||
if (mode === "development") {
|
const clientConfig = {
|
||||||
// config.cache = {
|
...base,
|
||||||
// type: "filesystem",
|
|
||||||
// name: "server",
|
|
||||||
// };
|
|
||||||
|
|
||||||
config.plugins.push(
|
|
||||||
new RunNodeWebpackPlugin({
|
|
||||||
runOnlyInWatchMode: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createClientConfig = (env, mode) => {
|
|
||||||
const base = getBase(env, mode);
|
|
||||||
const config = merge({}, base, {
|
|
||||||
mode,
|
|
||||||
entry: "./src/client/index.tsx",
|
entry: "./src/client/index.tsx",
|
||||||
output: {
|
output: {
|
||||||
|
...base.output,
|
||||||
filename: "js/client.js",
|
filename: "js/client.js",
|
||||||
publicPath: `/static/${env.COMMIT_HASH}/`,
|
publicPath: `/static/${env.COMMIT_HASH}/`,
|
||||||
},
|
},
|
||||||
|
@ -158,18 +138,22 @@ const createClientConfig = (env, mode) => {
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
};
|
||||||
|
|
||||||
if (mode === "none") {
|
if (mode === "development") {
|
||||||
const BundleAnalyzerPlugin =
|
// serverConfig.cache = {
|
||||||
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
// type: "filesystem",
|
||||||
config.plugins.push(new BundleAnalyzerPlugin());
|
// name: "server",
|
||||||
|
// };
|
||||||
|
|
||||||
|
const RunNodeWebpackPlugin = require("run-node-webpack-plugin");
|
||||||
|
serverConfig.plugins.push(
|
||||||
|
new RunNodeWebpackPlugin({ runOnlyInWatchMode: true })
|
||||||
|
);
|
||||||
|
} else if (mode === "none") {
|
||||||
|
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
|
||||||
|
serverConfig.plugins.push(new BundleAnalyzerPlugin());
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return [serverConfig, clientConfig];
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = (env, properties) => [
|
|
||||||
createServerConfig(env, properties.mode || "development"),
|
|
||||||
createClientConfig(env, properties.mode || "development"),
|
|
||||||
];
|
|
||||||
|
|
Loading…
Reference in a new issue