]> Untitled Git - lemmy-ui.git/commitdiff
Merge branch 'main' into expand-video-embeds-to-fullwidth
authorDavid Palmer <cloventt@users.noreply.github.com>
Wed, 21 Jun 2023 21:25:50 +0000 (09:25 +1200)
committerGitHub <noreply@github.com>
Wed, 21 Jun 2023 21:25:50 +0000 (09:25 +1200)
22 files changed:
CONTRIBUTING.md
lemmy-translations
package.json
src/server/handlers/catch-all-handler.tsx
src/server/handlers/manifest-handler.ts [new file with mode: 0644]
src/server/index.tsx
src/server/utils/create-ssr-html.tsx
src/server/utils/generate-manifest-json.ts [moved from src/server/utils/generate-manifest-base64.ts with 69% similarity]
src/shared/components/common/emoji-picker.tsx
src/shared/components/common/language-select.tsx
src/shared/components/common/tabs.tsx
src/shared/components/community/communities.tsx
src/shared/components/home/admin-settings.tsx
src/shared/components/home/home.tsx
src/shared/components/home/rate-limit-form.tsx
src/shared/components/home/site-sidebar.tsx
src/shared/components/person/settings.tsx
src/shared/components/post/create-post.tsx
src/shared/components/post/post-form.tsx
src/shared/utils.ts
tsconfig.json
yarn.lock

index 75015d8f5b11d1ffa4359ffc1655a156ecd9e288..e300add34ea61b06b52d6669c1fce6365d1d9717 100644 (file)
@@ -1,3 +1,3 @@
 # 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.
index 7fc71d0860bbe5c6d620ec27112350ffe5b9229c..a241fe1255a6363c7ae1ec5a09520c066745e6ce 160000 (submodule)
@@ -1 +1 @@
-Subproject commit 7fc71d0860bbe5c6d620ec27112350ffe5b9229c
+Subproject commit a241fe1255a6363c7ae1ec5a09520c066745e6ce
index 7b4f46210679a29ffc464c2a6165e67c28b8dc7c..9a285463d74f9f39a89ce27a4c048ebdef2465d7 100644 (file)
@@ -1,6 +1,6 @@
 {
   "name": "lemmy-ui",
-  "version": "0.18.0-rc.3",
+  "version": "0.18.0-rc.4",
   "description": "An isomorphic UI for lemmy",
   "repository": "https://github.com/LemmyNet/lemmy-ui",
   "license": "AGPL-3.0",
     "translations:update": "git submodule update --remote --recursive"
   },
   "lint-staged": {
-    "*.{ts,tsx,js}": [
-      "prettier --write",
-      "eslint --fix"
-    ],
-    "*.{css, scss}": [
-      "prettier --write"
-    ],
-    "package.json": [
-      "sortpack"
-    ]
+    "*.{ts,tsx,js}": ["prettier --write", "eslint --fix"],
+    "*.{css, scss}": ["prettier --write"],
+    "package.json": ["sortpack"]
   },
   "dependencies": {
     "@babel/plugin-proposal-decorators": "^7.21.0",
@@ -66,7 +59,7 @@
     "inferno-server": "^8.1.1",
     "isomorphic-cookie": "^1.2.4",
     "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",
     "markdown-it": "^13.0.1",
     "markdown-it-container": "^3.0.0",
index eb847dc7a2b6944e37f6278450083ef36e0f72d5..025aaa6833c33451c6c5b03c64049c0e91443c7d 100644 (file)
@@ -28,7 +28,9 @@ export default async (req: Request, res: Response) => {
     const getSiteForm: GetSite = { auth };
 
     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;
 
diff --git a/src/server/handlers/manifest-handler.ts b/src/server/handlers/manifest-handler.ts
new file mode 100644 (file)
index 0000000..55c7b64
--- /dev/null
@@ -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);
+};
index f109fc1103b91f595f803c3fdd638a3c21c72b53..144b596ec618f5effa7d2f7f4b4edf60b4309bd9 100644 (file)
@@ -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);
index 5cc38d7dc3df4314d8491f5072ed07a09b168886..2c35aa29a9f676197e7d575ce8a3a8ba52e5f2da 100644 (file)
@@ -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} />
   
similarity index 69%
rename from src/server/utils/generate-manifest-base64.ts
rename to src/server/utils/generate-manifest-json.ts
index e89b15596d99e700461120c49e4d63606f9f412d..b03fd87bdf385102101396f552a5bdc3708efac9 100644 (file)
@@ -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");
 }
