]> Untitled Git - lemmy-ui.git/commitdiff
Merge branch 'main' of https://github.com/LemmyNet/lemmy-ui
authorDessalines <tyhou13@gmx.com>
Fri, 16 Jun 2023 16:18:15 +0000 (12:18 -0400)
committerDessalines <tyhou13@gmx.com>
Fri, 16 Jun 2023 16:18:15 +0000 (12:18 -0400)
33 files changed:
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
.github/ISSUE_TEMPLATE/QUESTION.yml
.github/ISSUE_TEMPLATE/hexbear.yml
Dockerfile
dev.dockerfile
src/assets/css/main.css
src/server/index.tsx
src/shared/components/app/navbar.tsx
src/shared/components/comment/comment-node.tsx
src/shared/components/comment/comment-nodes.tsx
src/shared/components/common/html-tags.tsx
src/shared/components/common/markdown-textarea.tsx
src/shared/components/common/moment-time.tsx
src/shared/components/community/community-form.tsx
src/shared/components/community/create-community.tsx
src/shared/components/community/sidebar.tsx
src/shared/components/home/admin-settings.tsx
src/shared/components/home/emojis-form.tsx
src/shared/components/home/home.tsx
src/shared/components/home/login.tsx
src/shared/components/home/rate-limit-form.tsx
src/shared/components/home/setup.tsx
src/shared/components/home/signup.tsx
src/shared/components/home/site-form.tsx
src/shared/components/home/tagline-form.tsx
src/shared/components/person/settings.tsx
src/shared/components/post/create-post.tsx
src/shared/components/post/post-form.tsx
src/shared/components/post/post-listing.tsx
src/shared/components/post/post-listings.tsx
src/shared/i18next.ts
src/shared/utils.ts

index 64579090c1b1f27ec212dd285c1ef558815da4f3..ae2d4e51f0a74a9ffb498c83f10a0f8acced7bfc 100644 (file)
@@ -9,6 +9,19 @@ body:
         Found a bug? Please fill out the sections below. 👍
         Thanks for taking the time to fill out this bug report!
         For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
