mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-23 11:21:26 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
0e052602ad
24 changed files with 453 additions and 400 deletions
|
@ -1,3 +1,3 @@
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
See [here](https://join-lemmy.org/docs/en/contributors/01-overview.html) for contributing Instructions.
|
See [here](https://join-lemmy.org/docs/contributors/01-overview.html) for contributing Instructions.
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 7fc71d0860bbe5c6d620ec27112350ffe5b9229c
|
Subproject commit a241fe1255a6363c7ae1ec5a09520c066745e6ce
|
17
package.json
17
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lemmy-ui",
|
"name": "lemmy-ui",
|
||||||
"version": "0.18.0-rc.3",
|
"version": "0.18.0-rc.4",
|
||||||
"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",
|
||||||
|
@ -22,16 +22,9 @@
|
||||||
"translations:update": "git submodule update --remote --recursive"
|
"translations:update": "git submodule update --remote --recursive"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx,js}": [
|
"*.{ts,tsx,js}": ["prettier --write", "eslint --fix"],
|
||||||
"prettier --write",
|
"*.{css, scss}": ["prettier --write"],
|
||||||
"eslint --fix"
|
"package.json": ["sortpack"]
|
||||||
],
|
|
||||||
"*.{css, scss}": [
|
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
"package.json": [
|
|
||||||
"sortpack"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||||
|
@ -66,7 +59,7 @@
|
||||||
"inferno-server": "^8.1.1",
|
"inferno-server": "^8.1.1",
|
||||||
"isomorphic-cookie": "^1.2.4",
|
"isomorphic-cookie": "^1.2.4",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lemmy-js-client": "0.18.0-rc.1",
|
"lemmy-js-client": "0.18.0-rc.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"markdown-it-container": "^3.0.0",
|
"markdown-it-container": "^3.0.0",
|
||||||
|
|
|
@ -28,7 +28,9 @@ export default async (req: Request, res: Response) => {
|
||||||
const getSiteForm: GetSite = { auth };
|
const getSiteForm: GetSite = { auth };
|
||||||
|
|
||||||
const headers = setForwardedHeaders(req.headers);
|
const headers = setForwardedHeaders(req.headers);
|
||||||
const client = wrapClient(new LemmyHttp(getHttpBaseInternal(), headers));
|
const client = wrapClient(
|
||||||
|
new LemmyHttp(getHttpBaseInternal(), { fetchFunction: fetch, headers })
|
||||||
|
);
|
||||||
|
|
||||||
const { path, url, query } = req;
|
const { path, url, query } = req;
|
||||||
|
|
||||||
|
|
30
src/server/handlers/manifest-handler.ts
Normal file
30
src/server/handlers/manifest-handler.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
import { LemmyHttp } from "lemmy-js-client";
|
||||||
|
import { getHttpBaseInternal } from "../../shared/env";
|
||||||
|
import { wrapClient } from "../../shared/services/HttpService";
|
||||||
|
import generateManifestJson from "../utils/generate-manifest-json";
|
||||||
|
import { setForwardedHeaders } from "../utils/set-forwarded-headers";
|
||||||
|
|
||||||
|
let manifest: Awaited<ReturnType<typeof generateManifestJson>> | undefined =
|
||||||
|
undefined;
|
||||||
|
|
||||||
|
export default async (req: Request, res: Response) => {
|
||||||
|
if (!manifest) {
|
||||||
|
const headers = setForwardedHeaders(req.headers);
|
||||||
|
const client = wrapClient(
|
||||||
|
new LemmyHttp(getHttpBaseInternal(), { fetchFunction: fetch, headers })
|
||||||
|
);
|
||||||
|
const site = await client.getSite({});
|
||||||
|
|
||||||
|
if (site.state === "success") {
|
||||||
|
manifest = await generateManifestJson(site.data);
|
||||||
|
} else {
|
||||||
|
res.sendStatus(500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader("content-type", "application/manifest+json");
|
||||||
|
|
||||||
|
res.send(manifest);
|
||||||
|
};
|
|
@ -15,7 +15,7 @@ export default async (req: Request, res: Response) => {
|
||||||
res.send("Theme must be a css file");
|
res.send("Theme must be a css file");
|
||||||
}
|
}
|
||||||
|
|
||||||
const customTheme = path.resolve(`./${extraThemesFolder}/${theme}`);
|
const customTheme = path.resolve(extraThemesFolder, theme);
|
||||||
|
|
||||||
if (existsSync(customTheme)) {
|
if (existsSync(customTheme)) {
|
||||||
res.sendFile(customTheme);
|
res.sendFile(customTheme);
|
||||||
|
|
|
@ -2,6 +2,7 @@ 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 RobotsHandler from "./handlers/robots-handler";
|
import RobotsHandler from "./handlers/robots-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";
|
||||||
|
@ -24,6 +25,7 @@ if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {
|
||||||
|
|
||||||
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", ManifestHandler);
|
||||||
server.get("/css/themes/:name", ThemeHandler);
|
server.get("/css/themes/:name", ThemeHandler);
|
||||||
server.get("/css/themelist", ThemesListHandler);
|
server.get("/css/themelist", ThemesListHandler);
|
||||||
server.get("/*", CatchAllHandler);
|
server.get("/*", CatchAllHandler);
|
||||||
|
|
|
@ -5,32 +5,35 @@ import sharp from "sharp";
|
||||||
import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces";
|
import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces";
|
||||||
import { favIconPngUrl, favIconUrl } from "../../shared/utils";
|
import { favIconPngUrl, favIconUrl } from "../../shared/utils";
|
||||||
import { fetchIconPng } from "./fetch-icon-png";
|
import { fetchIconPng } from "./fetch-icon-png";
|
||||||
import { generateManifestBase64 } from "./generate-manifest-base64";
|
|
||||||
|
|
||||||
const customHtmlHeader = process.env["LEMMY_UI_CUSTOM_HTML_HEADER"] || "";
|
const customHtmlHeader = process.env["LEMMY_UI_CUSTOM_HTML_HEADER"] || "";
|
||||||
|
|
||||||
|
let appleTouchIcon: string | undefined = undefined;
|
||||||
|
|
||||||
export async function createSsrHtml(
|
export async function createSsrHtml(
|
||||||
root: string,
|
root: string,
|
||||||
isoData: IsoDataOptionalSite
|
isoData: IsoDataOptionalSite
|
||||||
) {
|
) {
|
||||||
const site = isoData.site_res;
|
const site = isoData.site_res;
|
||||||
|
|
||||||
const appleTouchIcon = site?.site_view.site.icon
|
if (!appleTouchIcon) {
|
||||||
? `data:image/png;base64,${sharp(
|
appleTouchIcon = site?.site_view.site.icon
|
||||||
await fetchIconPng(site.site_view.site.icon)
|
? `data:image/png;base64,${sharp(
|
||||||
)
|
await fetchIconPng(site.site_view.site.icon)
|
||||||
.resize(180, 180)
|
)
|
||||||
.extend({
|
.resize(180, 180)
|
||||||
bottom: 20,
|
.extend({
|
||||||
top: 20,
|
bottom: 20,
|
||||||
left: 20,
|
top: 20,
|
||||||
right: 20,
|
left: 20,
|
||||||
background: "#222222",
|
right: 20,
|
||||||
})
|
background: "#222222",
|
||||||
.png()
|
})
|
||||||
.toBuffer()
|
.png()
|
||||||
.then(buf => buf.toString("base64"))}`
|
.toBuffer()
|
||||||
: favIconPngUrl;
|
.then(buf => buf.toString("base64"))}`
|
||||||
|
: favIconPngUrl;
|
||||||
|
}
|
||||||
|
|
||||||
const erudaStr =
|
const erudaStr =
|
||||||
process.env["LEMMY_UI_DEBUG"] === "true"
|
process.env["LEMMY_UI_DEBUG"] === "true"
|
||||||
|
@ -74,15 +77,7 @@ export async function createSsrHtml(
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Web app manifest -->
|
<!-- Web app manifest -->
|
||||||
${
|
<link rel="manifest" href="/manifest" />
|
||||||
site &&
|
|
||||||
`<link
|
|
||||||
rel="manifest"
|
|
||||||
href=${`data:application/manifest+json;base64,${await generateManifestBase64(
|
|
||||||
site
|
|
||||||
)}`}
|
|
||||||
/>`
|
|
||||||
}
|
|
||||||
<link rel="apple-touch-icon" href=${appleTouchIcon} />
|
<link rel="apple-touch-icon" href=${appleTouchIcon} />
|
||||||
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
|
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import sharp from "sharp";
|
||||||
import { getHttpBaseExternal } from "../../shared/env";
|
import { getHttpBaseExternal } from "../../shared/env";
|
||||||
import { fetchIconPng } from "./fetch-icon-png";
|
import { fetchIconPng } from "./fetch-icon-png";
|
||||||
|
|
||||||
const iconSizes = [72, 96, 144, 192, 512];
|
const iconSizes = [72, 96, 128, 144, 152, 192, 384, 512];
|
||||||
|
|
||||||
const defaultLogoPathDirectory = path.join(
|
const defaultLogoPathDirectory = path.join(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
|
@ -14,7 +14,7 @@ const defaultLogoPathDirectory = path.join(
|
||||||
"icons"
|
"icons"
|
||||||
);
|
);
|
||||||
|
|
||||||
export async function generateManifestBase64({
|
export default async function ({
|
||||||
my_user,
|
my_user,
|
||||||
site_view: {
|
site_view: {
|
||||||
site,
|
site,
|
||||||
|
@ -25,7 +25,7 @@ export async function generateManifestBase64({
|
||||||
|
|
||||||
const icon = site.icon ? await fetchIconPng(site.icon) : null;
|
const icon = site.icon ? await fetchIconPng(site.icon) : null;
|
||||||
|
|
||||||
const manifest = {
|
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: url,
|
||||||
|
@ -69,31 +69,24 @@ export async function generateManifestBase64({
|
||||||
short_name: "Communities",
|
short_name: "Communities",
|
||||||
description: "Browse communities",
|
description: "Browse communities",
|
||||||
},
|
},
|
||||||
]
|
{
|
||||||
.concat(
|
name: "Create Post",
|
||||||
my_user
|
url: "/create_post",
|
||||||
? [
|
short_name: "Create Post",
|
||||||
{
|
description: "Create a post.",
|
||||||
name: "Create Post",
|
},
|
||||||
url: "/create_post",
|
].concat(
|
||||||
short_name: "Create Post",
|
my_user?.local_user_view.person.admin || !community_creation_admin_only
|
||||||
description: "Create a post.",
|
? [
|
||||||
},
|
{
|
||||||
]
|
name: "Create Community",
|
||||||
: []
|
url: "/create_community",
|
||||||
)
|
short_name: "Create Community",
|
||||||
.concat(
|
description: "Create a community",
|
||||||
my_user?.local_user_view.person.admin || !community_creation_admin_only
|
},
|
||||||
? [
|
]
|
||||||
{
|
: []
|
||||||
name: "Create Community",
|
),
|
||||||
url: "/create_community",
|
|
||||||
short_name: "Create Community",
|
|
||||||
description: "Create a community",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []
|
|
||||||
),
|
|
||||||
related_applications: [
|
related_applications: [
|
||||||
{
|
{
|
||||||
platform: "f-droid",
|
platform: "f-droid",
|
||||||
|
@ -102,6 +95,4 @@ export async function generateManifestBase64({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return Buffer.from(JSON.stringify(manifest)).toString("base64");
|
|
||||||
}
|
}
|
|
@ -35,7 +35,11 @@ interface NavbarState {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCollapseClick(i: Navbar) {
|
function handleCollapseClick(i: Navbar) {
|
||||||
if (i.collapseButtonRef.current?.ariaExpanded === "true") {
|
if (
|
||||||
|
i.collapseButtonRef.current?.attributes &&
|
||||||
|
i.collapseButtonRef.current?.attributes.getNamedItem("aria-expanded")
|
||||||
|
?.value === "true"
|
||||||
|
) {
|
||||||
i.collapseButtonRef.current?.click();
|
i.collapseButtonRef.current?.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,12 +80,8 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
|
||||||
document.removeEventListener("mouseup", this.handleOutsideMenuClick);
|
document.removeEventListener("mouseup", this.handleOutsideMenuClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO class active corresponding to current pages
|
||||||
render() {
|
render() {
|
||||||
return this.navbar();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO class active corresponding to current page
|
|
||||||
navbar() {
|
|
||||||
const siteView = this.props.siteRes?.site_view;
|
const siteView = this.props.siteRes?.site_view;
|
||||||
const person = UserService.Instance.myUserInfo?.local_user_view.person;
|
const person = UserService.Instance.myUserInfo?.local_user_view.person;
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -66,10 +66,9 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
||||||
{i18n.t(this.props.multiple ? "language_plural" : "language")}
|
{i18n.t(this.props.multiple ? "language_plural" : "language")}
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(`col-sm-${this.props.multiple ? 9 : 10}`, {
|
||||||
"input-group",
|
"input-group": this.props.multiple,
|
||||||
`col-sm-${this.props.multiple ? 9 : 10}`
|
})}
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{this.selectBtn}
|
{this.selectBtn}
|
||||||
{this.props.multiple && (
|
{this.props.multiple && (
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
import classNames from "classnames";
|
||||||
import { Component, InfernoNode, linkEvent } from "inferno";
|
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||||
|
|
||||||
interface TabItem {
|
interface TabItem {
|
||||||
key: string;
|
key: string;
|
||||||
getNode: () => InfernoNode;
|
getNode: (isSelected: boolean) => InfernoNode;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,24 +31,33 @@ export default class Tabs extends Component<TabsProps, TabsState> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ul className="nav nav-tabs mb-2">
|
<ul className="nav nav-tabs mb-2" role="tablist">
|
||||||
{this.props.tabs.map(({ key, label }) => (
|
{this.props.tabs.map(({ key, label }) => (
|
||||||
<li key={key} className="nav-item">
|
<li key={key} className="nav-item">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`nav-link btn${
|
className={classNames("nav-link", {
|
||||||
this.state?.currentTab === key ? " active" : ""
|
active: this.state?.currentTab === key,
|
||||||
}`}
|
})}
|
||||||
onClick={linkEvent({ ctx: this, tab: key }, handleSwitchTab)}
|
onClick={linkEvent({ ctx: this, tab: key }, handleSwitchTab)}
|
||||||
|
aria-controls={`${key}-tab-pane`}
|
||||||
|
{...(this.state?.currentTab === key && {
|
||||||
|
...{
|
||||||
|
"aria-current": "page",
|
||||||
|
"aria-selected": "true",
|
||||||
|
},
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{this.props.tabs
|
<div className="tab-content">
|
||||||
.find(tab => tab.key === this.state?.currentTab)
|
{this.props.tabs.map(({ key, getNode }) => {
|
||||||
?.getNode()}
|
return getNode(this.state?.currentTab === key);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,19 +100,17 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
const { listingType, page } = this.getCommunitiesQueryParams();
|
const { listingType, page } = this.getCommunitiesQueryParams();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="row">
|
<h1 className="h4">{i18n.t("list_of_communities")}</h1>
|
||||||
<div className="col-md-6">
|
<div className="row g-2 justify-content-between">
|
||||||
<h4>{i18n.t("list_of_communities")}</h4>
|
<div className="col-auto">
|
||||||
<span className="mb-2">
|
<ListingTypeSelect
|
||||||
<ListingTypeSelect
|
type_={listingType}
|
||||||
type_={listingType}
|
showLocal={showLocal(this.isoData)}
|
||||||
showLocal={showLocal(this.isoData)}
|
showSubscribed
|
||||||
showSubscribed
|
onChange={this.handleListingTypeChange}
|
||||||
onChange={this.handleListingTypeChange}
|
/>
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">{this.searchForm()}</div>
|
<div className="col-auto">{this.searchForm()}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
|
@ -220,14 +218,14 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
searchForm() {
|
searchForm() {
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="row justify-content-end"
|
className="row mb-2"
|
||||||
onSubmit={linkEvent(this, this.handleSearchSubmit)}
|
onSubmit={linkEvent(this, this.handleSearchSubmit)}
|
||||||
>
|
>
|
||||||
<div className="col-auto">
|
<div className="col-auto">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="communities-search"
|
id="communities-search"
|
||||||
className="form-control me-2 mb-2"
|
className="form-control"
|
||||||
value={this.state.searchText}
|
value={this.state.searchText}
|
||||||
placeholder={`${i18n.t("search")}...`}
|
placeholder={`${i18n.t("search")}...`}
|
||||||
onInput={linkEvent(this, this.handleSearchChange)}
|
onInput={linkEvent(this, this.handleSearchChange)}
|
||||||
|
@ -239,7 +237,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
<label className="visually-hidden" htmlFor="communities-search">
|
<label className="visually-hidden" htmlFor="communities-search">
|
||||||
{i18n.t("search")}
|
{i18n.t("search")}
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" className="btn btn-secondary mb-2">
|
<button type="submit" className="btn btn-secondary">
|
||||||
<span>{i18n.t("search")}</span>
|
<span>{i18n.t("search")}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import classNames from "classnames";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import {
|
import {
|
||||||
BannedPersonsResponse,
|
BannedPersonsResponse,
|
||||||
|
@ -130,22 +131,30 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
{
|
{
|
||||||
key: "site",
|
key: "site",
|
||||||
label: i18n.t("site"),
|
label: i18n.t("site"),
|
||||||
getNode: () => (
|
getNode: isSelected => (
|
||||||
<div className="row">
|
<div
|
||||||
<div className="col-12 col-md-6">
|
className={classNames("tab-pane show", {
|
||||||
<SiteForm
|
active: isSelected,
|
||||||
showLocal={showLocal(this.isoData)}
|
})}
|
||||||
allowedInstances={federationData?.allowed}
|
role="tabpanel"
|
||||||
blockedInstances={federationData?.blocked}
|
id="site-tab-pane"
|
||||||
onSaveSite={this.handleEditSite}
|
>
|
||||||
siteRes={this.state.siteRes}
|
<div className="row">
|
||||||
themeList={this.state.themeList}
|
<div className="col-12 col-md-6">
|
||||||
loading={this.state.loading}
|
<SiteForm
|
||||||
/>
|
showLocal={showLocal(this.isoData)}
|
||||||
</div>
|
allowedInstances={federationData?.allowed}
|
||||||
<div className="col-12 col-md-6">
|
blockedInstances={federationData?.blocked}
|
||||||
{this.admins()}
|
onSaveSite={this.handleEditSite}
|
||||||
{this.bannedUsers()}
|
siteRes={this.state.siteRes}
|
||||||
|
themeList={this.state.themeList}
|
||||||
|
loading={this.state.loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
{this.admins()}
|
||||||
|
{this.bannedUsers()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
@ -153,40 +162,64 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
{
|
{
|
||||||
key: "rate_limiting",
|
key: "rate_limiting",
|
||||||
label: "Rate Limiting",
|
label: "Rate Limiting",
|
||||||
getNode: () => (
|
getNode: isSelected => (
|
||||||
<RateLimitForm
|
<div
|
||||||
rateLimits={
|
className={classNames("tab-pane", {
|
||||||
this.state.siteRes.site_view.local_site_rate_limit
|
active: isSelected,
|
||||||
}
|
})}
|
||||||
onSaveSite={this.handleEditSite}
|
role="tabpanel"
|
||||||
loading={this.state.loading}
|
id="rate_limiting-tab-pane"
|
||||||
/>
|
>
|
||||||
),
|
<RateLimitForm
|
||||||
},
|
rateLimits={
|
||||||
{
|
this.state.siteRes.site_view.local_site_rate_limit
|
||||||
key: "taglines",
|
}
|
||||||
label: i18n.t("taglines"),
|
|
||||||
getNode: () => (
|
|
||||||
<div className="row">
|
|
||||||
<TaglineForm
|
|
||||||
taglines={this.state.siteRes.taglines}
|
|
||||||
onSaveSite={this.handleEditSite}
|
onSaveSite={this.handleEditSite}
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "taglines",
|
||||||
|
label: i18n.t("taglines"),
|
||||||
|
getNode: isSelected => (
|
||||||
|
<div
|
||||||
|
className={classNames("tab-pane", {
|
||||||
|
active: isSelected,
|
||||||
|
})}
|
||||||
|
role="tabpanel"
|
||||||
|
id="taglines-tab-pane"
|
||||||
|
>
|
||||||
|
<div className="row">
|
||||||
|
<TaglineForm
|
||||||
|
taglines={this.state.siteRes.taglines}
|
||||||
|
onSaveSite={this.handleEditSite}
|
||||||
|
loading={this.state.loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "emojis",
|
key: "emojis",
|
||||||
label: i18n.t("emojis"),
|
label: i18n.t("emojis"),
|
||||||
getNode: () => (
|
getNode: isSelected => (
|
||||||
<div className="row">
|
<div
|
||||||
<EmojiForm
|
className={classNames("tab-pane", {
|
||||||
onCreate={this.handleCreateEmoji}
|
active: isSelected,
|
||||||
onDelete={this.handleDeleteEmoji}
|
})}
|
||||||
onEdit={this.handleEditEmoji}
|
role="tabpanel"
|
||||||
loading={this.state.emojiLoading}
|
id="emojis-tab-pane"
|
||||||
/>
|
>
|
||||||
|
<div className="row">
|
||||||
|
<EmojiForm
|
||||||
|
onCreate={this.handleCreateEmoji}
|
||||||
|
onDelete={this.handleDeleteEmoji}
|
||||||
|
onEdit={this.handleEditEmoji}
|
||||||
|
loading={this.state.emojiLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
@ -570,8 +570,6 @@ export class Home extends Component<any, HomeState> {
|
||||||
data-tippy-content={
|
data-tippy-content={
|
||||||
subscribedCollapsed ? i18n.t("expand") : i18n.t("collapse")
|
subscribedCollapsed ? i18n.t("expand") : i18n.t("collapse")
|
||||||
}
|
}
|
||||||
data-bs-toggle="collapse"
|
|
||||||
data-bs-target="#sidebarSubscribedBody"
|
|
||||||
aria-expanded="true"
|
aria-expanded="true"
|
||||||
aria-controls="sidebarSubscribedBody"
|
aria-controls="sidebarSubscribedBody"
|
||||||
>
|
>
|
||||||
|
@ -582,24 +580,25 @@ export class Home extends Component<any, HomeState> {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
<div
|
{!subscribedCollapsed && (
|
||||||
id="sidebarSubscribedBody"
|
<div
|
||||||
className="collapse show"
|
id="sidebarSubscribedBody"
|
||||||
aria-labelledby="sidebarSubscribedHeader"
|
aria-labelledby="sidebarSubscribedHeader"
|
||||||
>
|
>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<ul className="list-inline mb-0">
|
<ul className="list-inline mb-0">
|
||||||
{UserService.Instance.myUserInfo?.follows.map(cfv => (
|
{UserService.Instance.myUserInfo?.follows.map(cfv => (
|
||||||
<li
|
<li
|
||||||
key={cfv.community.id}
|
key={cfv.community.id}
|
||||||
className="list-inline-item d-inline-block"
|
className="list-inline-item d-inline-block"
|
||||||
>
|
>
|
||||||
<CommunityLink community={cfv.community} />
|
<CommunityLink community={cfv.community} />
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import classNames from "classnames";
|
||||||
import { Component, FormEventHandler, linkEvent } from "inferno";
|
import { Component, FormEventHandler, linkEvent } from "inferno";
|
||||||
import { EditSite, LocalSiteRateLimit } from "lemmy-js-client";
|
import { EditSite, LocalSiteRateLimit } from "lemmy-js-client";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
|
@ -19,6 +20,7 @@ interface RateLimitsProps {
|
||||||
handleRateLimitPerSecond: FormEventHandler<HTMLInputElement>;
|
handleRateLimitPerSecond: FormEventHandler<HTMLInputElement>;
|
||||||
rateLimitValue?: number;
|
rateLimitValue?: number;
|
||||||
rateLimitPerSecondValue?: number;
|
rateLimitPerSecondValue?: number;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RateLimitFormProps {
|
interface RateLimitFormProps {
|
||||||
|
@ -49,9 +51,10 @@ function RateLimits({
|
||||||
handleRateLimitPerSecond,
|
handleRateLimitPerSecond,
|
||||||
rateLimitPerSecondValue,
|
rateLimitPerSecondValue,
|
||||||
rateLimitValue,
|
rateLimitValue,
|
||||||
|
className,
|
||||||
}: RateLimitsProps) {
|
}: RateLimitsProps) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 row">
|
<div role="tabpanel" className={classNames("mb-3 row", className)}>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label htmlFor="rate-limit">{i18n.t("rate_limit")}</label>
|
<label htmlFor="rate-limit">{i18n.t("rate_limit")}</label>
|
||||||
<input
|
<input
|
||||||
|
@ -142,8 +145,11 @@ export default class RateLimitsForm extends Component<
|
||||||
tabs={rateLimitTypes.map(rateLimitType => ({
|
tabs={rateLimitTypes.map(rateLimitType => ({
|
||||||
key: rateLimitType,
|
key: rateLimitType,
|
||||||
label: i18n.t(`rate_limit_${rateLimitType}`),
|
label: i18n.t(`rate_limit_${rateLimitType}`),
|
||||||
getNode: () => (
|
getNode: isSelected => (
|
||||||
<RateLimits
|
<RateLimits
|
||||||
|
className={classNames("tab-pane show", {
|
||||||
|
active: isSelected,
|
||||||
|
})}
|
||||||
handleRateLimit={linkEvent(
|
handleRateLimit={linkEvent(
|
||||||
{ rateLimitType, ctx: this },
|
{ rateLimitType, ctx: this },
|
||||||
handleRateLimitChange
|
handleRateLimitChange
|
||||||
|
|
|
@ -42,13 +42,11 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div
|
{!this.state.collapsed && (
|
||||||
id="sidebarInfoBody"
|
<div id="sidebarInfoBody" aria-labelledby="sidebarInfoHeader">
|
||||||
className="collapse show"
|
<div className="card-body">{this.siteInfo()}</div>
|
||||||
aria-labelledby="sidebarInfoHeader"
|
</div>
|
||||||
>
|
)}
|
||||||
<div className="card-body">{this.siteInfo()}</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { debounce } from "@utils/helpers";
|
import { debounce } from "@utils/helpers";
|
||||||
|
import classNames from "classnames";
|
||||||
import { NoOptionI18nKeys } from "i18next";
|
import { NoOptionI18nKeys } from "i18next";
|
||||||
import { Component, linkEvent } from "inferno";
|
import { Component, linkEvent } from "inferno";
|
||||||
import {
|
import {
|
||||||
|
@ -265,34 +266,50 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
userSettings() {
|
userSettings(isSelected) {
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div
|
||||||
<div className="col-12 col-md-6">
|
className={classNames("tab-pane show", {
|
||||||
<div className="card border-secondary mb-3">
|
active: isSelected,
|
||||||
<div className="card-body">{this.saveUserSettingsHtmlForm()}</div>
|
})}
|
||||||
|
role="tabpanel"
|
||||||
|
id="settings-tab-pane"
|
||||||
|
>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<div className="card border-secondary mb-3">
|
||||||
|
<div className="card-body">{this.saveUserSettingsHtmlForm()}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="col-12 col-md-6">
|
||||||
<div className="col-12 col-md-6">
|
<div className="card border-secondary mb-3">
|
||||||
<div className="card border-secondary mb-3">
|
<div className="card-body">{this.changePasswordHtmlForm()}</div>
|
||||||
<div className="card-body">{this.changePasswordHtmlForm()}</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
blockCards() {
|
blockCards(isSelected) {
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div
|
||||||
<div className="col-12 col-md-6">
|
className={classNames("tab-pane", {
|
||||||
<div className="card border-secondary mb-3">
|
active: isSelected,
|
||||||
<div className="card-body">{this.blockUserCard()}</div>
|
})}
|
||||||
|
role="tabpanel"
|
||||||
|
id="blocks-tab-pane"
|
||||||
|
>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-12 col-md-6">
|
||||||
|
<div className="card border-secondary mb-3">
|
||||||
|
<div className="card-body">{this.blockUserCard()}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="col-12 col-md-6">
|
||||||
<div className="col-12 col-md-6">
|
<div className="card border-secondary mb-3">
|
||||||
<div className="card border-secondary mb-3">
|
<div className="card-body">{this.blockCommunityCard()}</div>
|
||||||
<div className="card-body">{this.blockCommunityCard()}</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -114,7 +114,7 @@ export class CreatePost extends Component<
|
||||||
if (res.state === "success") {
|
if (res.state === "success") {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedCommunityChoice: {
|
selectedCommunityChoice: {
|
||||||
label: res.data.community_view.community.name,
|
label: res.data.community_view.community.title,
|
||||||
value: res.data.community_view.community.id.toString(),
|
value: res.data.community_view.community.id.toString(),
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -178,7 +178,7 @@ export class CreatePost extends Component<
|
||||||
id="createPostForm"
|
id="createPostForm"
|
||||||
className="col-12 col-lg-6 offset-lg-3 mb-4"
|
className="col-12 col-lg-6 offset-lg-3 mb-4"
|
||||||
>
|
>
|
||||||
<h5>{i18n.t("create_post")}</h5>
|
<h1 className="h4">{i18n.t("create_post")}</h1>
|
||||||
<PostForm
|
<PostForm
|
||||||
onCreate={this.handlePostCreate}
|
onCreate={this.handlePostCreate}
|
||||||
params={locationState}
|
params={locationState}
|
||||||
|
|
|
@ -79,6 +79,143 @@ interface PostFormState {
|
||||||
submitted: boolean;
|
submitted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handlePostSubmit(i: PostForm, event: any) {
|
||||||
|
event.preventDefault();
|
||||||
|
// Coerce empty url string to undefined
|
||||||
|
if ((i.state.form.url ?? "") === "") {
|
||||||
|
i.setState(s => ((s.form.url = undefined), s));
|
||||||
|
}
|
||||||
|
i.setState({ loading: true, submitted: true });
|
||||||
|
const auth = myAuthRequired();
|
||||||
|
|
||||||
|
const pForm = i.state.form;
|
||||||
|
const pv = i.props.post_view;
|
||||||
|
|
||||||
|
if (pv) {
|
||||||
|
i.props.onEdit?.({
|
||||||
|
name: pForm.name,
|
||||||
|
url: pForm.url,
|
||||||
|
body: pForm.body,
|
||||||
|
nsfw: pForm.nsfw,
|
||||||
|
post_id: pv.post.id,
|
||||||
|
language_id: pForm.language_id,
|
||||||
|
auth,
|
||||||
|
});
|
||||||
|
} else if (pForm.name && pForm.community_id) {
|
||||||
|
i.props.onCreate?.({
|
||||||
|
name: pForm.name,
|
||||||
|
community_id: pForm.community_id,
|
||||||
|
url: pForm.url,
|
||||||
|
body: pForm.body,
|
||||||
|
nsfw: pForm.nsfw,
|
||||||
|
language_id: pForm.language_id,
|
||||||
|
honeypot: pForm.honeypot,
|
||||||
|
auth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copySuggestedTitle(d: { i: PostForm; suggestedTitle?: string }) {
|
||||||
|
const sTitle = d.suggestedTitle;
|
||||||
|
if (sTitle) {
|
||||||
|
d.i.setState(
|
||||||
|
s => ((s.form.name = sTitle?.substring(0, MAX_POST_TITLE_LENGTH)), s)
|
||||||
|
);
|
||||||
|
d.i.setState({ suggestedPostsRes: { state: "empty" } });
|
||||||
|
setTimeout(() => {
|
||||||
|
const textarea: any = document.getElementById("post-title");
|
||||||
|
autosize.update(textarea);
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePostUrlChange(i: PostForm, event: any) {
|
||||||
|
const url = event.target.value;
|
||||||
|
|
||||||
|
i.setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
form: {
|
||||||
|
...prev.form,
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
imageDeleteUrl: "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
i.fetchPageTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePostNsfwChange(i: PostForm, event: any) {
|
||||||
|
i.setState(s => ((s.form.nsfw = event.target.checked), s));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHoneyPotChange(i: PostForm, event: any) {
|
||||||
|
i.setState(s => ((s.form.honeypot = event.target.value), s));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel(i: PostForm) {
|
||||||
|
i.props.onCancel?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImageUploadPaste(i: PostForm, event: any) {
|
||||||
|
const image = event.clipboardData.files[0];
|
||||||
|
if (image) {
|
||||||
|
handleImageUpload(i, image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImageUpload(i: PostForm, event: any) {
|
||||||
|
let file: any;
|
||||||
|
if (event.target) {
|
||||||
|
event.preventDefault();
|
||||||
|
file = event.target.files[0];
|
||||||
|
} else {
|
||||||
|
file = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.setState({ imageLoading: true });
|
||||||
|
|
||||||
|
HttpService.client.uploadImage({ image: file }).then(res => {
|
||||||
|
console.log("pictrs upload:");
|
||||||
|
console.log(res);
|
||||||
|
if (res.state === "success") {
|
||||||
|
if (res.data.msg === "ok") {
|
||||||
|
i.state.form.url = res.data.url;
|
||||||
|
i.setState({
|
||||||
|
imageLoading: false,
|
||||||
|
imageDeleteUrl: res.data.delete_url as string,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast(JSON.stringify(res), "danger");
|
||||||
|
}
|
||||||
|
} else if (res.state === "failed") {
|
||||||
|
console.error(res.msg);
|
||||||
|
toast(res.msg, "danger");
|
||||||
|
i.setState({ imageLoading: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePostNameChange(i: PostForm, event: any) {
|
||||||
|
i.setState(s => ((s.form.name = event.target.value), s));
|
||||||
|
i.fetchSimilarPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImageDelete(i: PostForm) {
|
||||||
|
const { imageDeleteUrl } = i.state;
|
||||||
|
|
||||||
|
fetch(imageDeleteUrl);
|
||||||
|
|
||||||
|
i.setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
imageDeleteUrl: "",
|
||||||
|
imageLoading: false,
|
||||||
|
form: {
|
||||||
|
...prev.form,
|
||||||
|
url: "",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export class PostForm extends Component<PostFormProps, PostFormState> {
|
export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
state: PostFormState = {
|
state: PostFormState = {
|
||||||
suggestedPostsRes: { state: "empty" },
|
suggestedPostsRes: { state: "empty" },
|
||||||
|
@ -123,16 +260,16 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
...this.state.form,
|
...this.state.form,
|
||||||
community_id: getIdFromString(selectedCommunityChoice.value),
|
community_id: getIdFromString(selectedCommunityChoice.value),
|
||||||
},
|
},
|
||||||
communitySearchOptions: [selectedCommunityChoice]
|
communitySearchOptions: [selectedCommunityChoice].concat(
|
||||||
.concat(
|
(
|
||||||
this.props.initialCommunities?.map(
|
this.props.initialCommunities?.map(
|
||||||
({ community: { id, title } }) => ({
|
({ community: { id, title } }) => ({
|
||||||
label: title,
|
label: title,
|
||||||
value: id.toString(),
|
value: id.toString(),
|
||||||
})
|
})
|
||||||
) ?? []
|
) ?? []
|
||||||
)
|
).filter(option => option.value !== selectedCommunityChoice.value)
|
||||||
.filter(option => option.value !== selectedCommunityChoice.value),
|
),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -188,15 +325,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
|
|
||||||
const url = this.state.form.url;
|
const url = this.state.form.url;
|
||||||
|
|
||||||
// TODO
|
|
||||||
// const promptCheck =
|
|
||||||
// !!this.state.form.name || !!this.state.form.url || !!this.state.form.body;
|
|
||||||
// <Prompt when={promptCheck} message={i18n.t("block_leaving")} />
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form className="post-form" onSubmit={linkEvent(this, handlePostSubmit)}>
|
||||||
className="post-form"
|
|
||||||
onSubmit={linkEvent(this, this.handlePostSubmit)}
|
|
||||||
>
|
|
||||||
<NavigationPrompt
|
<NavigationPrompt
|
||||||
when={
|
when={
|
||||||
!!(
|
!!(
|
||||||
|
@ -215,9 +345,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
type="url"
|
type="url"
|
||||||
id="post-url"
|
id="post-url"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
value={this.state.form.url}
|
value={url}
|
||||||
onInput={linkEvent(this, this.handlePostUrlChange)}
|
onInput={linkEvent(this, handlePostUrlChange)}
|
||||||
onPaste={linkEvent(this, this.handleImageUploadPaste)}
|
onPaste={linkEvent(this, handleImageUploadPaste)}
|
||||||
/>
|
/>
|
||||||
{this.renderSuggestedTitleCopy()}
|
{this.renderSuggestedTitleCopy()}
|
||||||
<form>
|
<form>
|
||||||
|
@ -237,7 +367,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
name="file"
|
name="file"
|
||||||
className="d-none"
|
className="d-none"
|
||||||
disabled={!UserService.Instance.myUserInfo}
|
disabled={!UserService.Instance.myUserInfo}
|
||||||
onChange={linkEvent(this, this.handleImageUpload)}
|
onChange={linkEvent(this, handleImageUpload)}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
{url && validURL(url) && (
|
{url && validURL(url) && (
|
||||||
|
@ -276,7 +406,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
{this.state.imageDeleteUrl && (
|
{this.state.imageDeleteUrl && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-danger btn-sm mt-2"
|
className="btn btn-danger btn-sm mt-2"
|
||||||
onClick={linkEvent(this, this.handleImageDelete)}
|
onClick={linkEvent(this, handleImageDelete)}
|
||||||
aria-label={i18n.t("delete")}
|
aria-label={i18n.t("delete")}
|
||||||
data-tippy-content={i18n.t("delete")}
|
data-tippy-content={i18n.t("delete")}
|
||||||
>
|
>
|
||||||
|
@ -327,7 +457,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
<textarea
|
<textarea
|
||||||
value={this.state.form.name}
|
value={this.state.form.name}
|
||||||
id="post-title"
|
id="post-title"
|
||||||
onInput={linkEvent(this, this.handlePostNameChange)}
|
onInput={linkEvent(this, handlePostNameChange)}
|
||||||
className={`form-control ${
|
className={`form-control ${
|
||||||
!validTitle(this.state.form.name) && "is-invalid"
|
!validTitle(this.state.form.name) && "is-invalid"
|
||||||
}`}
|
}`}
|
||||||
|
@ -357,6 +487,13 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<LanguageSelect
|
||||||
|
allLanguages={this.props.allLanguages}
|
||||||
|
siteLanguages={this.props.siteLanguages}
|
||||||
|
selectedLanguageIds={selectedLangs}
|
||||||
|
multiple={false}
|
||||||
|
onChange={this.handleLanguageChange}
|
||||||
|
/>
|
||||||
{!this.props.post_view && (
|
{!this.props.post_view && (
|
||||||
<div className="mb-3 row">
|
<div className="mb-3 row">
|
||||||
<label className="col-sm-2 col-form-label" htmlFor="post-community">
|
<label className="col-sm-2 col-form-label" htmlFor="post-community">
|
||||||
|
@ -381,30 +518,17 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{this.props.enableNsfw && (
|
{this.props.enableNsfw && (
|
||||||
<div className="mb-3 row">
|
<div className="form-check mb-3">
|
||||||
<legend className="col-form-label col-sm-2 pt-0">
|
<input
|
||||||
{i18n.t("nsfw")}
|
className="form-check-input"
|
||||||
</legend>
|
id="post-nsfw"
|
||||||
<div className="col-sm-10">
|
type="checkbox"
|
||||||
<div className="form-check">
|
checked={this.state.form.nsfw}
|
||||||
<input
|
onChange={linkEvent(this, handlePostNsfwChange)}
|
||||||
className="form-check-input position-static"
|
/>
|
||||||
id="post-nsfw"
|
<label className="form-check-label">{i18n.t("nsfw")}</label>
|
||||||
type="checkbox"
|
|
||||||
checked={this.state.form.nsfw}
|
|
||||||
onChange={linkEvent(this, this.handlePostNsfwChange)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<LanguageSelect
|
|
||||||
allLanguages={this.props.allLanguages}
|
|
||||||
siteLanguages={this.props.siteLanguages}
|
|
||||||
selectedLanguageIds={selectedLangs}
|
|
||||||
multiple={false}
|
|
||||||
onChange={this.handleLanguageChange}
|
|
||||||
/>
|
|
||||||
<input
|
<input
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
autoComplete="false"
|
autoComplete="false"
|
||||||
|
@ -413,7 +537,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
className="form-control honeypot"
|
className="form-control honeypot"
|
||||||
id="register-honey"
|
id="register-honey"
|
||||||
value={this.state.form.honeypot}
|
value={this.state.form.honeypot}
|
||||||
onInput={linkEvent(this, this.handleHoneyPotChange)}
|
onInput={linkEvent(this, handleHoneyPotChange)}
|
||||||
/>
|
/>
|
||||||
<div className="mb-3 row">
|
<div className="mb-3 row">
|
||||||
<div className="col-sm-10">
|
<div className="col-sm-10">
|
||||||
|
@ -434,7 +558,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
onClick={linkEvent(this, this.handleCancel)}
|
onClick={linkEvent(this, handleCancel)}
|
||||||
>
|
>
|
||||||
{i18n.t("cancel")}
|
{i18n.t("cancel")}
|
||||||
</button>
|
</button>
|
||||||
|
@ -459,7 +583,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
role="button"
|
role="button"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(
|
||||||
{ i: this, suggestedTitle },
|
{ i: this, suggestedTitle },
|
||||||
this.copySuggestedTitle
|
copySuggestedTitle
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{i18n.t("copy_suggested_title", { title: "" })} {suggestedTitle}
|
{i18n.t("copy_suggested_title", { title: "" })} {suggestedTitle}
|
||||||
|
@ -517,69 +641,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePostSubmit(i: PostForm, event: any) {
|
|
||||||
event.preventDefault();
|
|
||||||
// Coerce empty url string to undefined
|
|
||||||
if ((i.state.form.url ?? "") === "") {
|
|
||||||
i.setState(s => ((s.form.url = undefined), s));
|
|
||||||
}
|
|
||||||
i.setState({ loading: true, submitted: true });
|
|
||||||
const auth = myAuthRequired();
|
|
||||||
|
|
||||||
const pForm = i.state.form;
|
|
||||||
const pv = i.props.post_view;
|
|
||||||
|
|
||||||
if (pv) {
|
|
||||||
i.props.onEdit?.({
|
|
||||||
name: pForm.name,
|
|
||||||
url: pForm.url,
|
|
||||||
body: pForm.body,
|
|
||||||
nsfw: pForm.nsfw,
|
|
||||||
post_id: pv.post.id,
|
|
||||||
language_id: pForm.language_id,
|
|
||||||
auth,
|
|
||||||
});
|
|
||||||
} else if (pForm.name && pForm.community_id) {
|
|
||||||
i.props.onCreate?.({
|
|
||||||
name: pForm.name,
|
|
||||||
community_id: pForm.community_id,
|
|
||||||
url: pForm.url,
|
|
||||||
body: pForm.body,
|
|
||||||
nsfw: pForm.nsfw,
|
|
||||||
language_id: pForm.language_id,
|
|
||||||
honeypot: pForm.honeypot,
|
|
||||||
auth,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
copySuggestedTitle(d: { i: PostForm; suggestedTitle?: string }) {
|
|
||||||
const sTitle = d.suggestedTitle;
|
|
||||||
if (sTitle) {
|
|
||||||
d.i.setState(
|
|
||||||
s => ((s.form.name = sTitle?.substring(0, MAX_POST_TITLE_LENGTH)), s)
|
|
||||||
);
|
|
||||||
d.i.setState({ suggestedPostsRes: { state: "empty" } });
|
|
||||||
setTimeout(() => {
|
|
||||||
const textarea: any = document.getElementById("post-title");
|
|
||||||
autosize.update(textarea);
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePostUrlChange(i: PostForm, event: any) {
|
|
||||||
const url = event.target.value;
|
|
||||||
|
|
||||||
i.setState({
|
|
||||||
form: {
|
|
||||||
url,
|
|
||||||
},
|
|
||||||
imageDeleteUrl: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
i.fetchPageTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchPageTitle() {
|
async fetchPageTitle() {
|
||||||
const url = this.state.form.url;
|
const url = this.state.form.url;
|
||||||
if (url && validURL(url)) {
|
if (url && validURL(url)) {
|
||||||
|
@ -590,11 +651,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePostNameChange(i: PostForm, event: any) {
|
|
||||||
i.setState(s => ((s.form.name = event.target.value), s));
|
|
||||||
i.fetchSimilarPosts();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchSimilarPosts() {
|
async fetchSimilarPosts() {
|
||||||
const q = this.state.form.name;
|
const q = this.state.form.name;
|
||||||
if (q && q !== "") {
|
if (q && q !== "") {
|
||||||
|
@ -618,84 +674,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
this.setState(s => ((s.form.body = val), s));
|
this.setState(s => ((s.form.body = val), s));
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePostCommunityChange(i: PostForm, event: any) {
|
|
||||||
i.setState(s => ((s.form.community_id = Number(event.target.value)), s));
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePostNsfwChange(i: PostForm, event: any) {
|
|
||||||
i.setState(s => ((s.form.nsfw = event.target.checked), s));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLanguageChange(val: number[]) {
|
handleLanguageChange(val: number[]) {
|
||||||
this.setState(s => ((s.form.language_id = val.at(0)), s));
|
this.setState(s => ((s.form.language_id = val.at(0)), s));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHoneyPotChange(i: PostForm, event: any) {
|
|
||||||
i.setState(s => ((s.form.honeypot = event.target.value), s));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancel(i: PostForm) {
|
|
||||||
i.props.onCancel?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePreviewToggle(i: PostForm, event: any) {
|
|
||||||
event.preventDefault();
|
|
||||||
i.setState({ previewMode: !i.state.previewMode });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleImageUploadPaste(i: PostForm, event: any) {
|
|
||||||
const image = event.clipboardData.files[0];
|
|
||||||
if (image) {
|
|
||||||
i.handleImageUpload(i, image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleImageUpload(i: PostForm, event: any) {
|
|
||||||
let file: any;
|
|
||||||
if (event.target) {
|
|
||||||
event.preventDefault();
|
|
||||||
file = event.target.files[0];
|
|
||||||
} else {
|
|
||||||
file = event;
|
|
||||||
}
|
|
||||||
|
|
||||||
i.setState({ imageLoading: true });
|
|
||||||
|
|
||||||
HttpService.client.uploadImage({ image: file }).then(res => {
|
|
||||||
console.log("pictrs upload:");
|
|
||||||
console.log(res);
|
|
||||||
if (res.state === "success") {
|
|
||||||
if (res.data.msg === "ok") {
|
|
||||||
i.state.form.url = res.data.url;
|
|
||||||
i.setState({
|
|
||||||
imageLoading: false,
|
|
||||||
imageDeleteUrl: res.data.delete_url as string,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast(JSON.stringify(res), "danger");
|
|
||||||
}
|
|
||||||
} else if (res.state === "failed") {
|
|
||||||
console.error(res.msg);
|
|
||||||
toast(res.msg, "danger");
|
|
||||||
i.setState({ imageLoading: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleImageDelete(i: PostForm) {
|
|
||||||
const { imageDeleteUrl } = i.state;
|
|
||||||
|
|
||||||
fetch(imageDeleteUrl);
|
|
||||||
|
|
||||||
i.setState({
|
|
||||||
imageDeleteUrl: "",
|
|
||||||
imageLoading: false,
|
|
||||||
form: {
|
|
||||||
url: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCommunitySearch = debounce(async (text: string) => {
|
handleCommunitySearch = debounce(async (text: string) => {
|
||||||
const { selectedCommunityChoice } = this.props;
|
const { selectedCommunityChoice } = this.props;
|
||||||
this.setState({ communitySearchLoading: true });
|
this.setState({ communitySearchLoading: true });
|
||||||
|
|
|
@ -734,24 +734,22 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
get commentsButton() {
|
get commentsButton() {
|
||||||
const post_view = this.postView;
|
const post_view = this.postView;
|
||||||
|
const title = i18n.t("number_of_comments", {
|
||||||
|
count: Number(post_view.counts.comments),
|
||||||
|
formattedCount: Number(post_view.counts.comments),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className="btn btn-link text-muted ps-0 text-muted"
|
className="btn btn-link btn-sm text-muted ps-0"
|
||||||
title={i18n.t("number_of_comments", {
|
title={title}
|
||||||
count: Number(post_view.counts.comments),
|
|
||||||
formattedCount: Number(post_view.counts.comments),
|
|
||||||
})}
|
|
||||||
to={`/post/${post_view.post.id}?scrollToComments=true`}
|
to={`/post/${post_view.post.id}?scrollToComments=true`}
|
||||||
|
data-tippy-content={title}
|
||||||
>
|
>
|
||||||
<Icon icon="message-square" classes="me-1" inline />
|
<Icon icon="message-square" classes="me-1" inline />
|
||||||
<span className="me-2">
|
{post_view.counts.comments}
|
||||||
{i18n.t("number_of_comments", {
|
|
||||||
count: Number(post_view.counts.comments),
|
|
||||||
formattedCount: numToSI(post_view.counts.comments),
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
{this.unreadCount && (
|
{this.unreadCount && (
|
||||||
<span className="small text-warning">
|
<span className="badge text-bg-warning">
|
||||||
({this.unreadCount} {i18n.t("new")})
|
({this.unreadCount} {i18n.t("new")})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -65,10 +65,10 @@ export const favIconPngUrl = "/static/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";
|
||||||
export const donateLemmyUrl = `${joinLemmyUrl}/donate`;
|
export const donateLemmyUrl = `${joinLemmyUrl}/donate`;
|
||||||
export const docsUrl = `${joinLemmyUrl}/docs/en/index.html`;
|
export const docsUrl = `${joinLemmyUrl}/docs/index.html`;
|
||||||
export const helpGuideUrl = `${joinLemmyUrl}/docs/en/users/01-getting-started.html`; // TODO find a way to redirect to the non-en folder
|
export const helpGuideUrl = `${joinLemmyUrl}/docs/users/01-getting-started.html`; // TODO find a way to redirect to the non-en folder
|
||||||
export const markdownHelpUrl = `${joinLemmyUrl}/docs/en/users/02-media.html`;
|
export const markdownHelpUrl = `${joinLemmyUrl}/docs/users/02-media.html`;
|
||||||
export const sortingHelpUrl = `${joinLemmyUrl}/docs/en/users/03-votes-and-ranking.html`;
|
export const sortingHelpUrl = `${joinLemmyUrl}/docs/users/03-votes-and-ranking.html`;
|
||||||
export const archiveTodayUrl = "https://archive.today";
|
export const archiveTodayUrl = "https://archive.today";
|
||||||
export const ghostArchiveUrl = "https://ghostarchive.org";
|
export const ghostArchiveUrl = "https://ghostarchive.org";
|
||||||
export const webArchiveUrl = "https://web.archive.org";
|
export const webArchiveUrl = "https://web.archive.org";
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["/*"],
|
"@/*": ["/*"],
|
||||||
"@utils/*": ["shared/utils/*"],
|
"@utils/*": ["shared/utils/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|
|
@ -5695,10 +5695,10 @@ leac@^0.6.0:
|
||||||
resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912"
|
resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912"
|
||||||
integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==
|
integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==
|
||||||
|
|
||||||
lemmy-js-client@0.18.0-rc.1:
|
lemmy-js-client@0.18.0-rc.2:
|
||||||
version "0.18.0-rc.1"
|
version "0.18.0-rc.2"
|
||||||
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.18.0-rc.1.tgz#fd0c88810572d90413696011ebaed19e3b8162d8"
|
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.18.0-rc.2.tgz#6cd8b4dc95de8f2a6f99de56819c141a394dca04"
|
||||||
integrity sha512-lQe443Nr5UCSoY+IxmT7mBe0IRF6EAZ/4PJSRoPSL+U8A+egMMBPbuxnisHzLsC+eDOWRUIgOqZlwlaRnbmuig==
|
integrity sha512-bnYs89MjlQHwVIr1YIoAvgFkCTWrXDjSgPbCJx8ijrxZXqOKW/KAgWEisfqyFpy3dYpA3/sxFjh7b4sdxM+8VA==
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-fetch "^3.1.5"
|
cross-fetch "^3.1.5"
|
||||||
form-data "^4.0.0"
|
form-data "^4.0.0"
|
||||||
|
|
Loading…
Reference in a new issue