mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-23 11:21:26 +00:00
Merge branch 'main' into added-darkly-compact-552
This commit is contained in:
commit
376ca28f1d
12 changed files with 282 additions and 290 deletions
|
@ -1 +1 @@
|
|||
Subproject commit 7fc71d0860bbe5c6d620ec27112350ffe5b9229c
|
||||
Subproject commit a241fe1255a6363c7ae1ec5a09520c066745e6ce
|
28
src/server/handlers/manifest-handler.ts
Normal file
28
src/server/handlers/manifest-handler.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
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(), 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);
|
||||
};
|
|
@ -2,6 +2,7 @@ import express from "express";
|
|||
import path from "path";
|
||||
import process from "process";
|
||||
import CatchAllHandler from "./handlers/catch-all-handler";
|
||||
import ManifestHandler from "./handlers/manifest-handler";
|
||||
import RobotsHandler from "./handlers/robots-handler";
|
||||
import ServiceWorkerHandler from "./handlers/service-worker-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("/service-worker.js", ServiceWorkerHandler);
|
||||
server.get("/manifest", ManifestHandler);
|
||||
server.get("/css/themes/:name", ThemeHandler);
|
||||
server.get("/css/themelist", ThemesListHandler);
|
||||
server.get("/*", CatchAllHandler);
|
||||
|
|
|
@ -5,32 +5,35 @@ import sharp from "sharp";
|
|||
import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces";
|
||||
import { favIconPngUrl, favIconUrl } from "../../shared/utils";
|
||||
import { fetchIconPng } from "./fetch-icon-png";
|
||||
import { generateManifestBase64 } from "./generate-manifest-base64";
|
||||
|
||||
const customHtmlHeader = process.env["LEMMY_UI_CUSTOM_HTML_HEADER"] || "";
|
||||
|
||||
let appleTouchIcon: string | undefined = undefined;
|
||||
|
||||
export async function createSsrHtml(
|
||||
root: string,
|
||||
isoData: IsoDataOptionalSite
|
||||
) {
|
||||
const site = isoData.site_res;
|
||||
|
||||
const appleTouchIcon = site?.site_view.site.icon
|
||||
? `data:image/png;base64,${sharp(
|
||||
await fetchIconPng(site.site_view.site.icon)
|
||||
)
|
||||
.resize(180, 180)
|
||||
.extend({
|
||||
bottom: 20,
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
background: "#222222",
|
||||
})
|
||||
.png()
|
||||
.toBuffer()
|
||||
.then(buf => buf.toString("base64"))}`
|
||||
: favIconPngUrl;
|
||||
if (!appleTouchIcon) {
|
||||
appleTouchIcon = site?.site_view.site.icon
|
||||
? `data:image/png;base64,${sharp(
|
||||
await fetchIconPng(site.site_view.site.icon)
|
||||
)
|
||||
.resize(180, 180)
|
||||
.extend({
|
||||
bottom: 20,
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
background: "#222222",
|
||||
})
|
||||
.png()
|
||||
.toBuffer()
|
||||
.then(buf => buf.toString("base64"))}`
|
||||
: favIconPngUrl;
|
||||
}
|
||||
|
||||
const erudaStr =
|
||||
process.env["LEMMY_UI_DEBUG"] === "true"
|
||||
|
@ -74,15 +77,7 @@ export async function createSsrHtml(
|
|||
/>
|
||||
|
||||
<!-- Web app manifest -->
|
||||
${
|
||||
site &&
|
||||
`<link
|
||||
rel="manifest"
|
||||
href=${`data:application/manifest+json;base64,${await generateManifestBase64(
|
||||
site
|
||||
)}`}
|
||||
/>`
|
||||
}
|
||||
<link rel="manifest" href="/manifest" />
|
||||
<link rel="apple-touch-icon" href=${appleTouchIcon} />
|
||||
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import sharp from "sharp";
|
|||
import { getHttpBaseExternal } from "../../shared/env";
|
||||
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(
|
||||
process.cwd(),
|
||||
|
@ -14,7 +14,7 @@ const defaultLogoPathDirectory = path.join(
|
|||
"icons"
|
||||
);
|
||||
|
||||
export async function generateManifestBase64({
|
||||
export default async function ({
|
||||
my_user,
|
||||
site_view: {
|
||||
site,
|
||||
|
@ -25,7 +25,7 @@ export async function generateManifestBase64({
|
|||
|
||||
const icon = site.icon ? await fetchIconPng(site.icon) : null;
|
||||
|
||||
const manifest = {
|
||||
return {
|
||||
name: site.name,
|
||||
description: site.description ?? "A link aggregator for the fediverse",
|
||||
start_url: url,
|
||||
|
@ -69,31 +69,24 @@ export async function generateManifestBase64({
|
|||
short_name: "Communities",
|
||||
description: "Browse communities",
|
||||
},
|
||||
]
|
||||
.concat(
|
||||
my_user
|
||||
? [
|
||||
{
|
||||
name: "Create Post",
|
||||
url: "/create_post",
|
||||
short_name: "Create Post",
|
||||
description: "Create a post.",
|
||||
},
|
||||
]
|
||||
: []
|
||||
)
|
||||
.concat(
|
||||
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",
|
||||
},
|
||||
]
|
||||
: []
|
||||
),
|
||||
{
|
||||
name: "Create Post",
|
||||
url: "/create_post",
|
||||
short_name: "Create Post",
|
||||
description: "Create a post.",
|
||||
},
|
||||
].concat(
|
||||
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: [
|
||||
{
|
||||
platform: "f-droid",
|
||||
|
@ -102,6 +95,4 @@ export async function generateManifestBase64({
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
return Buffer.from(JSON.stringify(manifest)).toString("base64");
|
||||
}
|
|
@ -66,10 +66,9 @@ export class LanguageSelect extends Component<LanguageSelectProps, any> {
|
|||
{i18n.t(this.props.multiple ? "language_plural" : "language")}
|
||||
</label>
|
||||
<div
|
||||
className={classNames(
|
||||
"input-group",
|
||||
`col-sm-${this.props.multiple ? 9 : 10}`
|
||||
)}
|
||||
className={classNames(`col-sm-${this.props.multiple ? 9 : 10}`, {
|
||||
"input-group": this.props.multiple,
|
||||
})}
|
||||
>
|
||||
{this.selectBtn}
|
||||
{this.props.multiple && (
|
||||
|
|
|
@ -100,19 +100,17 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
const { listingType, page } = this.getCommunitiesQueryParams();
|
||||
return (
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<h4>{i18n.t("list_of_communities")}</h4>
|
||||
<span className="mb-2">
|
||||
<ListingTypeSelect
|
||||
type_={listingType}
|
||||
showLocal={showLocal(this.isoData)}
|
||||
showSubscribed
|
||||
onChange={this.handleListingTypeChange}
|
||||
/>
|
||||
</span>
|
||||
<h1 className="h4">{i18n.t("list_of_communities")}</h1>
|
||||
<div className="row g-2 justify-content-between">
|
||||
<div className="col-auto">
|
||||
<ListingTypeSelect
|
||||
type_={listingType}
|
||||
showLocal={showLocal(this.isoData)}
|
||||
showSubscribed
|
||||
onChange={this.handleListingTypeChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">{this.searchForm()}</div>
|
||||
<div className="col-auto">{this.searchForm()}</div>
|
||||
</div>
|
||||
|
||||
<div className="table-responsive">
|
||||
|
@ -220,14 +218,14 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
searchForm() {
|
||||
return (
|
||||
<form
|
||||
className="row justify-content-end"
|
||||
className="row mb-2"
|
||||
onSubmit={linkEvent(this, this.handleSearchSubmit)}
|
||||
>
|
||||
<div className="col-auto">
|
||||
<input
|
||||
type="text"
|
||||
id="communities-search"
|
||||
className="form-control me-2 mb-2"
|
||||
className="form-control"
|
||||
value={this.state.searchText}
|
||||
placeholder={`${i18n.t("search")}...`}
|
||||
onInput={linkEvent(this, this.handleSearchChange)}
|
||||
|
@ -239,7 +237,7 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
<label className="visually-hidden" htmlFor="communities-search">
|
||||
{i18n.t("search")}
|
||||
</label>
|
||||
<button type="submit" className="btn btn-secondary mb-2">
|
||||
<button type="submit" className="btn btn-secondary">
|
||||
<span>{i18n.t("search")}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -570,8 +570,6 @@ export class Home extends Component<any, HomeState> {
|
|||
data-tippy-content={
|
||||
subscribedCollapsed ? i18n.t("expand") : i18n.t("collapse")
|
||||
}
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#sidebarSubscribedBody"
|
||||
aria-expanded="true"
|
||||
aria-controls="sidebarSubscribedBody"
|
||||
>
|
||||
|
@ -582,24 +580,25 @@ export class Home extends Component<any, HomeState> {
|
|||
</button>
|
||||
)}
|
||||
</header>
|
||||
<div
|
||||
id="sidebarSubscribedBody"
|
||||
className="collapse show"
|
||||
aria-labelledby="sidebarSubscribedHeader"
|
||||
>
|
||||
<div className="card-body">
|
||||
<ul className="list-inline mb-0">
|
||||
{UserService.Instance.myUserInfo?.follows.map(cfv => (
|
||||
<li
|
||||
key={cfv.community.id}
|
||||
className="list-inline-item d-inline-block"
|
||||
>
|
||||
<CommunityLink community={cfv.community} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{!subscribedCollapsed && (
|
||||
<div
|
||||
id="sidebarSubscribedBody"
|
||||
aria-labelledby="sidebarSubscribedHeader"
|
||||
>
|
||||
<div className="card-body">
|
||||
<ul className="list-inline mb-0">
|
||||
{UserService.Instance.myUserInfo?.follows.map(cfv => (
|
||||
<li
|
||||
key={cfv.community.id}
|
||||
className="list-inline-item d-inline-block"
|
||||
>
|
||||
<CommunityLink community={cfv.community} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -42,13 +42,11 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
|
|||
)}
|
||||
</header>
|
||||
|
||||
<div
|
||||
id="sidebarInfoBody"
|
||||
className="collapse show"
|
||||
aria-labelledby="sidebarInfoHeader"
|
||||
>
|
||||
<div className="card-body">{this.siteInfo()}</div>
|
||||
</div>
|
||||
{!this.state.collapsed && (
|
||||
<div id="sidebarInfoBody" aria-labelledby="sidebarInfoHeader">
|
||||
<div className="card-body">{this.siteInfo()}</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -114,7 +114,7 @@ export class CreatePost extends Component<
|
|||
if (res.state === "success") {
|
||||
this.setState({
|
||||
selectedCommunityChoice: {
|
||||
label: res.data.community_view.community.name,
|
||||
label: res.data.community_view.community.title,
|
||||
value: res.data.community_view.community.id.toString(),
|
||||
},
|
||||
loading: false,
|
||||
|
@ -178,7 +178,7 @@ export class CreatePost extends Component<
|
|||
id="createPostForm"
|
||||
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
|
||||
onCreate={this.handlePostCreate}
|
||||
params={locationState}
|
||||
|
|
|
@ -79,6 +79,143 @@ interface PostFormState {
|
|||
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> {
|
||||
state: PostFormState = {
|
||||
suggestedPostsRes: { state: "empty" },
|
||||
|
@ -123,16 +260,16 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
...this.state.form,
|
||||
community_id: getIdFromString(selectedCommunityChoice.value),
|
||||
},
|
||||
communitySearchOptions: [selectedCommunityChoice]
|
||||
.concat(
|
||||
communitySearchOptions: [selectedCommunityChoice].concat(
|
||||
(
|
||||
this.props.initialCommunities?.map(
|
||||
({ community: { id, title } }) => ({
|
||||
label: title,
|
||||
value: id.toString(),
|
||||
})
|
||||
) ?? []
|
||||
)
|
||||
.filter(option => option.value !== selectedCommunityChoice.value),
|
||||
).filter(option => option.value !== selectedCommunityChoice.value)
|
||||
),
|
||||
};
|
||||
} else {
|
||||
this.state = {
|
||||
|
@ -188,15 +325,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
|
||||
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 (
|
||||
<form
|
||||
className="post-form"
|
||||
onSubmit={linkEvent(this, this.handlePostSubmit)}
|
||||
>
|
||||
<form className="post-form" onSubmit={linkEvent(this, handlePostSubmit)}>
|
||||
<NavigationPrompt
|
||||
when={
|
||||
!!(
|
||||
|
@ -215,9 +345,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
type="url"
|
||||
id="post-url"
|
||||
className="form-control"
|
||||
value={this.state.form.url}
|
||||
onInput={linkEvent(this, this.handlePostUrlChange)}
|
||||
onPaste={linkEvent(this, this.handleImageUploadPaste)}
|
||||
value={url}
|
||||
onInput={linkEvent(this, handlePostUrlChange)}
|
||||
onPaste={linkEvent(this, handleImageUploadPaste)}
|
||||
/>
|
||||
{this.renderSuggestedTitleCopy()}
|
||||
<form>
|
||||
|
@ -237,7 +367,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
name="file"
|
||||
className="d-none"
|
||||
disabled={!UserService.Instance.myUserInfo}
|
||||
onChange={linkEvent(this, this.handleImageUpload)}
|
||||
onChange={linkEvent(this, handleImageUpload)}
|
||||
/>
|
||||
</form>
|
||||
{url && validURL(url) && (
|
||||
|
@ -276,7 +406,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
{this.state.imageDeleteUrl && (
|
||||
<button
|
||||
className="btn btn-danger btn-sm mt-2"
|
||||
onClick={linkEvent(this, this.handleImageDelete)}
|
||||
onClick={linkEvent(this, handleImageDelete)}
|
||||
aria-label={i18n.t("delete")}
|
||||
data-tippy-content={i18n.t("delete")}
|
||||
>
|
||||
|
@ -327,7 +457,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
<textarea
|
||||
value={this.state.form.name}
|
||||
id="post-title"
|
||||
onInput={linkEvent(this, this.handlePostNameChange)}
|
||||
onInput={linkEvent(this, handlePostNameChange)}
|
||||
className={`form-control ${
|
||||
!validTitle(this.state.form.name) && "is-invalid"
|
||||
}`}
|
||||
|
@ -357,6 +487,13 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<LanguageSelect
|
||||
allLanguages={this.props.allLanguages}
|
||||
siteLanguages={this.props.siteLanguages}
|
||||
selectedLanguageIds={selectedLangs}
|
||||
multiple={false}
|
||||
onChange={this.handleLanguageChange}
|
||||
/>
|
||||
{!this.props.post_view && (
|
||||
<div className="mb-3 row">
|
||||
<label className="col-sm-2 col-form-label" htmlFor="post-community">
|
||||
|
@ -381,30 +518,17 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
</div>
|
||||
)}
|
||||
{this.props.enableNsfw && (
|
||||
<div className="mb-3 row">
|
||||
<legend className="col-form-label col-sm-2 pt-0">
|
||||
{i18n.t("nsfw")}
|
||||
</legend>
|
||||
<div className="col-sm-10">
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="form-check-input position-static"
|
||||
id="post-nsfw"
|
||||
type="checkbox"
|
||||
checked={this.state.form.nsfw}
|
||||
onChange={linkEvent(this, this.handlePostNsfwChange)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-check mb-3">
|
||||
<input
|
||||
className="form-check-input"
|
||||
id="post-nsfw"
|
||||
type="checkbox"
|
||||
checked={this.state.form.nsfw}
|
||||
onChange={linkEvent(this, handlePostNsfwChange)}
|
||||
/>
|
||||
<label className="form-check-label">{i18n.t("nsfw")}</label>
|
||||
</div>
|
||||
)}
|
||||
<LanguageSelect
|
||||
allLanguages={this.props.allLanguages}
|
||||
siteLanguages={this.props.siteLanguages}
|
||||
selectedLanguageIds={selectedLangs}
|
||||
multiple={false}
|
||||
onChange={this.handleLanguageChange}
|
||||
/>
|
||||
<input
|
||||
tabIndex={-1}
|
||||
autoComplete="false"
|
||||
|
@ -413,7 +537,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
className="form-control honeypot"
|
||||
id="register-honey"
|
||||
value={this.state.form.honeypot}
|
||||
onInput={linkEvent(this, this.handleHoneyPotChange)}
|
||||
onInput={linkEvent(this, handleHoneyPotChange)}
|
||||
/>
|
||||
<div className="mb-3 row">
|
||||
<div className="col-sm-10">
|
||||
|
@ -434,7 +558,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={linkEvent(this, this.handleCancel)}
|
||||
onClick={linkEvent(this, handleCancel)}
|
||||
>
|
||||
{i18n.t("cancel")}
|
||||
</button>
|
||||
|
@ -459,7 +583,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
role="button"
|
||||
onClick={linkEvent(
|
||||
{ i: this, suggestedTitle },
|
||||
this.copySuggestedTitle
|
||||
copySuggestedTitle
|
||||
)}
|
||||
>
|
||||
{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() {
|
||||
const url = this.state.form.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() {
|
||||
const q = this.state.form.name;
|
||||
if (q && q !== "") {
|
||||
|
@ -618,84 +674,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
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[]) {
|
||||
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) => {
|
||||
const { selectedCommunityChoice } = this.props;
|
||||
this.setState({ communitySearchLoading: true });
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"noFallthroughCasesInSwitch": true,
|
||||
"paths": {
|
||||
"@/*": ["/*"],
|
||||
"@utils/*": ["shared/utils/*"],
|
||||
"@utils/*": ["shared/utils/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
|
Loading…
Reference in a new issue