+  - type: checkboxes
+    attributes:
+      label: Requirements
+      description: Before you create a bug report please do the following.
+      options:
+        - label: Is this a bug report? For questions or discussions use https://lemmy.ml/c/lemmy_support
+          required: true
+        - label: Did you check to see if this issue already exists?
+          required: true
+        - label: Is this only a single bug? Do not put multiple bugs in one issue.
+          required: true
+        - label: Is this a server side (not related to the UI) issue? Use the [Lemmy back end](https://github.com/LemmyNet/lemmy) repo.
+          required: false
   - type: textarea
     id: summary
     attributes:
@@ -22,7 +35,7 @@ body:
       label: Steps to Reproduce
       description: |
         Describe the steps to reproduce the bug.
-        The better your description is _(go 'here', click 'there'...)_ the fastest you'll get an _(accurate)_ resolution. 
+        The better your description is _(go 'here', click 'there'...)_ the fastest you'll get an _(accurate)_ resolution.
       value: |
         1.
         2.
@@ -45,3 +58,9 @@ body:
       placeholder: ex. 0.17.4-rc.4
     validations:
       required: true
+  - type: input
+    id: lemmy-instance
+    attributes:
+      label: Lemmy Instance URL
+      description: Which Lemmy instance do you use? The address
+      placeholder: lemmy.ml, lemmy.world, etc
index 375d06d3b81bc42b21f0b6a4df0bef3dc8cc6599..3c75050ab8a964b03a93030ba90821c505ef5de4 100644 (file)
@@ -7,6 +7,19 @@ body:
       value: |
         Have a suggestion about Lemmy's UI?
         For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
+  - type: checkboxes
+    attributes:
+      label: Requirements
+      description: Before you create a bug report please do the following.
+      options:
+        - label: Is this a feature request? For questions or discussions use https://lemmy.ml/c/lemmy_support
+          required: true
+        - label: Did you check to see if this issue already exists?
+          required: true
+        - label: Is this only a feature request? Do not put multiple feature requests in one issue.
+          required: true
+        - label: Is this a server side (not related to the UI) issue? Use the [Lemmy back end](https://github.com/LemmyNet/lemmy) repo.
+          required: false
   - type: textarea
     id: problem
     attributes:
index 460d9a4431140711a560c179e07f8671a903e684..734937e9ac1fbb588a6bf9eb282fd51a31e84f3b 100644 (file)
@@ -14,4 +14,4 @@ body:
       label: Question
       description: What's the question you have about Lemmy's UI?
     validations:
-      required: true
\ No newline at end of file
+      required: true
index 199b97e94da627195e25d588351712ef5ba4d389..73ef5482e100d0035fb0907896af92b5447de916 100644 (file)
@@ -8,4 +8,4 @@ body:
       label: Question
       description: What's the question you have about hexbear?
     validations:
-      required: true
\ No newline at end of file
+      required: true
index 3d6d6212d8dc4401373873cf3d9bb67cf5cfbecb..2b36581d28d6ceebe2a891d35abe15fb9da410a3 100644 (file)
@@ -1,4 +1,4 @@
-FROM node:alpine as builder\r
+FROM node:20.2-alpine as builder\r
 RUN apk update && apk add curl yarn python3 build-base gcc wget git --no-cache\r
 RUN curl -sf https://gobinaries.com/tj/node-prune | sh\r
 \r
index 0e925c0a90dfca99dceb40c7186bc82e79fb757d..3bfc10daed7f14b92853e908e5e16c488f45e1e3 100644 (file)
@@ -1,4 +1,4 @@
-FROM node:alpine as builder\r
+FROM node:20.2-alpine as builder\r
 RUN apk update && apk add curl yarn python3 build-base gcc wget git --no-cache\r
 \r
 WORKDIR /usr/src/app\r
index 1c45341d53d21016b3110b7ea66cf7a54d971cb8..82f8433e89d063fd4c514dc9e5a09664fdbece4d 100644 (file)
   font-size: 1.2rem;
 }
 
-.md-div table {
-  border-collapse: collapse;
-  width: 100%;
-  margin-bottom: 1rem;
-  border: 1px solid var(--dark);
-}
-
-.md-div table th,
-.md-div table td {
-  padding: 0.3rem;
-  vertical-align: top;
-  border-top: 1px solid var(--dark);
-  border: 1px solid var(--dark);
-}
-
-.md-div table thead th {
-  vertical-align: bottom;
-  border-bottom: 2px solid var(--dark);
-}
-
-.md-div table tbody + tbody {
-  border-top: 2px solid var(--dark);
+.md-div pre {
+  white-space: pre;
+  overflow-x: auto;
 }
 
 .vote-bar {
@@ -213,6 +194,11 @@ blockquote {
   overflow-y: auto;
 }
 
+.comments {
+  list-style: none;
+  padding: 0;
+}
+
 .thumbnail {
   object-fit: cover;
   min-height: 60px;
index 43024076ebb74db9d7624a89cf354555d355d3f0..3a12ad7e57e56dd6a34e0919aaad120e04effc65 100644 (file)
@@ -156,7 +156,7 @@ server.get("/*", async (req, res) => {
       site = try_site.data;
       initializeSite(site);
 
-      if (path != "/setup" && !site.site_view.local_site.site_setup) {
+      if (path !== "/setup" && !site.site_view.local_site.site_setup) {
         return res.redirect("/setup");
       }
 
@@ -421,7 +421,7 @@ async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) {
   <!-- Required meta tags -->
   <meta name="Description" content="Lemmy">
   <meta charset="utf-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
   <link
      id="favicon"
      rel="shortcut icon"
index bdbac9ff4818adbb80144599b121ad39296e69b0..6d310eef36d3b10455638700192f4efd3aa630a7 100644 (file)
@@ -16,8 +16,10 @@ import {
   isBrowser,
   myAuth,
   numToSI,
+  poll,
   showAvatars,
   toast,
+  updateUnreadCountsInterval,
 } from "../../utils";
 import { Icon } from "../common/icon";
 import { PictrsImage } from "../common/pictrs-image";
@@ -64,7 +66,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     if (isBrowser()) {
       // On the first load, check the unreads
       this.requestNotificationPermission();
-      await this.fetchUnreads();
+      this.fetchUnreads();
       this.requestNotificationPermission();
 
       document.addEventListener("mouseup", this.handleOutsideMenuClick);
@@ -406,35 +408,36 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     return amAdmin() || moderatesS;
   }
 
-  async fetchUnreads() {
-    const auth = myAuth();
-    if (auth) {
-      this.setState({ unreadInboxCountRes: { state: "loading" } });
-      this.setState({
-        unreadInboxCountRes: await HttpService.client.getUnreadCount({
-          auth,
-        }),
-      });
-
-      if (this.moderatesSomething) {
-        this.setState({ unreadReportCountRes: { state: "loading" } });
-        this.setState({
-          unreadReportCountRes: await HttpService.client.getReportCount({
-            auth,
-          }),
-        });
-      }
-
-      if (amAdmin()) {
-        this.setState({ unreadApplicationCountRes: { state: "loading" } });
-        this.setState({
-          unreadApplicationCountRes:
-            await HttpService.client.getUnreadRegistrationApplicationCount({
+  fetchUnreads() {
+    poll(async () => {
+      if (window.document.visibilityState !== "hidden") {
+        const auth = myAuth();
+        if (auth) {
+          this.setState({
+            unreadInboxCountRes: await HttpService.client.getUnreadCount({
               auth,
             }),
-        });
+          });
+
+          if (this.moderatesSomething) {
+            this.setState({
+              unreadReportCountRes: await HttpService.client.getReportCount({
+                auth,
+              }),
+            });
+          }
+
+          if (amAdmin()) {
+            this.setState({
+              unreadApplicationCountRes:
+                await HttpService.client.getUnreadRegistrationApplicationCount({
+                  auth,
+                }),
+            });
+          }
+        }
       }
-    }
+    }, updateUnreadCountsInterval);
   }
 
   get unreadInboxCount(): number {
index 8559f38baa1355d0809ed3766d34f328a35dc639..0380a72663935e8f119e309d1e5dc159723f3d7c 100644 (file)
@@ -270,9 +270,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
       this.props.moderators
     );
 
-    const borderColor = this.props.node.depth
-      ? colorList[(this.props.node.depth - 1) % colorList.length]
-      : colorList[0];
     const moreRepliesBorderColor = this.props.node.depth
       ? colorList[this.props.node.depth % colorList.length]
       : colorList[0];
@@ -284,26 +281,17 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
       node.comment_view.counts.child_count > 0;
 
     return (
-      <div
-        className={`comment ${
-          this.props.node.depth && !this.props.noIndent ? "ml-1" : ""
-        }`}
-      >
+      <li className="comment" role="comment">
         <div
           id={`comment-${cv.comment.id}`}
           className={classNames(`details comment-node py-2`, {
             "border-top border-light": !this.props.noBorder,
             mark: this.isCommentNew || this.commentView.comment.distinguished,
           })}
-          style={
-            !this.props.noIndent && this.props.node.depth
-              ? `border-left: 2px ${borderColor} solid !important`
-              : ""
-          }
         >
           <div
             className={classNames({
-              "ml-2": !this.props.noIndent && this.props.node.depth,
+              "ml-2": !this.props.noIndent,
             })}
           >
             <div className="d-flex flex-wrap align-items-center text-muted small">
@@ -959,9 +947,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
         </div>
         {showMoreChildren && (
           <div
-            className={`details ml-1 comment-node py-2 ${
-              !this.props.noBorder ? "border-top border-light" : ""
-            }`}
+            className={classNames("details ml-1 comment-node py-2", {
+              "border-top border-light": !this.props.noBorder,
+            })}
             style={`border-left: 2px ${moreRepliesBorderColor} solid !important`}
           >
             <button
@@ -1169,6 +1157,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
             allLanguages={this.props.allLanguages}
             siteLanguages={this.props.siteLanguages}
             hideImages={this.props.hideImages}
+            isChild={!this.props.noIndent}
+            depth={this.props.node.depth + 1}
             finished={this.props.finished}
             onCommentReplyRead={this.props.onCommentReplyRead}
             onPersonMentionRead={this.props.onPersonMentionRead}
@@ -1192,8 +1182,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
           />
         )}
         {/* A collapsed clearfix */}
-        {this.state.collapsed && <div className="row col-12"></div>}
-      </div>
+        {this.state.collapsed && <div className="row col-12" />}
+      </li>
     );
   }
 
@@ -1211,6 +1201,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
 
   linkBtn(small = false) {
     const cv = this.commentView;
+
     const classnames = classNames("btn btn-link btn-animate text-muted", {
       "btn-sm": small,
     });
index 3f9b48ef68eaa0dffa5fb5ded882ce53a25c7557..8c0a236eff78e4b23c2cc137d67990ee27730dad 100644 (file)
@@ -1,3 +1,4 @@
+import classNames from "classnames";
 import { Component } from "inferno";
 import {
   AddAdmin,
@@ -25,6 +26,7 @@ import {
   TransferCommunity,
 } from "lemmy-js-client";
 import { CommentNodeI, CommentViewType } from "../../interfaces";
+import { colorList } from "../../utils";
 import { CommentNode } from "./comment-node";
 
 interface CommentNodesProps {
@@ -44,6 +46,8 @@ interface CommentNodesProps {
   allLanguages: Language[];
   siteLanguages: number[];
   hideImages?: boolean;
+  isChild?: boolean;
+  depth?: number;
   finished: Map<CommentId, boolean | undefined>;
   onSaveComment(form: SaveComment): void;
   onCommentReplyRead(form: MarkCommentReplyAsRead): void;
@@ -74,49 +78,61 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
   render() {
     const maxComments = this.props.maxCommentsShown ?? this.props.nodes.length;
 
+    const borderColor = this.props.depth
+      ? colorList[this.props.depth % colorList.length]
+      : colorList[0];
+
     return (
-      <div className="comments">
-        {this.props.nodes.slice(0, maxComments).map(node => (
-          <CommentNode
-            key={node.comment_view.comment.id}
-            node={node}
-            noBorder={this.props.noBorder}
-            noIndent={this.props.noIndent}
-            viewOnly={this.props.viewOnly}
-            locked={this.props.locked}
-            moderators={this.props.moderators}
-            admins={this.props.admins}
-            markable={this.props.markable}
-            showContext={this.props.showContext}
-            showCommunity={this.props.showCommunity}
-            enableDownvotes={this.props.enableDownvotes}
-            viewType={this.props.viewType}
-            allLanguages={this.props.allLanguages}
-            siteLanguages={this.props.siteLanguages}
-            hideImages={this.props.hideImages}
-            onCommentReplyRead={this.props.onCommentReplyRead}
-            onPersonMentionRead={this.props.onPersonMentionRead}
-            finished={this.props.finished}
-            onCreateComment={this.props.onCreateComment}
-            onEditComment={this.props.onEditComment}
-            onCommentVote={this.props.onCommentVote}
-            onBlockPerson={this.props.onBlockPerson}
-            onSaveComment={this.props.onSaveComment}
-            onDeleteComment={this.props.onDeleteComment}
-            onRemoveComment={this.props.onRemoveComment}
-            onDistinguishComment={this.props.onDistinguishComment}
-            onAddModToCommunity={this.props.onAddModToCommunity}
-            onAddAdmin={this.props.onAddAdmin}
-            onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
-            onBanPerson={this.props.onBanPerson}
-            onTransferCommunity={this.props.onTransferCommunity}
-            onFetchChildren={this.props.onFetchChildren}
-            onCommentReport={this.props.onCommentReport}
-            onPurgePerson={this.props.onPurgePerson}
-            onPurgeComment={this.props.onPurgeComment}
-          />
-        ))}
-      </div>
+      this.props.nodes.length > 0 && (
+        <ul
+          className={classNames("comments", {
+            "ms-1": !!this.props.isChild,
+            "border-top border-light": !this.props.noBorder,
+          })}
+          style={`border-left: 2px solid ${borderColor} !important;`}
+        >
+          {this.props.nodes.slice(0, maxComments).map(node => (
+            <CommentNode
+              key={node.comment_view.comment.id}
+              node={node}
+              noBorder={this.props.noBorder}
+              noIndent={this.props.noIndent}
+              viewOnly={this.props.viewOnly}
+              locked={this.props.locked}
+              moderators={this.props.moderators}
+              admins={this.props.admins}
+              markable={this.props.markable}
+              showContext={this.props.showContext}
+              showCommunity={this.props.showCommunity}
+              enableDownvotes={this.props.enableDownvotes}
+              viewType={this.props.viewType}
+              allLanguages={this.props.allLanguages}
+              siteLanguages={this.props.siteLanguages}
+              hideImages={this.props.hideImages}
+              onCommentReplyRead={this.props.onCommentReplyRead}
+              onPersonMentionRead={this.props.onPersonMentionRead}
+              finished={this.props.finished}
+              onCreateComment={this.props.onCreateComment}
+              onEditComment={this.props.onEditComment}
+              onCommentVote={this.props.onCommentVote}
+              onBlockPerson={this.props.onBlockPerson}
+              onSaveComment={this.props.onSaveComment}
+              onDeleteComment={this.props.onDeleteComment}
+              onRemoveComment={this.props.onRemoveComment}
+              onDistinguishComment={this.props.onDistinguishComment}
+              onAddModToCommunity={this.props.onAddModToCommunity}
+              onAddAdmin={this.props.onAddAdmin}
+              onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
+              onBanPerson={this.props.onBanPerson}
+              onTransferCommunity={this.props.onTransferCommunity}
+              onFetchChildren={this.props.onFetchChildren}
+              onCommentReport={this.props.onCommentReport}
+              onPurgePerson={this.props.onPurgePerson}
+              onPurgeComment={this.props.onPurgeComment}
+            />
+          ))}
+        </ul>
+      )
     );
   }
 }
index 0e6cb2d03ad381528ed3f78447e0c0bc18d2b1bf..f32b0fc048a04687102fc740f08912c7fb0701c3 100644 (file)
@@ -2,7 +2,8 @@ import { htmlToText } from "html-to-text";
 import { Component } from "inferno";
 import { Helmet } from "inferno-helmet";
 import { httpExternalPath } from "../../env";
-import { getLanguages, md } from "../../utils";
+import { i18n } from "../../i18next";
+import { md } from "../../utils";
 
 interface HtmlTagsProps {
   title: string;
@@ -17,11 +18,10 @@ export class HtmlTags extends Component<HtmlTagsProps, any> {
     const url = httpExternalPath(this.props.path);
     const desc = this.props.description;
     const image = this.props.image;
-    const lang = getLanguages()[0];
 
     return (
       <Helmet title={this.props.title}>
-        <html lang={lang == "browser" ? "en" : lang} />
+        <html lang={i18n.resolvedLanguage} />
 
         {["title", "og:title", "twitter:title"].map(t => (
           <meta key={t} property={t} content={this.props.title} />
index 9318d3bb8f972b25b19a48711fcf05aed2ccb8b3..a4459ac0236dfef97de988603172a9f05763cc78 100644 (file)
@@ -184,53 +184,6 @@ export class MarkdownTextArea extends Component<
         </div>
         <div className="row">
           <div className="col-sm-12 d-flex flex-wrap">
-            {this.props.buttonTitle && (
-              <button
-                type="submit"
-                className="btn btn-sm btn-secondary mr-2"
-                disabled={this.isDisabled}
-              >
-                {this.state.loading ? (
-                  <Spinner />
-                ) : (
-                  <span>{this.props.buttonTitle}</span>
-                )}
-              </button>
-            )}
-            {this.props.replyType && (
-              <button
-                type="button"
-                className="btn btn-sm btn-secondary mr-2"
-                onClick={linkEvent(this, this.handleReplyCancel)}
-              >
-                {i18n.t("cancel")}
-              </button>
-            )}
-            {this.state.content && (
-              <button
-                className={`btn btn-sm btn-secondary mr-2 ${
-                  this.state.previewMode && "active"
-                }`}
-                onClick={linkEvent(this, this.handlePreviewToggle)}
-              >
-                {this.state.previewMode ? i18n.t("edit") : i18n.t("preview")}
-              </button>
-            )}
-            {/* A flex expander */}
-            <div className="flex-grow-1"></div>
-
-            {this.props.showLanguage && (
-              <LanguageSelect
-                iconVersion
-                allLanguages={this.props.allLanguages}
-                selectedLanguageIds={
-                  languageId ? Array.of(languageId) : undefined
-                }
-                siteLanguages={this.props.siteLanguages}
-                onChange={this.handleLanguageChange}
-                disabled={this.isDisabled}
-              />
-            )}
             {this.getFormatButton("bold", this.handleInsertBold)}
             {this.getFormatButton("italic", this.handleInsertItalic)}
             {this.getFormatButton("link", this.handleInsertLink)}
@@ -283,6 +236,57 @@ export class MarkdownTextArea extends Component<
               <Icon icon="help-circle" classes="icon-inline" />
             </a>
           </div>
+
+          <div className="col-sm-12 d-flex align-items-center flex-wrap">
+            {this.props.showLanguage && (
+              <LanguageSelect
+                iconVersion
+                allLanguages={this.props.allLanguages}
+                selectedLanguageIds={
+                  languageId ? Array.of(languageId) : undefined
+                }
+                siteLanguages={this.props.siteLanguages}
+                onChange={this.handleLanguageChange}
+                disabled={this.isDisabled}
+              />
+            )}
+
+            {/* A flex expander */}
+            <div className="flex-grow-1"></div>
+
+            {this.props.buttonTitle && (
+              <button
+                type="submit"
+                className="btn btn-sm btn-secondary mr-2"
+                disabled={this.isDisabled}
+              >
+                {this.state.loading ? (
+                  <Spinner />
+                ) : (
+                  <span>{this.props.buttonTitle}</span>
+                )}
+              </button>
+            )}
+            {this.props.replyType && (
+              <button
+                type="button"
+                className="btn btn-sm btn-secondary mr-2"
+                onClick={linkEvent(this, this.handleReplyCancel)}
+              >
+                {i18n.t("cancel")}
+              </button>
+            )}
+            {this.state.content && (
+              <button
+                className={`btn btn-sm btn-secondary mr-2 ${
+                  this.state.previewMode && "active"
+                }`}
+                onClick={linkEvent(this, this.handlePreviewToggle)}
+              >
+                {this.state.previewMode ? i18n.t("edit") : i18n.t("preview")}
+              </button>
+            )}
+          </div>
         </div>
       </form>
     );
index 10714f5bb48fea50e3b18766ac608f3f44fb521b..30c1682c9eaaf6d7980768b35036bfd0e8a060df 100644 (file)
@@ -1,7 +1,7 @@
 import { Component } from "inferno";
 import moment from "moment";
 import { i18n } from "../../i18next";
-import { capitalizeFirstLetter, getLanguages } from "../../utils";
+import { capitalizeFirstLetter } from "../../utils";
 import { Icon } from "./icon";
 
 interface MomentTimeProps {
@@ -15,9 +15,7 @@ export class MomentTime extends Component<MomentTimeProps, any> {
   constructor(props: any, context: any) {
     super(props, context);
 
-    const lang = getLanguages();
-
-    moment.locale(lang);
+    moment.locale([...i18n.languages]);
   }
 
   createdAndModifiedTimes() {
index f317c983b364fde1725c28e5e26a684ebf52679e..4eed4645d461c358f5bb2a84cc4e6f26d87a9aa9 100644 (file)
@@ -21,6 +21,7 @@ interface CommunityFormProps {
   onCancel?(): any;
   onUpsertCommunity(form: CreateCommunity | EditCommunity): void;
   enableNsfw?: boolean;
+  loading?: boolean;
 }
 
 interface CommunityFormState {
@@ -34,7 +35,6 @@ interface CommunityFormState {
     posting_restricted_to_mods?: boolean;
     discussion_languages?: number[];
   };
-  loading: boolean;
   submitted: boolean;
 }
 
@@ -46,7 +46,6 @@ export class CommunityForm extends Component<
 
   state: CommunityFormState = {
     form: {},
-    loading: false,
     submitted: false,
   };
 
@@ -80,7 +79,6 @@ export class CommunityForm extends Component<
           posting_restricted_to_mods: cv.community.posting_restricted_to_mods,
           discussion_languages: this.props.communityLanguages,
         },
-        loading: false,
       };
     }
   }
@@ -90,7 +88,7 @@ export class CommunityForm extends Component<
       <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
         <NavigationPrompt
           when={
-            !this.state.loading &&
+            !this.props.loading &&
             !!(
               this.state.form.name ||
               this.state.form.title ||
@@ -243,9 +241,9 @@ export class CommunityForm extends Component<
             <button
               type="submit"
               className="btn btn-secondary mr-2"
-              disabled={this.state.loading}
+              disabled={this.props.loading}
             >
-              {this.state.loading ? (
+              {this.props.loading ? (
                 <Spinner />
               ) : this.props.community_view ? (
                 capitalizeFirstLetter(i18n.t("save"))
@@ -270,7 +268,7 @@ export class CommunityForm extends Component<
 
   handleCreateCommunitySubmit(i: CommunityForm, event: any) {
     event.preventDefault();
-    i.setState({ loading: true, submitted: true });
+    i.setState({ submitted: true });
     const cForm = i.state.form;
     const auth = myAuthRequired();
 
index f75c4fbb3e7db0fe2a94ccd5845f3a1cf2ee5b33..ff31b839edc9cb1c968ca68bd7dba73cc6b32742 100644 (file)
@@ -11,12 +11,14 @@ import { CommunityForm } from "./community-form";
 
 interface CreateCommunityState {
   siteRes: GetSiteResponse;
+  loading: boolean;
 }
 
 export class CreateCommunity extends Component<any, CreateCommunityState> {
   private isoData = setIsoData(this.context);
   state: CreateCommunityState = {
     siteRes: this.isoData.site_res,
+    loading: false,
   };
   constructor(props: any, context: any) {
     super(props, context);
@@ -45,6 +47,7 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
               allLanguages={this.state.siteRes.all_languages}
               siteLanguages={this.state.siteRes.discussion_languages}
               communityLanguages={this.state.siteRes.discussion_languages}
+              loading={this.state.loading}
             />
           </div>
         </div>
@@ -53,10 +56,15 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
   }
 
   async handleCommunityCreate(form: CreateCommunityI) {
+    this.setState({ loading: true });
+
     const res = await HttpService.client.createCommunity(form);
+
     if (res.state === "success") {
       const name = res.data.community_view.community.name;
       this.props.history.replace(`/c/${name}`);
+    } else {
+      this.setState({ loading: false });
     }
   }
 }
index a5c620f3b4b5704e8763a4d549af8fbd97c877d8..63378a18f4ad8baf1b816be3a5c630f20f387dc7 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, InfernoNode, linkEvent } from "inferno";
+import { T } from "inferno-i18next-dess";
 import { Link } from "inferno-router";
 import {
   AddModToCommunity,
@@ -144,10 +145,15 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
             {myUSerInfo && this.blockCommunity()}
             {!myUSerInfo && (
               <div className="alert alert-info" role="alert">
-                {i18n.t("community_not_logged_in_alert", {
-                  community: name,
-                  instance: hostname(actor_id),
-                })}
+                <T
+                  i18nKey="community_not_logged_in_alert"
+                  interpolation={{
+                    community: name,
+                    instance: hostname(actor_id),
+                  }}
+                >
+                  #<code className="user-select-all">#</code>#
+                </T>
               </div>
             )}
           </div>
index 9b7256d03a507e561f5bf2e92cecc0586b216b60..302e96bdf85e4a3798ee9af1e5c28e8c2305450d 100644 (file)
@@ -39,6 +39,8 @@ interface AdminSettingsState {
   instancesRes: RequestState<GetFederatedInstancesResponse>;
   bannedRes: RequestState<BannedPersonsResponse>;
   leaveAdminTeamRes: RequestState<GetSiteResponse>;
+  emojiLoading: boolean;
+  loading: boolean;
   themeList: string[];
   isIsomorphic: boolean;
 }
@@ -52,6 +54,8 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
     bannedRes: { state: "empty" },
     instancesRes: { state: "empty" },
     leaveAdminTeamRes: { state: "empty" },
+    emojiLoading: false,
+    loading: false,
     themeList: [],
     isIsomorphic: false,
   };
@@ -81,6 +85,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
       bannedRes: { state: "loading" },
       instancesRes: { state: "loading" },
       themeList: [],
+      loading: true,
     });
 
     const auth = myAuthRequired();
@@ -95,6 +100,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
       bannedRes,
       instancesRes,
       themeList,
+      loading: false,
     });
   }
 
@@ -156,6 +162,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
                       onSaveSite={this.handleEditSite}
                       siteRes={this.state.siteRes}
                       themeList={this.state.themeList}
+                      loading={this.state.loading}
                     />
                   </div>
                   <div className="col-12 col-md-6">
@@ -174,6 +181,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
                     this.state.siteRes.site_view.local_site_rate_limit
                   }
                   onSaveSite={this.handleEditSite}
+                  loading={this.state.loading}
                 />
               ),
             },
@@ -185,6 +193,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
                   <TaglineForm
                     taglines={this.state.siteRes.taglines}
                     onSaveSite={this.handleEditSite}
+                    loading={this.state.loading}
                   />
                 </div>
               ),
@@ -198,6 +207,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
                     onCreate={this.handleCreateEmoji}
                     onDelete={this.handleDeleteEmoji}
                     onEdit={this.handleEditEmoji}
+                    loading={this.state.emojiLoading}
                   />
                 </div>
               ),
@@ -266,6 +276,8 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
   }
 
   async handleEditSite(form: EditSite) {
+    this.setState({ loading: true });
+
     const editRes = await HttpService.client.editSite(form);
 
     if (editRes.state === "success") {
@@ -278,6 +290,8 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
       toast(i18n.t("site_saved"));
     }
 
+    this.setState({ loading: false });
+
     return editRes;
   }
 
@@ -300,23 +314,35 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
   }
 
   async handleEditEmoji(form: EditCustomEmoji) {
+    this.setState({ emojiLoading: true });
+
     const res = await HttpService.client.editCustomEmoji(form);
     if (res.state === "success") {
       updateEmojiDataModel(res.data.custom_emoji);
     }
+
+    this.setState({ emojiLoading: false });
   }
 
   async handleDeleteEmoji(form: DeleteCustomEmoji) {
+    this.setState({ emojiLoading: true });
+
     const res = await HttpService.client.deleteCustomEmoji(form);
     if (res.state === "success") {
       removeFromEmojiDataModel(res.data.id);
     }
+
+    this.setState({ emojiLoading: false });
   }
 
   async handleCreateEmoji(form: CreateCustomEmoji) {
+    this.setState({ emojiLoading: true });
+
     const res = await HttpService.client.createCustomEmoji(form);
     if (res.state === "success") {
       updateEmojiDataModel(res.data.custom_emoji);
     }
+
+    this.setState({ emojiLoading: false });
   }
 }
index 171b7c99b2d8f4e71543e14612377d7cb1be1888..f77f51258e14c584ce9cc3192873a5b7ac1bd726 100644 (file)
@@ -23,12 +23,12 @@ interface EmojiFormProps {
   onEdit(form: EditCustomEmoji): void;
   onCreate(form: CreateCustomEmoji): void;
   onDelete(form: DeleteCustomEmoji): void;
+  loading: boolean;
 }
 
 interface EmojiFormState {
   siteRes: GetSiteResponse;
   customEmojis: CustomEmojiViewForm[];
-  loading: boolean;
   page: number;
 }
 
@@ -47,7 +47,6 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
   private isoData = setIsoData(this.context);
   private itemsPerPage = 15;
   private emptyState: EmojiFormState = {
-    loading: false,
     siteRes: this.isoData.site_res,
     customEmojis: this.isoData.site_res.custom_emojis.map((x, index) => ({
       id: x.custom_emoji.id,
@@ -223,7 +222,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
                             data-tippy-content={i18n.t("save")}
                             aria-label={i18n.t("save")}
                             disabled={
-                              this.state.loading ||
+                              this.props.loading ||
                               !this.canEdit(cv) ||
                               !cv.changed
                             }
@@ -243,7 +242,7 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> {
                           )}
                           data-tippy-content={i18n.t("delete")}
                           aria-label={i18n.t("delete")}
-                          disabled={this.state.loading}
+                          disabled={this.props.loading}
                           title={i18n.t("delete")}
                         >
                           <Icon
index 8be983042a5ce584988bb282d928656678ddfeb0..1abb1ee329f391ab52e93e1c33d389e173ddf834 100644 (file)
@@ -244,9 +244,10 @@ export class Home extends Component<any, HomeState> {
   }
 
   async componentDidMount() {
-    if (!this.state.isIsomorphic) {
+    if (!this.state.isIsomorphic || !this.isoData.routeData.length) {
       await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]);
     }
+
     setupTippy();
   }
 
@@ -456,7 +457,7 @@ export class Home extends Component<any, HomeState> {
   }
 
   trendingCommunities(isMobile = false) {
-    switch (this.state.trendingCommunitiesRes.state) {
+    switch (this.state.trendingCommunitiesRes?.state) {
       case "loading":
         return (
           <h5>
@@ -573,7 +574,7 @@ export class Home extends Component<any, HomeState> {
     const siteRes = this.state.siteRes;
 
     if (dataType === DataType.Post) {
-      switch (this.state.postsRes.state) {
+      switch (this.state.postsRes?.state) {
         case "loading":
           return (
             <h5>
index 87ef234e543ca3461012c05438da3bc8af8241b7..381c13bb0ce05dac89f5b3068009f5c59940c0d7 100644 (file)
@@ -186,7 +186,9 @@ export class Login extends Component<any, State> {
             UserService.Instance.myUserInfo = site.data.my_user;
           }
 
-          i.props.history.replace("/");
+          i.props.history.action === "PUSH"
+            ? i.props.history.back()
+            : i.props.history.replace("/");
 
           break;
         }
@@ -195,7 +197,7 @@ export class Login extends Component<any, State> {
   }
 
   handleLoginUsernameChange(i: Login, event: any) {
-    i.state.form.username_or_email = event.target.value;
+    i.state.form.username_or_email = event.target.value.trim();
     i.setState(i.state);
   }
 
index 74ed18e32c81bd19ab67325997561950cb3fd8f3..0ce01260088f3b616e56cfb3a5700348f7889367 100644 (file)
@@ -24,6 +24,7 @@ interface RateLimitsProps {
 interface RateLimitFormProps {
   rateLimits: LocalSiteRateLimit;
   onSaveSite(form: EditSite): void;
+  loading: boolean;
 }
 
 interface RateLimitFormState {
@@ -41,7 +42,6 @@ interface RateLimitFormState {
     register?: number;
     register_per_second?: number;
   };
-  loading: boolean;
 }
 
 function RateLimits({
@@ -117,7 +117,6 @@ function submitRateLimitForm(i: RateLimitsForm, event: any) {
     }
   );
 
-  i.setState({ loading: true });
   i.props.onSaveSite(form);
 }
 
@@ -126,7 +125,6 @@ export default class RateLimitsForm extends Component<
   RateLimitFormState
 > {
   state: RateLimitFormState = {
-    loading: false,
     form: this.props.rateLimits,
   };
   constructor(props: RateLimitFormProps, context: any) {
@@ -164,9 +162,9 @@ export default class RateLimitsForm extends Component<
             <button
               type="submit"
               className="btn btn-secondary mr-2"
-              disabled={this.state.loading}
+              disabled={this.props.loading}
             >
-              {this.state.loading ? (
+              {this.props.loading ? (
                 <Spinner />
               ) : (
                 capitalizeFirstLetter(i18n.t("save"))
index 581c1c563db33eb685c48b70f4d2c3c9babbf6db..b658bd2432cc1703346b4150f31ce925f019d3b5 100644 (file)
@@ -73,6 +73,7 @@ export class Setup extends Component<any, State> {
                 onSaveSite={this.handleCreateSite}
                 siteRes={this.state.siteRes}
                 themeList={this.state.themeList}
+                loading={false}
               />
             )}
           </div>
@@ -221,7 +222,7 @@ export class Setup extends Component<any, State> {
   }
 
   handleRegisterUsernameChange(i: Setup, event: any) {
-    i.state.form.username = event.target.value;
+    i.state.form.username = event.target.value.trim();
     i.setState(i.state);
   }
 
index 3efeac6208e104b99b3cf123dbc1afeceaa495a0..16a3cc6d3eef61dbb4624a6acdefaf90429b3009 100644 (file)
@@ -496,7 +496,7 @@ export class Signup extends Component<any, State> {
   }
 
   handleRegisterUsernameChange(i: Signup, event: any) {
-    i.state.form.username = event.target.value;
+    i.state.form.username = event.target.value.trim();
     i.setState(i.state);
   }
 
index 3b451e66ab10bd04493faea8b1ece5e4deda08e7..8b56808e4c53d2bdf748af736d04b9f76f4f36c7 100644 (file)
@@ -12,7 +12,11 @@ import {
   ListingType,
 } from "lemmy-js-client";
 import { i18n } from "../../i18next";
-import { capitalizeFirstLetter, myAuthRequired } from "../../utils";
+import {
+  capitalizeFirstLetter,
+  myAuthRequired,
+  validInstanceTLD,
+} from "../../utils";
 import { Icon, Spinner } from "../common/icon";
 import { ImageUploadForm } from "../common/image-upload-form";
 import { LanguageSelect } from "../common/language-select";
@@ -27,11 +31,11 @@ interface SiteFormProps {
   themeList?: string[];
   onSaveSite(form: EditSite): void;
   siteRes: GetSiteResponse;
+  loading: boolean;
 }
 
 interface SiteFormState {
   siteForm: EditSite;
-  loading: boolean;
   instance_select: {
     allowed_instances: string;
     blocked_instances: string;
@@ -44,7 +48,6 @@ type InstanceKey = "allowed_instances" | "blocked_instances";
 export class SiteForm extends Component<SiteFormProps, SiteFormState> {
   state: SiteFormState = {
     siteForm: this.initSiteForm(),
-    loading: false,
     instance_select: {
       allowed_instances: "",
       blocked_instances: "",
@@ -107,23 +110,21 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
 
     this.handleDiscussionLanguageChange =
       this.handleDiscussionLanguageChange.bind(this);
+
     this.handleAddInstance = this.handleAddInstance.bind(this);
+    this.handleRemoveInstance = this.handleRemoveInstance.bind(this);
+
     this.handleInstanceEnterPress = this.handleInstanceEnterPress.bind(this);
     this.handleInstanceTextChange = this.handleInstanceTextChange.bind(this);
   }
 
-  // Necessary to stop the loading
-  componentWillReceiveProps() {
-    this.setState({ loading: false });
-  }
-
   render() {
     const siteSetup = this.props.siteRes.site_view.local_site.site_setup;
     return (
       <form onSubmit={linkEvent(this, this.handleSaveSiteSubmit)}>
         <NavigationPrompt
           when={
-            !this.state.loading &&
+            !this.props.loading &&
             !siteSetup &&
             !!(
               this.state.siteForm.name ||
@@ -136,8 +137,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
         />
         <h5>{`${
           siteSetup
-            ? capitalizeFirstLetter(i18n.t("save"))
-            : capitalizeFirstLetter(i18n.t("name"))
+            ? capitalizeFirstLetter(i18n.t("edit"))
+            : capitalizeFirstLetter(i18n.t("setup"))
         } ${i18n.t("your_site")}`}</h5>
         <div className="form-group row">
           <label className="col-12 col-form-label" htmlFor="create-site-name">
@@ -157,7 +158,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
           </div>
         </div>
         <div className="form-group">
-          <label>{i18n.t("icon")}</label>
+          <label className="mr-2">{i18n.t("icon")}</label>
           <ImageUploadForm
             uploadTitle={i18n.t("upload_icon")}
             imageSrc={this.state.siteForm.icon}
@@ -167,7 +168,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
           />
         </div>
         <div className="form-group">
-          <label>{i18n.t("banner")}</label>
+          <label className="mr-2">{i18n.t("banner")}</label>
           <ImageUploadForm
             uploadTitle={i18n.t("upload_banner")}
             imageSrc={this.state.siteForm.banner}
@@ -609,9 +610,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
             <button
               type="submit"
               className="btn btn-secondary mr-2"
-              disabled={this.state.loading}
+              disabled={this.props.loading}
             >
-              {this.state.loading ? (
+              {this.props.loading ? (
                 <Spinner />
               ) : siteSetup ? (
                 capitalizeFirstLetter(i18n.t("save"))
@@ -717,7 +718,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
     event.preventDefault();
     const auth = myAuthRequired();
     i.setState(s => ((s.siteForm.auth = auth), s));
-    i.setState({ loading: true, submitted: true });
+    i.setState({ submitted: true });
 
     const stateSiteForm = i.state.siteForm;
 
@@ -780,6 +781,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
 
   handleAddInstance(key: InstanceKey) {
     const instance = this.state.instance_select[key].trim();
+
+    if (!validInstanceTLD(instance)) {
+      return;
+    }
+
     if (!this.state.siteForm[key]?.includes(instance)) {
       this.setState(s => ({
         ...s,
index 44ca4fc02f4c1172689da10913e6d39f0a3182ab..59e8dec4a2e7b2e7b99c710a830edf43963780f1 100644 (file)
@@ -9,17 +9,16 @@ import { MarkdownTextArea } from "../common/markdown-textarea";
 interface TaglineFormProps {
   taglines: Array<Tagline>;
   onSaveSite(form: EditSite): void;
+  loading: boolean;
 }
 
 interface TaglineFormState {
   taglines: Array<string>;
-  loading: boolean;
   editingRow?: number;
 }
 
 export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
   state: TaglineFormState = {
-    loading: false,
     editingRow: undefined,
     taglines: this.props.taglines.map(x => x.content),
   };
@@ -30,10 +29,6 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
     return i18n.t("taglines");
   }
 
-  componentWillReceiveProps() {
-    this.setState({ loading: false });
-  }
-
   render() {
     return (
       <div className="col-12">
@@ -110,9 +105,9 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
               <button
                 onClick={linkEvent(this, this.handleSaveClick)}
                 className="btn btn-secondary mr-2"
-                disabled={this.state.loading}
+                disabled={this.props.loading}
               >
-                {this.state.loading ? (
+                {this.props.loading ? (
                   <Spinner />
                 ) : (
                   capitalizeFirstLetter(i18n.t("save"))
@@ -153,7 +148,6 @@ export class TaglineForm extends Component<TaglineFormProps, TaglineFormState> {
   }
 
   async handleSaveClick(i: TaglineForm) {
-    i.setState({ loading: true });
     i.props.onSaveSite({
       taglines: i.state.taglines,
       auth: myAuthRequired(),
index a29f61b008102ba4d60e3ab0d4988bd13e8f34d4..56d57a7a56850778c7e1a65923d36b4b38e2e0e9 100644 (file)
@@ -25,7 +25,6 @@ import {
   fetchCommunities,
   fetchThemeList,
   fetchUsers,
-  getLanguages,
   myAuth,
   myAuthRequired,
   personToChoice,
@@ -1058,12 +1057,12 @@ export class Settings extends Component<any, SettingsState> {
   }
 
   handleInterfaceLangChange(i: Settings, event: any) {
+    const newLang = event.target.value ?? "browser";
+    i18n.changeLanguage(newLang === "browser" ? navigator.languages : newLang);
+
     i.setState(
       s => ((s.saveUserSettingsForm.interface_language = event.target.value), s)
     );
-    i18n.changeLanguage(
-      getLanguages(i.state.saveUserSettingsForm.interface_language).at(0)
-    );
   }
 
   handleDiscussionLanguageChange(val: number[]) {
index 71fac79aed8668bc9f694df463ee884628eb8419..c7597917740486cd58eae285e54232887737b893 100644 (file)
@@ -224,6 +224,10 @@ export class CreatePost extends Component<
     if (res.state === "success") {
       const postId = res.data.post_view.post.id;
       this.props.history.replace(`/post/${postId}`);
+    } else {
+      this.setState({
+        loading: false,
+      });
     }
   }
 
index 3ce96bb000be784b9e5bbf4083cd002a5800c648..4640922d022e4b0e214421fc1c12b4fa8113ee3b 100644 (file)
@@ -25,7 +25,6 @@ import {
   isImage,
   myAuth,
   myAuthRequired,
-  pictrsDeleteToast,
   relTags,
   setupTippy,
   toast,
@@ -73,6 +72,7 @@ interface PostFormState {
   suggestedPostsRes: RequestState<SearchResponse>;
   metadataRes: RequestState<GetSiteMetadataResponse>;
   imageLoading: boolean;
+  imageDeleteUrl: string;
   communitySearchLoading: boolean;
   communitySearchOptions: Choice[];
   previewMode: boolean;
@@ -86,6 +86,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     form: {},
     loading: false,
     imageLoading: false,
+    imageDeleteUrl: "",
     communitySearchLoading: false,
     previewMode: false,
     communitySearchOptions: [],
@@ -269,6 +270,17 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
             {url && isImage(url) && (
               <img src={url} className="img-fluid" alt="" />
             )}
+            {this.state.imageDeleteUrl && (
+              <button
+                className="btn btn-danger btn-sm mt-2"
+                onClick={linkEvent(this, this.handleImageDelete)}
+                aria-label={i18n.t("delete")}
+                data-tippy-content={i18n.t("delete")}
+              >
+                <Icon icon="x" classes="icon-inline mr-1" />
+                {capitalizeFirstLetter(i18n.t("delete"))}
+              </button>
+            )}
             {this.props.crossPosts && this.props.crossPosts.length > 0 && (
               <>
                 <div className="my-1 text-muted small font-weight-bold">
@@ -553,7 +565,15 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
   }
 
   handlePostUrlChange(i: PostForm, event: any) {
-    i.setState(s => ((s.form.url = event.target.value), s));
+    const url = event.target.value;
+
+    i.setState({
+      form: {
+        url,
+      },
+      imageDeleteUrl: "",
+    });
+
     i.fetchPageTitle();
   }
 
@@ -644,18 +664,35 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       if (res.state === "success") {
         if (res.data.msg === "ok") {
           i.state.form.url = res.data.url;
-          pictrsDeleteToast(file.name, res.data.delete_url as string);
-          i.setState({ imageLoading: false });
+          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 f1f06c5869f123641f84edb82c449da788170702..60e188a33b77c942ba5e9f33cdceb29a8e07e6bb 100644 (file)
@@ -835,6 +835,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           search: "",
         }}
         title={i18n.t("cross_post")}
+        data-tippy-content={i18n.t("cross_post")}
+        aria-label={i18n.t("cross_post")}
       >
         <Icon icon="copy" inline />
       </Link>
@@ -1380,9 +1382,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   showMobilePreview() {
-    const body = this.postView.post.body;
+    const { body, id } = this.postView.post;
+
     return !this.showBody && body ? (
-      <div className="md-div mb-1 preview-lines">{body}</div>
+      <Link className="text-body" to={`/post/${id}`}>
+        <div className="md-div mb-1 preview-lines">{body}</div>
+      </Link>
     ) : (
       <></>
     );
index 098a015d1beecf9cdfcf0f25e2eed27092955450..00370f039bfea8b104d206e1fcf497e27e400501 100644 (file)
@@ -68,7 +68,7 @@ export class PostListings extends Component<PostListingsProps, any> {
     return (
       <div>
         {this.posts.length > 0 ? (
-          this.posts.map(post_view => (
+          this.posts.map((post_view, idx) => (
             <>
               <PostListing
                 post_view={post_view}
@@ -96,7 +96,9 @@ export class PostListings extends Component<PostListingsProps, any> {
                 onAddAdmin={this.props.onAddAdmin}
                 onTransferCommunity={this.props.onTransferCommunity}
               />
-              <hr className="my-3" />
+              {idx + 1 !== this.posts.length && (
+                <hr className="my-3 border border-primary" />
+              )}
             </>
           ))
         ) : (
index eaedbbf817b4fada361b862b4c7d7c33c26b3083..47ca6501542b536b0a2eead58434f2c1f59eda4f 100644 (file)
@@ -1,4 +1,5 @@
 import i18next, { i18nTyped, Resource } from "i18next";
+import { UserService } from "./services";
 import { ar } from "./translations/ar";
 import { bg } from "./translations/bg";
 import { ca } from "./translations/ca";
@@ -30,7 +31,7 @@ import { sv } from "./translations/sv";
 import { vi } from "./translations/vi";
 import { zh } from "./translations/zh";
 import { zh_Hant } from "./translations/zh_Hant";
-import { getLanguages } from "./utils";
+import { isBrowser } from "./utils";
 
 export const languages = [
   { resource: ar, code: "ar", name: "العربية" },
@@ -73,12 +74,31 @@ function format(value: any, format: any): any {
   return format === "uppercase" ? value.toUpperCase() : value;
 }
 
-i18next.init({
+class LanguageDetector {
+  static readonly type = "languageDetector";
+
+  detect() {
+    const langs: string[] = [];
+
+    const myLang =
+      UserService.Instance.myUserInfo?.local_user_view.local_user
+        .interface_language ?? "browser";
+
+    if (myLang !== "browser") langs.push(myLang);
+
+    if (isBrowser()) langs.push(...navigator.languages);
+
+    return langs;
+  }
+}
+
+i18next.use(LanguageDetector).init({
   debug: false,
   compatibilityJSON: "v3",
+  supportedLngs: languages.map(l => l.code),
+  nonExplicitSupportedLngs: true,
   // load: 'languageOnly',
   // initImmediate: false,
-  lng: getLanguages()[0],
   fallbackLng: "en",
   resources,
   interpolation: { format },
index 46e8601be08e5ff4895d6b9335f24fe765cb7168..df7673a47b6a1e8e860a41b764bf70c58745b464 100644 (file)
@@ -42,7 +42,7 @@ import moment from "moment";
 import tippy from "tippy.js";
 import Toastify from "toastify-js";
 import { getHttpBase } from "./env";
-import { i18n, languages } from "./i18next";
+import { i18n } from "./i18next";
 import { CommentNodeI, DataType, IsoData, VoteType } from "./interfaces";
 import { HttpService, UserService } from "./services";
 
@@ -75,6 +75,7 @@ export const commentTreeMaxDepth = 8;
 export const markdownFieldCharacterLimit = 50000;
 export const maxUploadImages = 20;
 export const concurrentImageUpload = 4;
+export const updateUnreadCountsInterval = 30000;
 
 export const relTags = "noopener nofollow";
 
@@ -315,6 +316,7 @@ export function amTopMod(
 
 const imageRegex = /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/;
 const videoRegex = /(http)?s?:?(\/\/[^"']*\.(?:mp4|webm))/;
+const tldRegex = /([a-z0-9]+\.)*[a-z0-9]+\.[a-z]+/;
 
 export function isImage(url: string) {
   return imageRegex.test(url);
@@ -328,6 +330,10 @@ export function validURL(str: string) {
   return !!new URL(str);
 }
 
+export function validInstanceTLD(str: string) {
+  return tldRegex.test(str);
+}
+
 export function communityRSSUrl(actorId: string, sort: string): string {
   const url = new URL(actorId);
   return `${url.origin}/feeds${url.pathname}.xml?sort=${sort}`;
@@ -398,31 +404,6 @@ export function debounce<T extends any[], R>(
   } as (...e: T) => R;
 }
 
-export function getLanguages(
-  override?: string,
-  myUserInfo = UserService.Instance.myUserInfo
-): string[] {
-  const myLang = myUserInfo?.local_user_view.local_user.interface_language;
-  const lang = override || myLang || "browser";
-
-  if (lang == "browser" && isBrowser()) {
-    return getBrowserLanguages();
-  } else {
-    return [lang];
-  }
-}
-
-function getBrowserLanguages(): string[] {
-  // Intersect lemmy's langs, with the browser langs
-  const langs = languages ? languages.map(l => l.code) : ["en"];
-
-  // NOTE, mobile browsers seem to be missing this list, so append en
-  const allowedLangs = navigator.languages
-    .concat("en")
-    .filter(v => langs.includes(v));
-  return allowedLangs;
-}
-
 export async function fetchThemeList(): Promise<string[]> {
   return fetch("/css/themelist").then(res => res.json());
 }
@@ -738,7 +719,7 @@ function setupMarkdown() {
       defs: emojiDefs,
     })
     .disable("image");
-  var defaultRenderer = md.renderer.rules.image;
+  const defaultRenderer = md.renderer.rules.image;
   md.renderer.rules.image = function (
     tokens: Token[],
     idx: number,
@@ -757,6 +738,9 @@ function setupMarkdown() {
     const alt_text = item.content;
     return `<img class="icon icon-emoji" src="${src}" title="${title}" alt="${alt_text}"/>`;
   };
+  md.renderer.rules.table_open = function () {
+    return '<table class="table">';
+  };
 }
 
 export function getEmojiMart(
@@ -1127,7 +1111,7 @@ export const colorList: string[] = [
 ];
 
 function hsl(num: number) {
-  return `hsla(${num}, 35%, 50%, 1)`;
+  return `hsla(${num}, 35%, 50%, 0.5)`;
 }
 
 export function hostname(url: string): string {
@@ -1272,7 +1256,7 @@ export function personSelectName({
 
 export function initializeSite(site?: GetSiteResponse) {
   UserService.Instance.myUserInfo = site?.my_user;
-  i18n.changeLanguage(getLanguages()[0]);
+  i18n.changeLanguage();
   if (site) {
     setupEmojiDataModel(site.custom_emojis ?? []);
   }
@@ -1490,3 +1474,18 @@ export function newVote(voteType: VoteType, myVote?: number): number {
     return myVote == -1 ? 0 : -1;
   }
 }
+
+function sleep(millis: number): Promise<void> {
+  return new Promise(resolve => setTimeout(resolve, millis));
+}
+
+/**
+ * Polls / repeatedly runs a promise, every X milliseconds
+ */
+export async function poll(promiseFn: any, millis: number) {
+  if (window.document.visibilityState !== "hidden") {
+    await promiseFn();
+  }
+  await sleep(millis);
+  return poll(promiseFn, millis);
+}