index a7fc7d4fe65622a20bf7f02b594d078089655866..6c6037546b4c714269be3e09a5822fbe19c9b417 100644 (file)
@@ -38,16 +38,18 @@ export class EmojiPicker extends Component<EmojiPickerProps, EmojiPickerState> {
 
         {this.state.showPicker && (
           <>
-            <div className="emoji-picker-container">
-              <EmojiMart
-                onEmojiClick={this.handleEmojiClick}
-                pickerOptions={{}}
-              ></EmojiMart>
+            <div className="position-relative">
+              <div className="emoji-picker-container position-absolute w-100">
+                <EmojiMart
+                  onEmojiClick={this.handleEmojiClick}
+                  pickerOptions={{}}
+                ></EmojiMart>
+              </div>
+              <div
+                onClick={linkEvent(this, this.togglePicker)}
+                className="click-away-container"
+              />
             </div>
-            <div
-              onClick={linkEvent(this, this.togglePicker)}
-              className="click-away-container"
-            />
           </>
         )}
       </span>
index 619e1a5a6203adb04e9cf7bdbeef7cb4b68ffd01..02deb434a5ef0ca830342f2846b50b0a9f03ef42 100644 (file)
@@ -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 && (
index 17980a476ce2b6f448a9c56039da29b7cd6b0dcc..3c2726a56727f0e1447d44f8d4ff290aa9f905c8 100644 (file)
@@ -1,8 +1,9 @@
+import classNames from "classnames";
 import { Component, InfernoNode, linkEvent } from "inferno";
 
 interface TabItem {
   key: string;
-  getNode: () => InfernoNode;
+  getNode: (isSelected: boolean) => InfernoNode;
   label: string;
 }
 
@@ -30,24 +31,33 @@ export default class Tabs extends Component<TabsProps, TabsState> {
   render() {
     return (
       <div>
-        <ul className="nav nav-tabs mb-2">
+        <ul className="nav nav-tabs mb-2" role="tablist">
           {this.props.tabs.map(({ key, label }) => (
             <li key={key} className="nav-item">
               <button
                 type="button"
-                className={`nav-link btn${
-                  this.state?.currentTab === key ? " active" : ""
-                }`}
+                className={classNames("nav-link", {
+                  active: this.state?.currentTab === key,
+                })}
                 onClick={linkEvent({ ctx: this, tab: key }, handleSwitchTab)}
+                aria-controls={`${key}-tab-pane`}
+                {...(this.state?.currentTab === key && {
+                  ...{
+                    "aria-current": "page",
+                    "aria-selected": "true",
+                  },
+                })}
               >
                 {label}
               </button>
             </li>
           ))}
         </ul>
-        {this.props.tabs
-          .find(tab => tab.key === this.state?.currentTab)
-          ?.getNode()}
+        <div className="tab-content">
+          {this.props.tabs.map(({ key, getNode }) => {
+            return getNode(this.state?.currentTab === key);
+          })}
+        </div>
       </div>
     );
   }
index 7cf072ef551f590c8753668a8195a7767b01276c..bf8978238bcfba4db8c5e417a65690f28309e3b7 100644 (file)
@@ -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>
index 1de9f8751a2178beb6e59cd061423a8e00750e3f..23454ab9db8934c98fc0e560a01ac0b497d8e4c6 100644 (file)
@@ -1,3 +1,4 @@
+import classNames from "classnames";
 import { Component, linkEvent } from "inferno";
 import {
   BannedPersonsResponse,
@@ -130,22 +131,30 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
             {
               key: "site",
               label: i18n.t("site"),
-              getNode: () => (
-                <div className="row">
-                  <div className="col-12 col-md-6">
-                    <SiteForm
-                      showLocal={showLocal(this.isoData)}
-                      allowedInstances={federationData?.allowed}
-                      blockedInstances={federationData?.blocked}
-                      onSaveSite={this.handleEditSite}
-                      siteRes={this.state.siteRes}
-                      themeList={this.state.themeList}
-                      loading={this.state.loading}
-                    />
-                  </div>
-                  <div className="col-12 col-md-6">
-                    {this.admins()}
-                    {this.bannedUsers()}
+              getNode: isSelected => (
+                <div
+                  className={classNames("tab-pane show", {
+                    active: isSelected,
+                  })}
+                  role="tabpanel"
+                  id="site-tab-pane"
+                >
+                  <div className="row">
+                    <div className="col-12 col-md-6">
+                      <SiteForm
+                        showLocal={showLocal(this.isoData)}
+                        allowedInstances={federationData?.allowed}
+                        blockedInstances={federationData?.blocked}
+                        onSaveSite={this.handleEditSite}
+                        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>
               ),
@@ -153,40 +162,64 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
             {
               key: "rate_limiting",
               label: "Rate Limiting",
-              getNode: () => (
-                <RateLimitForm
-                  rateLimits={
-                    this.state.siteRes.site_view.local_site_rate_limit
-                  }
-                  onSaveSite={this.handleEditSite}
-                  loading={this.state.loading}
-                />
+              getNode: isSelected => (
+                <div
+                  className={classNames("tab-pane", {
+                    active: isSelected,
+                  })}
+                  role="tabpanel"
+                  id="rate_limiting-tab-pane"
+                >
+                  <RateLimitForm
+                    rateLimits={
+                      this.state.siteRes.site_view.local_site_rate_limit
+                    }
+                    onSaveSite={this.handleEditSite}
+                    loading={this.state.loading}
+                  />
+                </div>
               ),
             },
             {
               key: "taglines",
               label: i18n.t("taglines"),
-              getNode: () => (
-                <div className="row">
-                  <TaglineForm
-                    taglines={this.state.siteRes.taglines}
-                    onSaveSite={this.handleEditSite}
-                    loading={this.state.loading}
-                  />
+              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",
               label: i18n.t("emojis"),
-              getNode: () => (
-                <div className="row">
-                  <EmojiForm
-                    onCreate={this.handleCreateEmoji}
-                    onDelete={this.handleDeleteEmoji}
-                    onEdit={this.handleEditEmoji}
-                    loading={this.state.emojiLoading}
-                  />
+              getNode: isSelected => (
+                <div
+                  className={classNames("tab-pane", {
+                    active: isSelected,
+                  })}
+                  role="tabpanel"
+                  id="emojis-tab-pane"
+                >
+                  <div className="row">
+                    <EmojiForm
+                      onCreate={this.handleCreateEmoji}
+                      onDelete={this.handleDeleteEmoji}
+                      onEdit={this.handleEditEmoji}
+                      loading={this.state.emojiLoading}
+                    />
+                  </div>
                 </div>
               ),
             },
index 765dbf0307504854525118c44ac1b66ffbddd5de..4270bd0bee42d40546dbbc4285d8f6da24573eca 100644 (file)
@@ -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>
+        )}
       </>
     );
   }
index 1b2e281b243402bd7802269e7c07a22a379493ce..11c1a8e85b17f936eb148e98d3969823016b9c43 100644 (file)
@@ -1,3 +1,4 @@
+import classNames from "classnames";
 import { Component, FormEventHandler, linkEvent } from "inferno";
 import { EditSite, LocalSiteRateLimit } from "lemmy-js-client";
 import { i18n } from "../../i18next";
@@ -19,6 +20,7 @@ interface RateLimitsProps {
   handleRateLimitPerSecond: FormEventHandler<HTMLInputElement>;
   rateLimitValue?: number;
   rateLimitPerSecondValue?: number;
+  className?: string;
 }
 
 interface RateLimitFormProps {
@@ -49,9 +51,10 @@ function RateLimits({
   handleRateLimitPerSecond,
   rateLimitPerSecondValue,
   rateLimitValue,
+  className,
 }: RateLimitsProps) {
   return (
-    <div className="mb-3 row">
+    <div role="tabpanel" className={classNames("mb-3 row", className)}>
       <div className="col-md-6">
         <label htmlFor="rate-limit">{i18n.t("rate_limit")}</label>
         <input
@@ -142,8 +145,11 @@ export default class RateLimitsForm extends Component<
           tabs={rateLimitTypes.map(rateLimitType => ({
             key: rateLimitType,
             label: i18n.t(`rate_limit_${rateLimitType}`),
-            getNode: () => (
+            getNode: isSelected => (
               <RateLimits
+                className={classNames("tab-pane show", {
+                  active: isSelected,
+                })}
                 handleRateLimit={linkEvent(
                   { rateLimitType, ctx: this },
                   handleRateLimitChange
index 4caeed6b11121834e71672036f6747bb444b16a3..8f8b177e7f62922d8040a4675530fc0c8258720a 100644 (file)
@@ -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>
     );
index 5c3fc345c9433c556a07a9ddd50a80aba47b02f6..9acba57abb95dccaa5a995703737d8b75081c445 100644 (file)
@@ -1,4 +1,5 @@
 import { debounce } from "@utils/helpers";
+import classNames from "classnames";
 import { NoOptionI18nKeys } from "i18next";
 import { Component, linkEvent } from "inferno";
 import {
@@ -265,34 +266,50 @@ export class Settings extends Component<any, SettingsState> {
     );
   }
 
-  userSettings() {
+  userSettings(isSelected) {
     return (
-      <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
+        className={classNames("tab-pane show", {
+          active: isSelected,
+        })}
+        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 className="col-12 col-md-6">
-          <div className="card border-secondary mb-3">
-            <div className="card-body">{this.changePasswordHtmlForm()}</div>
+          <div className="col-12 col-md-6">
+            <div className="card border-secondary mb-3">
+              <div className="card-body">{this.changePasswordHtmlForm()}</div>
+            </div>
           </div>
         </div>
       </div>
     );
   }
 
-  blockCards() {
+  blockCards(isSelected) {
     return (
-      <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
+        className={classNames("tab-pane", {
+          active: isSelected,
+        })}
+        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 className="col-12 col-md-6">
-          <div className="card border-secondary mb-3">
-            <div className="card-body">{this.blockCommunityCard()}</div>
+          <div className="col-12 col-md-6">
+            <div className="card border-secondary mb-3">
+              <div className="card-body">{this.blockCommunityCard()}</div>
+            </div>
           </div>
         </div>
       </div>
index ebdf99954b9505108117d5a1a52511ebfb61fed8..5a9a1673ebfd8e856b58d5a309d0d1d1c9960428 100644 (file)
@@ -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}
index 4b74e07f8e88c77d6ab03041d4e66537bdd530dd..2475d49d982bab5e34885596824fc1f897c441bb 100644 (file)
@@ -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 });
index 8b01e5b48da194448a5e4cb8d784a9d24d1a1ed8..33658d170d4813ca3d013d0fa7e7d0a40ef88574 100644 (file)
@@ -65,10 +65,10 @@ export const favIconPngUrl = "/static/assets/icons/apple-touch-icon.png";
 export const repoUrl = "https://github.com/LemmyNet";
 export const joinLemmyUrl = "https://join-lemmy.org";
 export const donateLemmyUrl = `${joinLemmyUrl}/donate`;
-export const docsUrl = `${joinLemmyUrl}/docs/en/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 markdownHelpUrl = `${joinLemmyUrl}/docs/en/users/02-media.html`;
-export const sortingHelpUrl = `${joinLemmyUrl}/docs/en/users/03-votes-and-ranking.html`;
+export const docsUrl = `${joinLemmyUrl}/docs/index.html`;
+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/users/02-media.html`;
+export const sortingHelpUrl = `${joinLemmyUrl}/docs/users/03-votes-and-ranking.html`;
 export const archiveTodayUrl = "https://archive.today";
 export const ghostArchiveUrl = "https://ghostarchive.org";
 export const webArchiveUrl = "https://web.archive.org";
index a1c4fe5c8f43f258484c257cc4a9d5282aa7130c..aa3115fa48675b1960f54a09f80cd390ec5c62fe 100644 (file)
@@ -21,7 +21,7 @@
     "noFallthroughCasesInSwitch": true,
     "paths": {
       "@/*": ["/*"],
-      "@utils/*": ["shared/utils/*"],
+      "@utils/*": ["shared/utils/*"]
     }
   },
   "include": [
index 8ed829827f2f60e01433554268a5c03568a5684e..f874cb53757fb616e8e0c932a3995e5ca4ead4e6 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -5695,10 +5695,10 @@ leac@^0.6.0:
   resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912"
   integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==
 
-lemmy-js-client@0.18.0-rc.1:
-  version "0.18.0-rc.1"
-  resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.18.0-rc.1.tgz#fd0c88810572d90413696011ebaed19e3b8162d8"
-  integrity sha512-lQe443Nr5UCSoY+IxmT7mBe0IRF6EAZ/4PJSRoPSL+U8A+egMMBPbuxnisHzLsC+eDOWRUIgOqZlwlaRnbmuig==
+lemmy-js-client@0.18.0-rc.2:
+  version "0.18.0-rc.2"
+  resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.18.0-rc.2.tgz#6cd8b4dc95de8f2a6f99de56819c141a394dca04"
+  integrity sha512-bnYs89MjlQHwVIr1YIoAvgFkCTWrXDjSgPbCJx8ijrxZXqOKW/KAgWEisfqyFpy3dYpA3/sxFjh7b4sdxM+8VA==
   dependencies:
     cross-fetch "^3.1.5"
     form-data "^4.0.0"