]> Untitled Git - lemmy-ui.git/commitdiff
Improvements to post listings (#536)
authoreduardog3000 <eduardog3000@gmail.com>
Wed, 12 Jan 2022 00:53:23 +0000 (19:53 -0500)
committerGitHub <noreply@github.com>
Wed, 12 Jan 2022 00:53:23 +0000 (19:53 -0500)
* Improvements to post listings.

* Expanded image and body now show below the rest of the listing.
* The above are also offset to not be under the votes and thumbnail.
* Post title doesn't jump to a new line when you expand the image.
* Thumbnail doesn't disappear when you expand the image.
* Does not apply on mobile since things don't jump around as much.
* Clicking expanded image opens the original in a new tab.
* Does not apply on mobile since you can just long press.
* Instead tapping the image closes it.
* pictrs images now prefer the original format over jpg.
* Rendering split into many functions to improve readability.
* Post actions are now on the same line as the comments button.
* Post actions now show on mobile.
* Comments button made larger.
* Expanding or contracting an image now expands or contracts the body.

* Regenerated yarn.lock

To avoid problems resulting from malicous changes by the dev of `colors`

package.json
src/assets/css/main.css
src/shared/components/common/icon.tsx
src/shared/components/common/pictrs-image.tsx
src/shared/components/post/metadata-card.tsx
src/shared/components/post/post-listing.tsx
src/shared/utils.ts
yarn.lock

index 26e0b95cb035f4f7be6467117e1e46ce0985358c..db244081d61d4e0331c277a5b144c1b9ba438e00 100644 (file)
@@ -21,6 +21,7 @@
     "autosize": "^5.0.1",
     "check-password-strength": "^2.0.3",
     "choices.js": "^10.0.0",
+    "classnames": "^2.3.1",
     "emoji-short-name": "^1.0.0",
     "express": "~4.17.1",
     "i18next": "^21.5.4",
index 637bce04e664d768404ed20b464c985be421fc36..b9fe4b98cafbeeb10775a59614008b1b1609d3b0 100644 (file)
@@ -370,3 +370,7 @@ br.big {
 .honeypot {
   display:none !important;
 }
+
+.slight-radius {
+  border-radius: 4px;
+}
index 3d103c7cc94c2d6af4dfee0209b6eeaa2d9b22f2..5de9a3d1582214034dd1dab961f73e0259c9f644 100644 (file)
@@ -1,8 +1,11 @@
+import classNames from "classnames";
 import { Component } from "inferno";
 
 interface IconProps {
   icon: string;
   classes?: string;
+  inline?: boolean;
+  small?: boolean;
 }
 
 export class Icon extends Component<IconProps, any> {
@@ -12,7 +15,12 @@ export class Icon extends Component<IconProps, any> {
 
   render() {
     return (
-      <svg class={`icon ${this.props.classes}`}>
+      <svg
+        class={classNames("icon", this.props.classes, {
+          "icon-inline": this.props.inline,
+          small: this.props.small,
+        })}
+      >
         <use xlinkHref={`#icon-${this.props.icon}`}></use>
         <div class="sr-only">
           <title>{this.props.icon}</title>
index 7de304bf1930f7bc2bbe1c30f5503f3a6a7a04b6..31e3052f9dd3727a8f3d61ede287c5e896353d61 100644 (file)
@@ -1,8 +1,8 @@
+import classNames from "classnames";
 import { Component } from "inferno";
 
 const iconThumbnailSize = 96;
 const thumbnailSize = 256;
-const maxImageSize = 3000;
 
 interface PictrsImageProps {
   src: string;
@@ -24,24 +24,23 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
     return (
       <picture>
         <source srcSet={this.src("webp")} type="image/webp" />
+        <source srcSet={this.props.src} />
         <source srcSet={this.src("jpg")} type="image/jpeg" />
         <img
-          src={this.src("jpg")}
+          src={this.props.src}
           alt={this.alt()}
           loading="lazy"
-          className={`
-        ${!this.props.icon && !this.props.iconOverlay && "img-fluid "}
-        ${this.props.banner && "banner "}
-        ${
-          this.props.thumbnail && !this.props.icon && !this.props.banner
-            ? "thumbnail rounded "
-            : "img-expanded "
-        }
-        ${this.props.thumbnail && this.props.nsfw && "img-blur "}
-        ${this.props.icon && "rounded-circle img-icon mr-2 "}
-        ${this.props.iconOverlay && "ml-2 mb-0 rounded-circle avatar-overlay "}
-        ${this.props.pushup && "avatar-pushup "}
-        `}
+          className={classNames({
+            "img-fluid": !this.props.icon && !this.props.iconOverlay,
+            "banner": this.props.banner,
+            "thumbnail rounded": this.props.thumbnail && !this.props.icon && !this.props.banner,
+            "img-expanded slight-radius":
+              !this.props.thumbnail && !this.props.icon,
+            "img-blur": this.props.thumbnail && this.props.nsfw,
+            "rounded-circle img-icon mr-2": this.props.icon,
+            "ml-2 mb-0 rounded-circle avatar-overlay": this.props.iconOverlay,
+            "avatar-pushup": this.props.pushup,
+          })}
         />
       </picture>
     );
@@ -67,12 +66,10 @@ export class PictrsImage extends Component<PictrsImageProps, any> {
       params["thumbnail"] = thumbnailSize;
     } else if (this.props.icon) {
       params["thumbnail"] = iconThumbnailSize;
-    } else {
-      params["thumbnail"] = maxImageSize;
     }
 
-    let paramsStr = `?${new URLSearchParams(params).toString()}`;
-    let out = `${host}/pictrs/image/${path}${paramsStr}`;
+    let paramsStr = new URLSearchParams(params).toString();
+    let out = `${host}/pictrs/image/${path}?${paramsStr}`;
 
     return out;
   }
index 85fcdb672556f07e91c611de3ad2d28845c49e0c..f529e3ef1d2518e082b88abc640aa1e79772f4c7 100644 (file)
@@ -29,7 +29,7 @@ export class MetadataCard extends Component<
     return (
       <>
         {post.embed_title && !this.state.expanded && (
-          <div class="card border-secondary mt-3 mb-2">
+          <div class="card border-secondary mt-3 mb-2 offset-sm-3">
             <div class="row">
               <div class="col-12">
                 <div class="card-body">
index 5881b33b4be7cb4e23801a1e11f23b4172993446..a2b3469e18b88376ee2805924e1c75e3ed263a75 100644 (file)
@@ -1,3 +1,4 @@
+import classNames from "classnames";
 import { Component, linkEvent } from "inferno";
 import { Link } from "inferno-router";
 import {
@@ -130,12 +131,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   render() {
+    let post = this.props.post_view.post;
     return (
-      <div class="">
+      <div class="post-listing">
         {!this.state.showEdit ? (
           <>
             {this.listing()}
-            {this.body()}
+            {this.state.imageExpanded && this.img}
+            {post.url && this.showBody && post.embed_title && (
+              <MetadataCard post={post} />
+            )}
+            {this.showBody && post.body && this.body()}
           </>
         ) : (
           <div class="col-12">
@@ -155,26 +161,39 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   body() {
     let post = this.props.post_view.post;
     return (
-      <div class="row">
-        <div class="col-12">
-          {post.url && this.showBody && post.embed_title && (
-            <MetadataCard post={post} />
-          )}
-          {this.showBody &&
-            post.body &&
-            (this.state.viewSource ? (
-              <pre>{post.body}</pre>
-            ) : (
-              <div
-                className="md-div"
-                dangerouslySetInnerHTML={mdToHtml(post.body)}
-              />
-            ))}
-        </div>
+      <div class="col-12 col-sm-9 offset-sm-3 card my-2 p-2">
+        {this.state.viewSource ? (
+          <pre>{post.body}</pre>
+        ) : (
+          <div
+            className="md-div"
+            dangerouslySetInnerHTML={mdToHtml(post.body)}
+          />
+        )}
       </div>
     );
   }
 
+  get img() {
+    return (
+      <>
+        <div class="offset-sm-3 my-2 d-none d-sm-block">
+          <a href={this.imageSrc} class="d-inline-block">
+            <PictrsImage src={this.imageSrc} />
+          </a>
+        </div>
+        <div className="my-2 d-block d-sm-none">
+          <a
+            class="d-inline-block"
+            onClick={linkEvent(this, this.handleImageExpandClick)}
+          >
+            <PictrsImage src={this.imageSrc} />
+          </a>
+        </div>
+      </>
+    );
+  }
+
   imgThumb(src: string) {
     let post_view = this.props.post_view;
     return (
@@ -187,7 +206,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     );
   }
 
-  getImageSrc(): string {
+  get imageSrc(): string {
     let post = this.props.post_view.post;
     if (isImage(post.url)) {
       if (post.url.includes("pictrs")) {
@@ -210,25 +229,25 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     if (isImage(post.url)) {
       return (
         <a
-          href={this.getImageSrc()}
-          class="float-right text-body d-inline-block position-relative mb-2"
+          href={this.imageSrc}
+          class="text-body d-inline-block position-relative mb-2"
           data-tippy-content={i18n.t("expand_here")}
           onClick={linkEvent(this, this.handleImageExpandClick)}
           aria-label={i18n.t("expand_here")}
         >
-          {this.imgThumb(this.getImageSrc())}
+          {this.imgThumb(this.imageSrc)}
           <Icon icon="image" classes="mini-overlay" />
         </a>
       );
     } else if (post.thumbnail_url) {
       return (
         <a
-          class="float-right text-body d-inline-block position-relative mb-2"
+          class="text-body d-inline-block position-relative mb-2"
           href={post.url}
           rel="noopener"
           title={post.url}
         >
-          {this.imgThumb(this.getImageSrc())}
+          {this.imgThumb(this.imageSrc)}
           <Icon icon="external-link" classes="mini-overlay" />
         </a>
       );
@@ -283,10 +302,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         <li className="list-inline-item">
           <PersonListing person={post_view.creator} />
 
-          {this.isMod && (
+          {this.creatorIsMod && (
             <span className="mx-1 badge badge-light">{i18n.t("mod")}</span>
           )}
-          {this.isAdmin && (
+          {this.creatorIsAdmin && (
             <span className="mx-1 badge badge-light">{i18n.t("admin")}</span>
           )}
           {post_view.creator.bot_account && (
@@ -412,34 +431,20 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               {post.name}
             </Link>
           )}
-          {(isImage(post.url) || post.thumbnail_url) &&
-            (!this.state.imageExpanded ? (
-              <button
-                class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
-                data-tippy-content={i18n.t("expand_here")}
-                onClick={linkEvent(this, this.handleImageExpandClick)}
-              >
-                <Icon icon="plus-square" classes="icon-inline" />
-              </button>
-            ) : (
-              <span>
-                <button
-                  class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
-                  onClick={linkEvent(this, this.handleImageExpandClick)}
-                >
-                  <Icon icon="minus-square" classes="icon-inline" />
-                </button>
-                <div>
-                  <a
-                    href={this.getImageSrc()}
-                    class="btn btn-link d-inline-block"
-                    onClick={linkEvent(this, this.handleImageExpandClick)}
-                  >
-                    <PictrsImage src={this.getImageSrc()} />
-                  </a>
-                </div>
-              </span>
-            ))}
+          {(isImage(post.url) || post.thumbnail_url) && (
+            <button
+              class="btn btn-link text-monospace text-muted small d-inline-block ml-2"
+              data-tippy-content={i18n.t("expand_here")}
+              onClick={linkEvent(this, this.handleImageExpandClick)}
+            >
+              <Icon
+                icon={
+                  !this.state.imageExpanded ? "plus-square" : "minus-square"
+                }
+                classes="icon-inline"
+              />
+            </button>
+          )}
           {post.removed && (
             <small className="ml-2 text-muted font-italic">
               {i18n.t("removed")}
@@ -479,145 +484,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     );
   }
 
-  commentsLine(mobile = false) {
-    let post_view = this.props.post_view;
-    return (
-      <div class="d-flex justify-content-between justify-content-lg-start flex-wrap text-muted font-weight-bold mb-1">
-        <button class="btn btn-link text-muted p-0">
-          <Link
-            className="text-muted small"
-            title={i18n.t("number_of_comments", {
-              count: post_view.counts.comments,
-              formattedCount: post_view.counts.comments,
-            })}
-            to={`/post/${post_view.post.id}?scrollToComments=true`}
-          >
-            <Icon icon="message-square" classes="icon-inline mr-1" />
-            {i18n.t("number_of_comments", {
-              count: post_view.counts.comments,
-              formattedCount: numToSI(post_view.counts.comments),
-            })}
-          </Link>
-        </button>
-        {!mobile && (
-          <>
-            {this.state.downvotes !== 0 && showScores() && (
-              <button
-                class="btn text-muted py-0 pr-0"
-                data-tippy-content={this.pointsTippy}
-                aria-label={i18n.t("downvote")}
-              >
-                <small>
-                  <Icon icon="arrow-down1" classes="icon-inline mr-1" />
-                  <span>{numToSI(this.state.downvotes)}</span>
-                </small>
-              </button>
-            )}
-            {!this.showBody && (
-              <button
-                class="btn btn-link btn-animate text-muted py-0"
-                onClick={linkEvent(this, this.handleSavePostClick)}
-                data-tippy-content={
-                  post_view.saved ? i18n.t("unsave") : i18n.t("save")
-                }
-                aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
-              >
-                <small>
-                  <Icon
-                    icon="star"
-                    classes={`icon-inline ${post_view.saved && "text-warning"}`}
-                  />
-                </small>
-              </button>
-            )}
-          </>
-        )}
-        {/* This is an expanding spacer for mobile */}
-        <div className="flex-grow-1"></div>
-        {mobile && (
-          <>
-            <div>
-              {showScores() ? (
-                <button
-                  className={`btn-animate btn py-0 px-1 ${
-                    this.state.my_vote == 1 ? "text-info" : "text-muted"
-                  }`}
-                  data-tippy-content={this.pointsTippy}
-                  onClick={linkEvent(this, this.handlePostLike)}
-                  aria-label={i18n.t("upvote")}
-                >
-                  <Icon icon="arrow-up1" classes="icon-inline small mr-2" />
-                  {numToSI(this.state.upvotes)}
-                </button>
-              ) : (
-                <button
-                  className={`btn-animate btn py-0 px-1 ${
-                    this.state.my_vote == 1 ? "text-info" : "text-muted"
-                  }`}
-                  onClick={linkEvent(this, this.handlePostLike)}
-                  aria-label={i18n.t("upvote")}
-                >
-                  <Icon icon="arrow-up1" classes="icon-inline small" />
-                </button>
-              )}
-              {this.props.enableDownvotes &&
-                (showScores() ? (
-                  <button
-                    className={`ml-2 btn-animate btn py-0 pl-1 ${
-                      this.state.my_vote == -1 ? "text-danger" : "text-muted"
-                    }`}
-                    onClick={linkEvent(this, this.handlePostDisLike)}
-                    data-tippy-content={this.pointsTippy}
-                    aria-label={i18n.t("downvote")}
-                  >
-                    <Icon icon="arrow-down1" classes="icon-inline small mr-2" />
-                    {this.state.downvotes !== 0 && (
-                      <span>{numToSI(this.state.downvotes)}</span>
-                    )}
-                  </button>
-                ) : (
-                  <button
-                    className={`ml-2 btn-animate btn py-0 pl-1 ${
-                      this.state.my_vote == -1 ? "text-danger" : "text-muted"
-                    }`}
-                    onClick={linkEvent(this, this.handlePostDisLike)}
-                    aria-label={i18n.t("downvote")}
-                  >
-                    <Icon icon="arrow-down1" classes="icon-inline small" />
-                  </button>
-                ))}
-            </div>
-            <button
-              class="btn btn-link btn-animate text-muted py-0 pl-1 pr-0"
-              onClick={linkEvent(this, this.handleSavePostClick)}
-              aria-label={post_view.saved ? i18n.t("unsave") : i18n.t("save")}
-              data-tippy-content={
-                post_view.saved ? i18n.t("unsave") : i18n.t("save")
-              }
-            >
-              <Icon
-                icon="star"
-                classes={`icon-inline ${post_view.saved && "text-warning"}`}
-              />
-            </button>
-
-            {!this.state.showMoreMobile && this.showBody && (
-              <button
-                class="btn btn-link btn-animate text-muted py-0"
-                onClick={linkEvent(this, this.handleShowMoreMobile)}
-                aria-label={i18n.t("more")}
-                data-tippy-content={i18n.t("more")}
-              >
-                <Icon icon="more-vertical" classes="icon-inline" />
-              </button>
-            )}
-            {this.state.showMoreMobile && this.postActions(mobile)}
-          </>
-        )}
-      </div>
-    );
-  }
-
   duplicatesLine() {
     let dupes = this.props.duplicates;
     return (
@@ -643,341 +509,469 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     );
   }
 
+  commentsLine(mobile = false) {
+    return (
+      <div class="d-flex justify-content-start flex-wrap text-muted font-weight-bold mb-1">
+        {this.commentsButton}
+        {mobile && this.mobileVotes}
+        {UserService.Instance.myUserInfo && this.postActions(mobile)}
+      </div>
+    );
+  }
+
   postActions(mobile = false) {
+    // Possible enhancement: Priority+ pattern instead of just hard coding which get hidden behind the show more button.
+    // Possible enhancement: Make each button a component.
     let post_view = this.props.post_view;
     return (
-      UserService.Instance.myUserInfo && (
-        <>
-          {this.showBody && (
-            <>
-              {!mobile && (
-                <button
-                  class="btn btn-link btn-animate text-muted py-0 pl-0"
-                  onClick={linkEvent(this, this.handleSavePostClick)}
-                  data-tippy-content={
-                    post_view.saved ? i18n.t("unsave") : i18n.t("save")
-                  }
-                  aria-label={
-                    post_view.saved ? i18n.t("unsave") : i18n.t("save")
-                  }
+      <>
+        {this.saveButton}
+        {this.crossPostButton}
+        {mobile && this.showMoreButton}
+        {(!mobile || this.state.showAdvanced) && (
+          <>
+            {!this.myPost && (
+              <>
+                {this.reportButton}
+                {this.blockButton}
+              </>
+            )}
+            {this.myPost && (this.showBody || this.state.showAdvanced) && (
+              <>
+                {this.editButton}
+                {this.deleteButton}
+              </>
+            )}
+          </>
+        )}
+        {this.state.showAdvanced && (
+          <>
+            {this.showBody && post_view.post.body && this.viewSourceButton}
+            {this.canModOnSelf && (
+              <>
+                {this.lockButton}
+                {this.stickyButton}
+              </>
+            )}
+            {(this.canMod || this.canAdmin || true) && (
+              <>{this.modRemoveButton}</>
+            )}
+          </>
+        )}
+        {!mobile && this.showMoreButton}
+      </>
+    );
+  }
+
+  get commentsButton() {
+    let post_view = this.props.post_view;
+    return (
+      <button class="btn btn-link text-muted py-0 pl-0">
+        <Link
+          className="text-muted"
+          title={i18n.t("number_of_comments", {
+            count: post_view.counts.comments,
+            formattedCount: post_view.counts.comments,
+          })}
+          to={`/post/${post_view.post.id}?scrollToComments=true`}
+        >
+          <Icon icon="message-square" classes="mr-1" inline />
+          {i18n.t("number_of_comments", {
+            count: post_view.counts.comments,
+            formattedCount: numToSI(post_view.counts.comments),
+          })}
+        </Link>
+      </button>
+    );
+  }
+
+  get mobileVotes() {
+    // TODO: make nicer
+    let tippy = showScores() ? { "data-tippy-content": this.pointsTippy } : {};
+    return (
+      <>
+        <div>
+          <button
+            className={`btn-animate btn py-0 px-1 ${
+              this.state.my_vote == 1 ? "text-info" : "text-muted"
+            }`}
+            {...tippy}
+            onClick={linkEvent(this, this.handlePostLike)}
+            aria-label={i18n.t("upvote")}
+          >
+            <Icon icon="arrow-up1" classes="icon-inline small" />
+            {showScores() && (
+              <span class="ml-2">{numToSI(this.state.upvotes)}</span>
+            )}
+          </button>
+          {this.props.enableDownvotes && (
+            <button
+              className={`ml-2 btn-animate btn py-0 px-1 ${
+                this.state.my_vote == -1 ? "text-danger" : "text-muted"
+              }`}
+              onClick={linkEvent(this, this.handlePostDisLike)}
+              {...tippy}
+              aria-label={i18n.t("downvote")}
+            >
+              <Icon icon="arrow-down1" classes="icon-inline small" />
+              {showScores() && (
+                <span
+                  class={classNames("ml-2", {
+                    invisible: this.state.downvotes === 0,
+                  })}
                 >
-                  <Icon
-                    icon="star"
-                    classes={`icon-inline ${post_view.saved && "text-warning"}`}
-                  />
-                </button>
+                  {numToSI(this.state.downvotes)}
+                </span>
               )}
-              <Link
-                className="btn btn-link btn-animate text-muted py-0"
-                to={`/create_post${this.crossPostParams}`}
-                title={i18n.t("cross_post")}
-              >
-                <Icon icon="copy" classes="icon-inline" />
-              </Link>
-              {!this.myPost && (
-                <>
+            </button>
+          )}
+        </div>
+      </>
+    );
+  }
+
+  get saveButton() {
+    let saved = this.props.post_view.saved;
+    let label = saved ? i18n.t("unsave") : i18n.t("save");
+    return (
+      <button
+        class="btn btn-link btn-animate text-muted py-0"
+        onClick={linkEvent(this, this.handleSavePostClick)}
+        data-tippy-content={label}
+        aria-label={label}
+      >
+        <Icon
+          icon="star"
+          classes={classNames({ "text-warning": saved })}
+          inline
+        />
+      </button>
+    );
+  }
+
+  get crossPostButton() {
+    return (
+      <Link
+        className="btn btn-link btn-animate text-muted py-0"
+        to={`/create_post${this.crossPostParams}`}
+        title={i18n.t("cross_post")}
+      >
+        <Icon icon="copy" inline />
+      </Link>
+    );
+  }
+
+  get reportButton() {
+    return (
+      <button
+        class="btn btn-link btn-animate text-muted py-0"
+        onClick={linkEvent(this, this.handleShowReportDialog)}
+        data-tippy-content={i18n.t("show_report_dialog")}
+        aria-label={i18n.t("show_report_dialog")}
+      >
+        <Icon icon="flag" inline />
+      </button>
+    );
+  }
+
+  get blockButton() {
+    return (
+      <button
+        class="btn btn-link btn-animate text-muted py-0"
+        onClick={linkEvent(this, this.handleBlockUserClick)}
+        data-tippy-content={i18n.t("block_user")}
+        aria-label={i18n.t("block_user")}
+      >
+        <Icon icon="slash" inline />
+      </button>
+    );
+  }
+
+  get editButton() {
+    return (
+      <button
+        class="btn btn-link btn-animate text-muted py-0"
+        onClick={linkEvent(this, this.handleEditClick)}
+        data-tippy-content={i18n.t("edit")}
+        aria-label={i18n.t("edit")}
+      >
+        <Icon icon="edit" inline />
+      </button>
+    );
+  }
+
+  get deleteButton() {
+    let deleted = this.props.post_view.post.deleted;
+    let label = !deleted ? i18n.t("delete") : i18n.t("restore");
+    return (
+      <button
+        class="btn btn-link btn-animate text-muted py-0"
+        onClick={linkEvent(this, this.handleDeleteClick)}
+        data-tippy-content={label}
+        aria-label={label}
+      >
+        <Icon
+          icon="trash"
+          classes={classNames({ "text-danger": deleted })}
+          inline
+        />
+      </button>
+    );
+  }
+
+  get showMoreButton() {
+    return (
+      <button
+        class="btn btn-link btn-animate text-muted py-0"
+        onClick={linkEvent(this, this.handleShowAdvanced)}
+        data-tippy-content={i18n.t("more")}
+        aria-label={i18n.t("more")}
+      >
+        <Icon icon="more-vertical" inline />
+      </button>
+    );
+  }
+
+  get viewSourceButton() {
+    return (
+      <button
+        class="btn btn-link btn-animate text-muted py-0"
+        onClick={linkEvent(this, this.handleViewSource)}
+        data-tippy-content={i18n.t("view_source")}
+        aria-label={i18n.t("view_source")}
+      >
+        <Icon
+          icon="file-text"
+          classes={classNames({ "text-success": this.state.viewSource })}
+          inline
+        />
+      </button>
+    );
+  }
+
+  get lockButton() {
+    let locked = this.props.post_view.post.locked;
+    let label = locked ? i18n.t("unlock") : i18n.t("lock");
+    return (
+      <button
+        class="btn btn-link btn-animate text-muted py-0"
+        onClick={linkEvent(this, this.handleModLock)}
+        data-tippy-content={label}
+        aria-label={label}
+      >
+        <Icon
+          icon="lock"
+          classes={classNames({ "text-danger": locked })}
+          inline
+        />
+      </button>
+    );
+  }
+
+  get stickyButton() {
+    let stickied = this.props.post_view.post.stickied;
+    let label = stickied ? i18n.t("unsticky") : i18n.t("sticky");
+    return (
+      <button
+        class="btn btn-link btn-animate text-muted py-0"
+        onClick={linkEvent(this, this.handleModSticky)}
+        data-tippy-content={label}
+        aria-label={label}
+      >
+        <Icon
+          icon="pin"
+          classes={classNames({ "text-success": stickied })}
+          inline
+        />
+      </button>
+    );
+  }
+
+  get modRemoveButton() {
+    let removed = this.props.post_view.post.removed;
+    return (
+      <button
+        class="btn btn-link btn-animate text-muted py-0"
+        onClick={linkEvent(
+          this,
+          !removed ? this.handleModRemoveShow : this.handleModRemoveSubmit
+        )}
+      >
+        {/* TODO: Find an icon for this. */}
+        {!removed ? i18n.t("remove") : i18n.t("restore")}
+      </button>
+    );
+  }
+
+  /**
+   * Mod/Admin actions to be taken against the author.
+   */
+  userActionsLine() {
+    // TODO: make nicer
+    let post_view = this.props.post_view;
+    return (
+      this.state.showAdvanced && (
+        <>
+          {this.canMod && (
+            <>
+              {!this.creatorIsMod &&
+                (!post_view.creator_banned_from_community ? (
                   <button
                     class="btn btn-link btn-animate text-muted py-0"
-                    onClick={linkEvent(this, this.handleShowReportDialog)}
-                    data-tippy-content={i18n.t("show_report_dialog")}
-                    aria-label={i18n.t("show_report_dialog")}
+                    onClick={linkEvent(
+                      this,
+                      this.handleModBanFromCommunityShow
+                    )}
+                    aria-label={i18n.t("ban")}
                   >
-                    <Icon icon="flag" classes="icon-inline" />
+                    {i18n.t("ban")}
                   </button>
+                ) : (
                   <button
                     class="btn btn-link btn-animate text-muted py-0"
-                    onClick={linkEvent(this, this.handleBlockUserClick)}
-                    data-tippy-content={i18n.t("block_user")}
-                    aria-label={i18n.t("block_user")}
+                    onClick={linkEvent(
+                      this,
+                      this.handleModBanFromCommunitySubmit
+                    )}
+                    aria-label={i18n.t("unban")}
                   >
-                    <Icon icon="slash" classes="icon-inline" />
+                    {i18n.t("unban")}
                   </button>
-                </>
+                ))}
+              {!post_view.creator_banned_from_community && (
+                <button
+                  class="btn btn-link btn-animate text-muted py-0"
+                  onClick={linkEvent(this, this.handleAddModToCommunity)}
+                  aria-label={
+                    this.creatorIsMod
+                      ? i18n.t("remove_as_mod")
+                      : i18n.t("appoint_as_mod")
+                  }
+                >
+                  {this.creatorIsMod
+                    ? i18n.t("remove_as_mod")
+                    : i18n.t("appoint_as_mod")}
+                </button>
               )}
             </>
           )}
-          {this.myPost && this.showBody && (
-            <>
+          {/* Community creators and admins can transfer community to another mod */}
+          {(this.amCommunityCreator || this.canAdmin) &&
+            this.creatorIsMod &&
+            (!this.state.showConfirmTransferCommunity ? (
               <button
                 class="btn btn-link btn-animate text-muted py-0"
-                onClick={linkEvent(this, this.handleEditClick)}
-                data-tippy-content={i18n.t("edit")}
-                aria-label={i18n.t("edit")}
-              >
-                <Icon icon="edit" classes="icon-inline" />
-              </button>
-              <button
-                class="btn btn-link btn-animate text-muted py-0"
-                onClick={linkEvent(this, this.handleDeleteClick)}
-                data-tippy-content={
-                  !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
-                }
-                aria-label={
-                  !post_view.post.deleted ? i18n.t("delete") : i18n.t("restore")
-                }
+                onClick={linkEvent(
+                  this,
+                  this.handleShowConfirmTransferCommunity
+                )}
+                aria-label={i18n.t("transfer_community")}
               >
-                <Icon
-                  icon="trash"
-                  classes={`icon-inline ${
-                    post_view.post.deleted && "text-danger"
-                  }`}
-                />
+                {i18n.t("transfer_community")}
               </button>
-            </>
-          )}
-
-          {!this.state.showAdvanced && this.showBody ? (
-            <button
-              class="btn btn-link btn-animate text-muted py-0"
-              onClick={linkEvent(this, this.handleShowAdvanced)}
-              data-tippy-content={i18n.t("more")}
-              aria-label={i18n.t("more")}
-            >
-              <Icon icon="more-vertical" classes="icon-inline" />
-            </button>
-          ) : (
-            <>
-              {this.showBody && post_view.post.body && (
+            ) : (
+              <>
                 <button
-                  class="btn btn-link btn-animate text-muted py-0"
-                  onClick={linkEvent(this, this.handleViewSource)}
-                  data-tippy-content={i18n.t("view_source")}
-                  aria-label={i18n.t("view_source")}
+                  class="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0"
+                  aria-label={i18n.t("are_you_sure")}
                 >
-                  <Icon
-                    icon="file-text"
-                    classes={`icon-inline ${
-                      this.state.viewSource && "text-success"
-                    }`}
-                  />
+                  {i18n.t("are_you_sure")}
                 </button>
-              )}
-              {this.canModOnSelf && (
-                <>
-                  <button
-                    class="btn btn-link btn-animate text-muted py-0"
-                    onClick={linkEvent(this, this.handleModLock)}
-                    data-tippy-content={
-                      post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
-                    }
-                    aria-label={
-                      post_view.post.locked ? i18n.t("unlock") : i18n.t("lock")
-                    }
-                  >
-                    <Icon
-                      icon="lock"
-                      classes={`icon-inline ${
-                        post_view.post.locked && "text-danger"
-                      }`}
-                    />
-                  </button>
-                  <button
-                    class="btn btn-link btn-animate text-muted py-0"
-                    onClick={linkEvent(this, this.handleModSticky)}
-                    data-tippy-content={
-                      post_view.post.stickied
-                        ? i18n.t("unsticky")
-                        : i18n.t("sticky")
-                    }
-                    aria-label={
-                      post_view.post.stickied
-                        ? i18n.t("unsticky")
-                        : i18n.t("sticky")
-                    }
-                  >
-                    <Icon
-                      icon="pin"
-                      classes={`icon-inline ${
-                        post_view.post.stickied && "text-success"
-                      }`}
-                    />
-                  </button>
-                </>
-              )}
-              {/* Mods can ban from community, and appoint as mods to community */}
-              {(this.canMod || this.canAdmin) &&
-                (!post_view.post.removed ? (
-                  <button
-                    class="btn btn-link btn-animate text-muted py-0"
-                    onClick={linkEvent(this, this.handleModRemoveShow)}
-                    aria-label={i18n.t("remove")}
-                  >
-                    {i18n.t("remove")}
-                  </button>
-                ) : (
-                  <button
-                    class="btn btn-link btn-animate text-muted py-0"
-                    onClick={linkEvent(this, this.handleModRemoveSubmit)}
-                    aria-label={i18n.t("restore")}
-                  >
-                    {i18n.t("restore")}
-                  </button>
-                ))}
-              {this.canMod && (
-                <>
-                  {!this.isMod &&
-                    (!post_view.creator_banned_from_community ? (
-                      <button
-                        class="btn btn-link btn-animate text-muted py-0"
-                        onClick={linkEvent(
-                          this,
-                          this.handleModBanFromCommunityShow
-                        )}
-                        aria-label={i18n.t("ban")}
-                      >
-                        {i18n.t("ban")}
-                      </button>
-                    ) : (
-                      <button
-                        class="btn btn-link btn-animate text-muted py-0"
-                        onClick={linkEvent(
-                          this,
-                          this.handleModBanFromCommunitySubmit
-                        )}
-                        aria-label={i18n.t("unban")}
-                      >
-                        {i18n.t("unban")}
-                      </button>
-                    ))}
-                  {!post_view.creator_banned_from_community && (
-                    <button
-                      class="btn btn-link btn-animate text-muted py-0"
-                      onClick={linkEvent(this, this.handleAddModToCommunity)}
-                      aria-label={
-                        this.isMod
-                          ? i18n.t("remove_as_mod")
-                          : i18n.t("appoint_as_mod")
-                      }
-                    >
-                      {this.isMod
-                        ? i18n.t("remove_as_mod")
-                        : i18n.t("appoint_as_mod")}
-                    </button>
+                <button
+                  class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
+                  aria-label={i18n.t("yes")}
+                  onClick={linkEvent(this, this.handleTransferCommunity)}
+                >
+                  {i18n.t("yes")}
+                </button>
+                <button
+                  class="btn btn-link btn-animate text-muted py-0 d-inline-block"
+                  onClick={linkEvent(
+                    this,
+                    this.handleCancelShowConfirmTransferCommunity
                   )}
-                </>
-              )}
-              {/* Community creators and admins can transfer community to another mod */}
-              {(this.amCommunityCreator || this.canAdmin) &&
-                this.isMod &&
-                (!this.state.showConfirmTransferCommunity ? (
+                  aria-label={i18n.t("no")}
+                >
+                  {i18n.t("no")}
+                </button>
+              </>
+            ))}
+          {/* Admins can ban from all, and appoint other admins */}
+          {this.canAdmin && (
+            <>
+              {!this.creatorIsAdmin &&
+                (!isBanned(post_view.creator) ? (
                   <button
                     class="btn btn-link btn-animate text-muted py-0"
-                    onClick={linkEvent(
-                      this,
-                      this.handleShowConfirmTransferCommunity
-                    )}
-                    aria-label={i18n.t("transfer_community")}
+                    onClick={linkEvent(this, this.handleModBanShow)}
+                    aria-label={i18n.t("ban_from_site")}
                   >
-                    {i18n.t("transfer_community")}
+                    {i18n.t("ban_from_site")}
                   </button>
                 ) : (
-                  <>
-                    <button
-                      class="d-inline-block mr-1 btn btn-link btn-animate text-muted py-0"
-                      aria-label={i18n.t("are_you_sure")}
-                    >
-                      {i18n.t("are_you_sure")}
-                    </button>
-                    <button
-                      class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
-                      aria-label={i18n.t("yes")}
-                      onClick={linkEvent(this, this.handleTransferCommunity)}
-                    >
-                      {i18n.t("yes")}
-                    </button>
-                    <button
-                      class="btn btn-link btn-animate text-muted py-0 d-inline-block"
-                      onClick={linkEvent(
-                        this,
-                        this.handleCancelShowConfirmTransferCommunity
-                      )}
-                      aria-label={i18n.t("no")}
-                    >
-                      {i18n.t("no")}
-                    </button>
-                  </>
-                ))}
-              {/* Admins can ban from all, and appoint other admins */}
-              {this.canAdmin && (
-                <>
-                  {!this.isAdmin &&
-                    (!isBanned(post_view.creator) ? (
-                      <button
-                        class="btn btn-link btn-animate text-muted py-0"
-                        onClick={linkEvent(this, this.handleModBanShow)}
-                        aria-label={i18n.t("ban_from_site")}
-                      >
-                        {i18n.t("ban_from_site")}
-                      </button>
-                    ) : (
-                      <button
-                        class="btn btn-link btn-animate text-muted py-0"
-                        onClick={linkEvent(this, this.handleModBanSubmit)}
-                        aria-label={i18n.t("unban_from_site")}
-                      >
-                        {i18n.t("unban_from_site")}
-                      </button>
-                    ))}
-                  {!isBanned(post_view.creator) && post_view.creator.local && (
-                    <button
-                      class="btn btn-link btn-animate text-muted py-0"
-                      onClick={linkEvent(this, this.handleAddAdmin)}
-                      aria-label={
-                        this.isAdmin
-                          ? i18n.t("remove_as_admin")
-                          : i18n.t("appoint_as_admin")
-                      }
-                    >
-                      {this.isAdmin
-                        ? i18n.t("remove_as_admin")
-                        : i18n.t("appoint_as_admin")}
-                    </button>
-                  )}
-                </>
-              )}
-              {/* Site Creator can transfer to another admin */}
-              {this.amSiteCreator &&
-                this.isAdmin &&
-                (!this.state.showConfirmTransferSite ? (
                   <button
                     class="btn btn-link btn-animate text-muted py-0"
-                    onClick={linkEvent(
-                      this,
-                      this.handleShowConfirmTransferSite
-                    )}
-                    aria-label={i18n.t("transfer_site")}
+                    onClick={linkEvent(this, this.handleModBanSubmit)}
+                    aria-label={i18n.t("unban_from_site")}
                   >
-                    {i18n.t("transfer_site")}
+                    {i18n.t("unban_from_site")}
                   </button>
-                ) : (
-                  <>
-                    <button
-                      class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
-                      aria-label={i18n.t("are_you_sure")}
-                    >
-                      {i18n.t("are_you_sure")}
-                    </button>
-                    <button
-                      class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
-                      onClick={linkEvent(this, this.handleTransferSite)}
-                      aria-label={i18n.t("yes")}
-                    >
-                      {i18n.t("yes")}
-                    </button>
-                    <button
-                      class="btn btn-link btn-animate text-muted py-0 d-inline-block"
-                      onClick={linkEvent(
-                        this,
-                        this.handleCancelShowConfirmTransferSite
-                      )}
-                      aria-label={i18n.t("no")}
-                    >
-                      {i18n.t("no")}
-                    </button>
-                  </>
                 ))}
+              {!isBanned(post_view.creator) && post_view.creator.local && (
+                <button
+                  class="btn btn-link btn-animate text-muted py-0"
+                  onClick={linkEvent(this, this.handleAddAdmin)}
+                  aria-label={
+                    this.creatorIsAdmin
+                      ? i18n.t("remove_as_admin")
+                      : i18n.t("appoint_as_admin")
+                  }
+                >
+                  {this.creatorIsAdmin
+                    ? i18n.t("remove_as_admin")
+                    : i18n.t("appoint_as_admin")}
+                </button>
+              )}
             </>
           )}
+          {/* Site Creator can transfer to another admin */}
+          {this.amSiteCreator &&
+            this.creatorIsAdmin &&
+            (!this.state.showConfirmTransferSite ? (
+              <button
+                class="btn btn-link btn-animate text-muted py-0"
+                onClick={linkEvent(this, this.handleShowConfirmTransferSite)}
+                aria-label={i18n.t("transfer_site")}
+              >
+                {i18n.t("transfer_site")}
+              </button>
+            ) : (
+              <>
+                <button
+                  class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
+                  aria-label={i18n.t("are_you_sure")}
+                >
+                  {i18n.t("are_you_sure")}
+                </button>
+                <button
+                  class="btn btn-link btn-animate text-muted py-0 d-inline-block mr-1"
+                  onClick={linkEvent(this, this.handleTransferSite)}
+                  aria-label={i18n.t("yes")}
+                >
+                  {i18n.t("yes")}
+                </button>
+                <button
+                  class="btn btn-link btn-animate text-muted py-0 d-inline-block"
+                  onClick={linkEvent(
+                    this,
+                    this.handleCancelShowConfirmTransferSite
+                  )}
+                  aria-label={i18n.t("no")}
+                >
+                  {i18n.t("no")}
+                </button>
+              </>
+            ))}
         </>
       )
     );
@@ -1150,6 +1144,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               {this.showMobilePreview()}
 
               {this.commentsLine(true)}
+              {this.userActionsLine()}
               {this.duplicatesLine()}
               {this.removeAndBanDialogs()}
             </div>
@@ -1160,23 +1155,17 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         <div class="d-none d-sm-block">
           <div class="row">
             {this.voteBar()}
-            {!this.state.imageExpanded && (
-              <div class="col-sm-2 pr-0">
-                <div class="">{this.thumbnail()}</div>
-              </div>
-            )}
-            <div
-              class={`${
-                this.state.imageExpanded ? "col-12" : "col-12 col-sm-9"
-              }`}
-            >
+            <div class="col-sm-2 pr-0">
+              <div class="">{this.thumbnail()}</div>
+            </div>
+            <div class="col-12 col-sm-9">
               <div class="row">
                 <div className="col-12">
                   {this.postTitleLine()}
                   {this.createdLine()}
                   {this.commentsLine()}
                   {this.duplicatesLine()}
-                  {this.postActions()}
+                  {this.userActionsLine()}
                   {this.removeAndBanDialogs()}
                 </div>
               </div>
@@ -1195,7 +1184,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     );
   }
 
-  get isMod(): boolean {
+  get creatorIsMod(): boolean {
     return (
       this.props.moderators &&
       isMod(
@@ -1205,7 +1194,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     );
   }
 
-  get isAdmin(): boolean {
+  get creatorIsAdmin(): boolean {
     return (
       this.props.admins &&
       isMod(
@@ -1215,6 +1204,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     );
   }
 
+  /**
+   * If the current user is allowed to mod this post.
+   * The creator of this post is not allowed even if they are a mod.
+   */
   get canMod(): boolean {
     if (this.props.admins && this.props.moderators) {
       let adminsThenMods = this.props.admins
@@ -1231,6 +1224,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     }
   }
 
+  /**
+   * If the current user is allowed to mod this post.
+   * The creator of this post is allowed if they are a mod.
+   */
   get canModOnSelf(): boolean {
     if (this.props.admins && this.props.moderators) {
       let adminsThenMods = this.props.admins
@@ -1569,7 +1566,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     let form: AddModToCommunity = {
       person_id: i.props.post_view.creator.id,
       community_id: i.props.post_view.community.id,
-      added: !i.isMod,
+      added: !i.creatorIsMod,
       auth: authField(),
     };
     WebSocketService.Instance.send(wsClient.addModToCommunity(form));
@@ -1579,7 +1576,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   handleAddAdmin(i: PostListing) {
     let form: AddAdmin = {
       person_id: i.props.post_view.creator.id,
-      added: !i.isAdmin,
+      added: !i.creatorIsAdmin,
       auth: authField(),
     };
     WebSocketService.Instance.send(wsClient.addAdmin(form));
@@ -1630,7 +1627,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   handleImageExpandClick(i: PostListing, event: any) {
     event.preventDefault();
     i.state.imageExpanded = !i.state.imageExpanded;
+    i.state.showBody = i.state.imageExpanded;
     i.setState(i.state);
+    setupTippy();
   }
 
   handleViewSource(i: PostListing) {
index 422beac39fb5e1c1b8a6dfb506108610088ceb61..091e85ade0733be562e5ef97a220ea166882f7c2 100644 (file)
@@ -323,10 +323,8 @@ export function isMod(modIds: number[], creator_id: number): boolean {
   return modIds.includes(creator_id);
 }
 
-const imageRegex = new RegExp(
-  /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/
-);
-const videoRegex = new RegExp(`(http)?s?:?(\/\/[^"']*\.(?:mp4))`);
+const imageRegex = /(http)?s?:?(\/\/[^"']*\.(?:jpg|jpeg|gif|png|svg|webp))/;
+const videoRegex = /(http)?s?:?(\/\/[^"']*\.(?:mp4|webm))/;
 
 export function isImage(url: string) {
   return imageRegex.test(url);
index 2aac79006722bc0f240d63efd9ecb4b31c85700f..2fb682b9285d6790c2b49a762d1f2389a95dcd9c 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -10,9 +10,9 @@
     "@babel/highlight" "^7.16.7"
 
 "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.4":
-  version "7.16.4"
-  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.4.tgz#081d6bbc336ec5c2435c6346b2ae1fb98b5ac68e"
-  integrity sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==
+  version "7.16.8"
+  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.8.tgz#31560f9f29fdf1868de8cb55049538a1b9732a60"
+  integrity sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q==
 
 "@babel/core@^7.16.0", "@babel/core@^7.2.2":
   version "7.16.7"
     semver "^6.3.0"
     source-map "^0.5.0"
 
-"@babel/generator@^7.16.7":
-  version "7.16.7"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.7.tgz#b42bf46a3079fa65e1544135f32e7958f048adbb"
-  integrity sha512-/ST3Sg8MLGY5HVYmrjOgL60ENux/HfO/CsUh7y4MalThufhE/Ff/6EibFDHi4jiDCaWfJKoqbE6oTh21c5hrRg==
+"@babel/generator@^7.16.7", "@babel/generator@^7.16.8":
+  version "7.16.8"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.8.tgz#359d44d966b8cd059d543250ce79596f792f2ebe"
+  integrity sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==
   dependencies:
-    "@babel/types" "^7.16.7"
+    "@babel/types" "^7.16.8"
     jsesc "^2.5.1"
     source-map "^0.5.0"
 
   resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5"
   integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==
 
-"@babel/helper-remap-async-to-generator@^7.16.7":
-  version "7.16.7"
-  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.7.tgz#5ce2416990d55eb6e099128338848ae8ffa58a9a"
-  integrity sha512-C3o117GnP/j/N2OWo+oepeWbFEKRfNaay+F1Eo5Mj3A1SRjyx+qaFhm23nlipub7Cjv2azdUUiDH+VlpdwUFRg==
+"@babel/helper-remap-async-to-generator@^7.16.8":
+  version "7.16.8"
+  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3"
+  integrity sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw==
   dependencies:
     "@babel/helper-annotate-as-pure" "^7.16.7"
-    "@babel/helper-wrap-function" "^7.16.7"
-    "@babel/types" "^7.16.7"
+    "@babel/helper-wrap-function" "^7.16.8"
+    "@babel/types" "^7.16.8"
 
 "@babel/helper-replace-supers@^7.16.7":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23"
   integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==
 
-"@babel/helper-wrap-function@^7.16.7":
-  version "7.16.7"
-  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.7.tgz#8ddf9eaa770ed43de4bc3687f3f3b0d6d5ecf014"
-  integrity sha512-7a9sABeVwcunnztZZ7WTgSw6jVYLzM1wua0Z4HIXm9S3/HC96WKQTkFgGEaj5W06SHHihPJ6Le6HzS5cGOQMNw==
+"@babel/helper-wrap-function@^7.16.8":
+  version "7.16.8"
+  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz#58afda087c4cd235de92f7ceedebca2c41274200"
+  integrity sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw==
   dependencies:
     "@babel/helper-function-name" "^7.16.7"
     "@babel/template" "^7.16.7"
-    "@babel/traverse" "^7.16.7"
-    "@babel/types" "^7.16.7"
+    "@babel/traverse" "^7.16.8"
+    "@babel/types" "^7.16.8"
 
 "@babel/helpers@^7.16.7":
   version "7.16.7"
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.0.0-beta.54", "@babel/parser@^7.16.7":
-  version "7.16.7"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.7.tgz#d372dda9c89fcec340a82630a9f533f2fe15877e"
-  integrity sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA==
+"@babel/parser@^7.0.0-beta.54", "@babel/parser@^7.16.7", "@babel/parser@^7.16.8":
+  version "7.16.8"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.8.tgz#61c243a3875f7d0b0962b0543a33ece6ff2f1f17"
+  integrity sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw==
 
 "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7":
   version "7.16.7"
     "@babel/plugin-proposal-optional-chaining" "^7.16.7"
 
 "@babel/plugin-proposal-async-generator-functions@^7.16.7":
-  version "7.16.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.7.tgz#739adc1212a9e4892de440cd7dfffb06172df78d"
-  integrity sha512-TTXBT3A5c11eqRzaC6beO6rlFT3Mo9C2e8eB44tTr52ESXSK2CIc2fOp1ynpAwQA8HhBMho+WXhMHWlAe3xkpw==
+  version "7.16.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz#3bdd1ebbe620804ea9416706cd67d60787504bc8"
+  integrity sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.16.7"
-    "@babel/helper-remap-async-to-generator" "^7.16.7"
+    "@babel/helper-remap-async-to-generator" "^7.16.8"
     "@babel/plugin-syntax-async-generators" "^7.8.4"
 
 "@babel/plugin-proposal-class-properties@^7.16.7":
     "@babel/helper-plugin-utils" "^7.16.7"
 
 "@babel/plugin-transform-async-to-generator@^7.16.7":
-  version "7.16.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.7.tgz#646e1262ac341b587ff5449844d4492dbb10ac4b"
-  integrity sha512-pFEfjnK4DfXCfAlA5I98BYdDJD8NltMzx19gt6DAmfE+2lXRfPUoa0/5SUjT4+TDE1W/rcxU/1lgN55vpAjjdg==
+  version "7.16.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz#b83dff4b970cf41f1b819f8b49cc0cfbaa53a808"
+  integrity sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg==
   dependencies:
     "@babel/helper-module-imports" "^7.16.7"
     "@babel/helper-plugin-utils" "^7.16.7"
-    "@babel/helper-remap-async-to-generator" "^7.16.7"
+    "@babel/helper-remap-async-to-generator" "^7.16.8"
 
 "@babel/plugin-transform-block-scoped-functions@^7.16.7":
   version "7.16.7"
     babel-plugin-dynamic-import-node "^2.3.3"
 
 "@babel/plugin-transform-modules-commonjs@^7.16.7":
-  version "7.16.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.7.tgz#fd119e6a433c527d368425b45df361e1e95d3c1a"
-  integrity sha512-h2RP2kE7He1ZWKyAlanMZrAbdv+Acw1pA8dQZhE025WJZE2z0xzFADAinXA9fxd5bn7JnM+SdOGcndGx1ARs9w==
+  version "7.16.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz#cdee19aae887b16b9d331009aa9a219af7c86afe"
+  integrity sha512-oflKPvsLT2+uKQopesJt3ApiaIS2HW+hzHFcwRNtyDGieAeC/dIHZX8buJQ2J2X1rxGPy4eRcUijm3qcSPjYcA==
   dependencies:
     "@babel/helper-module-transforms" "^7.16.7"
     "@babel/helper-plugin-utils" "^7.16.7"
     "@babel/helper-plugin-utils" "^7.16.7"
 
 "@babel/plugin-transform-named-capturing-groups-regex@^7.16.7":
-  version "7.16.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.7.tgz#749d90d94e73cf62c60a0cc8d6b94d29305a81f2"
-  integrity sha512-kFy35VwmwIQwCjwrAQhl3+c/kr292i4KdLPKp5lPH03Ltc51qnFlIADoyPxc/6Naz3ok3WdYKg+KK6AH+D4utg==
+  version "7.16.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz#7f860e0e40d844a02c9dcf9d84965e7dfd666252"
+  integrity sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw==
   dependencies:
     "@babel/helper-create-regexp-features-plugin" "^7.16.7"
 
     "@babel/helper-plugin-utils" "^7.16.7"
 
 "@babel/plugin-transform-runtime@^7.16.4":
-  version "7.16.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.7.tgz#1da184cb83a2287a01956c10c60e66dd503c18aa"
-  integrity sha512-2FoHiSAWkdq4L06uaDN3rS43i6x28desUVxq+zAFuE6kbWYQeiLPJI5IC7Sg9xKYVcrBKSQkVUfH6aeQYbl9QA==
+  version "7.16.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.8.tgz#3339368701103edae708f0fba9e4bfb70a3e5872"
+  integrity sha512-6Kg2XHPFnIarNweZxmzbgYnnWsXxkx9WQUVk2sksBRL80lBC1RAQV3wQagWxdCHiYHqPN+oenwNIuttlYgIbQQ==
   dependencies:
     "@babel/helper-module-imports" "^7.16.7"
     "@babel/helper-plugin-utils" "^7.16.7"
     babel-plugin-polyfill-corejs2 "^0.3.0"
-    babel-plugin-polyfill-corejs3 "^0.4.0"
+    babel-plugin-polyfill-corejs3 "^0.5.0"
     babel-plugin-polyfill-regenerator "^0.3.0"
     semver "^6.3.0"
 
     "@babel/helper-plugin-utils" "^7.16.7"
 
 "@babel/plugin-transform-typescript@^7.16.1", "@babel/plugin-transform-typescript@^7.16.7":
-  version "7.16.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.7.tgz#33f8c2c890fbfdc4ef82446e9abb8de8211a3ff3"
-  integrity sha512-Hzx1lvBtOCWuCEwMmYOfpQpO7joFeXLgoPuzZZBtTxXqSqUGUubvFGZv2ygo1tB5Bp9q6PXV3H0E/kf7KM0RLA==
+  version "7.16.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz#591ce9b6b83504903fa9dd3652c357c2ba7a1ee0"
+  integrity sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ==
   dependencies:
     "@babel/helper-create-class-features-plugin" "^7.16.7"
     "@babel/helper-plugin-utils" "^7.16.7"
     "@babel/parser" "^7.16.7"
     "@babel/types" "^7.16.7"
 
-"@babel/traverse@^7.0.0-beta.54", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7":
-  version "7.16.7"
-  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.7.tgz#dac01236a72c2560073658dd1a285fe4e0865d76"
-  integrity sha512-8KWJPIb8c2VvY8AJrydh6+fVRo2ODx1wYBU2398xJVq0JomuLBZmVQzLPBblJgHIGYG4znCpUZUZ0Pt2vdmVYQ==
+"@babel/traverse@^7.0.0-beta.54", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8":
+  version "7.16.8"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.8.tgz#bab2f2b09a5fe8a8d9cad22cbfe3ba1d126fef9c"
+  integrity sha512-xe+H7JlvKsDQwXRsBhSnq1/+9c+LlQcCK3Tn/l5sbx02HYns/cn7ibp9+RV1sIUqu7hKg91NWsgHurO9dowITQ==
   dependencies:
     "@babel/code-frame" "^7.16.7"
-    "@babel/generator" "^7.16.7"
+    "@babel/generator" "^7.16.8"
     "@babel/helper-environment-visitor" "^7.16.7"
     "@babel/helper-function-name" "^7.16.7"
     "@babel/helper-hoist-variables" "^7.16.7"
     "@babel/helper-split-export-declaration" "^7.16.7"
-    "@babel/parser" "^7.16.7"
-    "@babel/types" "^7.16.7"
+    "@babel/parser" "^7.16.8"
+    "@babel/types" "^7.16.8"
     debug "^4.1.0"
     globals "^11.1.0"
 
-"@babel/types@^7", "@babel/types@^7.0.0-beta.54", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.4.4":
-  version "7.16.7"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.7.tgz#4ed19d51f840ed4bd5645be6ce40775fecf03159"
-  integrity sha512-E8HuV7FO9qLpx6OtoGfUQ2cjIYnbFwvZWYBS+87EwtdMvmUPJSwykpovFB+8insbpF0uJcpr8KMUi64XZntZcg==
+"@babel/types@^7", "@babel/types@^7.0.0-beta.54", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.4.4":
+  version "7.16.8"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.8.tgz#0ba5da91dd71e0a4e7781a30f22770831062e3c1"
+  integrity sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==
   dependencies:
     "@babel/helper-validator-identifier" "^7.16.7"
     to-fast-properties "^2.0.0"
     "@types/node" "*"
 
 "@types/eslint-scope@^3.7.0":
-  version "3.7.2"
-  resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.2.tgz#11e96a868c67acf65bf6f11d10bb89ea71d5e473"
-  integrity sha512-TzgYCWoPiTeRg6RQYgtuW7iODtVoKu3RVL72k3WohqhjfaOLK5Mg2T4Tg1o2bSfu0vPkoI48wdQFv5b/Xe04wQ==
+  version "3.7.3"
+  resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224"
+  integrity sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==
   dependencies:
     "@types/eslint" "*"
     "@types/estree" "*"
     "@types/node" "*"
 
 "@typescript-eslint/eslint-plugin@^5.6.0":
-  version "5.9.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.9.0.tgz#382182d5cb062f52aac54434cfc47c28898c8006"
-  integrity sha512-qT4lr2jysDQBQOPsCCvpPUZHjbABoTJW8V9ZzIYKHMfppJtpdtzszDYsldwhFxlhvrp7aCHeXD1Lb9M1zhwWwQ==
+  version "5.9.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.9.1.tgz#e5a86d7e1f9dc0b3df1e6d94feaf20dd838d066c"
+  integrity sha512-Xv9tkFlyD4MQGpJgTo6wqDqGvHIRmRgah/2Sjz1PUnJTawjHWIwBivUE9x0QtU2WVii9baYgavo/bHjrZJkqTw==
   dependencies:
-    "@typescript-eslint/experimental-utils" "5.9.0"
-    "@typescript-eslint/scope-manager" "5.9.0"
-    "@typescript-eslint/type-utils" "5.9.0"
+    "@typescript-eslint/experimental-utils" "5.9.1"
+    "@typescript-eslint/scope-manager" "5.9.1"
+    "@typescript-eslint/type-utils" "5.9.1"
     debug "^4.3.2"
     functional-red-black-tree "^1.0.1"
     ignore "^5.1.8"
     semver "^7.3.5"
     tsutils "^3.21.0"
 
-"@typescript-eslint/experimental-utils@5.9.0":
-  version "5.9.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.9.0.tgz#652762d37d6565ef07af285021b8347b6c79a827"
-  integrity sha512-ZnLVjBrf26dn7ElyaSKa6uDhqwvAi4jBBmHK1VxuFGPRAxhdi18ubQYSGA7SRiFiES3q9JiBOBHEBStOFkwD2g==
+"@typescript-eslint/experimental-utils@5.9.1":
+  version "5.9.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.9.1.tgz#8c407c4dd5ffe522329df6e4c9c2b52206d5f7f1"
+  integrity sha512-cb1Njyss0mLL9kLXgS/eEY53SZQ9sT519wpX3i+U457l2UXRDuo87hgKfgRazmu9/tQb0x2sr3Y0yrU+Zz0y+w==
   dependencies:
     "@types/json-schema" "^7.0.9"
-    "@typescript-eslint/scope-manager" "5.9.0"
-    "@typescript-eslint/types" "5.9.0"
-    "@typescript-eslint/typescript-estree" "5.9.0"
+    "@typescript-eslint/scope-manager" "5.9.1"
+    "@typescript-eslint/types" "5.9.1"
+    "@typescript-eslint/typescript-estree" "5.9.1"
     eslint-scope "^5.1.1"
     eslint-utils "^3.0.0"
 
 "@typescript-eslint/parser@^5.6.0":
-  version "5.9.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.9.0.tgz#fdbb08767a4caa6ca6ccfed5f9ffe9387f0c7d97"
-  integrity sha512-/6pOPz8yAxEt4PLzgbFRDpZmHnXCeZgPDrh/1DaVKOjvn/UPMlWhbx/gA96xRi2JxY1kBl2AmwVbyROUqys5xQ==
+  version "5.9.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.9.1.tgz#b114011010a87e17b3265ca715e16c76a9834cef"
+  integrity sha512-PLYO0AmwD6s6n0ZQB5kqPgfvh73p0+VqopQQLuNfi7Lm0EpfKyDalchpVwkE+81k5HeiRrTV/9w1aNHzjD7C4g==
   dependencies:
-    "@typescript-eslint/scope-manager" "5.9.0"
-    "@typescript-eslint/types" "5.9.0"
-    "@typescript-eslint/typescript-estree" "5.9.0"
+    "@typescript-eslint/scope-manager" "5.9.1"
+    "@typescript-eslint/types" "5.9.1"
+    "@typescript-eslint/typescript-estree" "5.9.1"
     debug "^4.3.2"
 
-"@typescript-eslint/scope-manager@5.9.0":
-  version "5.9.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.9.0.tgz#02dfef920290c1dcd7b1999455a3eaae7a1a3117"
-  integrity sha512-DKtdIL49Qxk2a8icF6whRk7uThuVz4A6TCXfjdJSwOsf+9ree7vgQWcx0KOyCdk0i9ETX666p4aMhrRhxhUkyg==
+"@typescript-eslint/scope-manager@5.9.1":
+  version "5.9.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.9.1.tgz#6c27be89f1a9409f284d95dfa08ee3400166fe69"
+  integrity sha512-8BwvWkho3B/UOtzRyW07ffJXPaLSUKFBjpq8aqsRvu6HdEuzCY57+ffT7QoV4QXJXWSU1+7g3wE4AlgImmQ9pQ==
   dependencies:
-    "@typescript-eslint/types" "5.9.0"
-    "@typescript-eslint/visitor-keys" "5.9.0"
+    "@typescript-eslint/types" "5.9.1"
+    "@typescript-eslint/visitor-keys" "5.9.1"
 
-"@typescript-eslint/type-utils@5.9.0":
-  version "5.9.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.9.0.tgz#fd5963ead04bc9b7af9c3a8e534d8d39f1ce5f93"
-  integrity sha512-uVCb9dJXpBrK1071ri5aEW7ZHdDHAiqEjYznF3HSSvAJXyrkxGOw2Ejibz/q6BXdT8lea8CMI0CzKNFTNI6TEQ==
+"@typescript-eslint/type-utils@5.9.1":
+  version "5.9.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.9.1.tgz#c6832ffe655b9b1fec642d36db1a262d721193de"
+  integrity sha512-tRSpdBnPRssjlUh35rE9ug5HrUvaB9ntREy7gPXXKwmIx61TNN7+l5YKgi1hMKxo5NvqZCfYhA5FvyuJG6X6vg==
   dependencies:
-    "@typescript-eslint/experimental-utils" "5.9.0"
+    "@typescript-eslint/experimental-utils" "5.9.1"
     debug "^4.3.2"
     tsutils "^3.21.0"
 
-"@typescript-eslint/types@5.9.0":
-  version "5.9.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.9.0.tgz#e5619803e39d24a03b3369506df196355736e1a3"
-  integrity sha512-mWp6/b56Umo1rwyGCk8fPIzb9Migo8YOniBGPAQDNC6C52SeyNGN4gsVwQTAR+RS2L5xyajON4hOLwAGwPtUwg==
+"@typescript-eslint/types@5.9.1":
+  version "5.9.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.9.1.tgz#1bef8f238a2fb32ebc6ff6d75020d9f47a1593c6"
+  integrity sha512-SsWegWudWpkZCwwYcKoDwuAjoZXnM1y2EbEerTHho19Hmm+bQ56QG4L4jrtCu0bI5STaRTvRTZmjprWlTw/5NQ==
 
-"@typescript-eslint/typescript-estree@5.9.0":
-  version "5.9.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.9.0.tgz#0e5c6f03f982931abbfbc3c1b9df5fbf92a3490f"
-  integrity sha512-kxo3xL2mB7XmiVZcECbaDwYCt3qFXz99tBSuVJR4L/sR7CJ+UNAPrYILILktGj1ppfZ/jNt/cWYbziJUlHl1Pw==
+"@typescript-eslint/typescript-estree@5.9.1":
+  version "5.9.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.9.1.tgz#d5b996f49476495070d2b8dd354861cf33c005d6"
+  integrity sha512-gL1sP6A/KG0HwrahVXI9fZyeVTxEYV//6PmcOn1tD0rw8VhUWYeZeuWHwwhnewnvEMcHjhnJLOBhA9rK4vmb8A==
   dependencies:
-    "@typescript-eslint/types" "5.9.0"
-    "@typescript-eslint/visitor-keys" "5.9.0"
+    "@typescript-eslint/types" "5.9.1"
+    "@typescript-eslint/visitor-keys" "5.9.1"
     debug "^4.3.2"
     globby "^11.0.4"
     is-glob "^4.0.3"
     semver "^7.3.5"
     tsutils "^3.21.0"
 
-"@typescript-eslint/visitor-keys@5.9.0":
-  version "5.9.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.9.0.tgz#7585677732365e9d27f1878150fab3922784a1a6"
-  integrity sha512-6zq0mb7LV0ThExKlecvpfepiB+XEtFv/bzx7/jKSgyXTFD7qjmSu1FoiS0x3OZaiS+UIXpH2vd9O89f02RCtgw==
+"@typescript-eslint/visitor-keys@5.9.1":
+  version "5.9.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.9.1.tgz#f52206f38128dd4f675cf28070a41596eee985b7"
+  integrity sha512-Xh37pNz9e9ryW4TVdwiFzmr4hloty8cFj8GTWMXh3Z8swGwyQWeCcNgF0hm6t09iZd6eiZmIf4zHedQVP6TVtg==
   dependencies:
-    "@typescript-eslint/types" "5.9.0"
+    "@typescript-eslint/types" "5.9.1"
     eslint-visitor-keys "^3.0.0"
 
 "@webassemblyjs/ast@1.11.1":
@@ -1744,6 +1744,14 @@ babel-plugin-polyfill-corejs3@^0.4.0:
     "@babel/helper-define-polyfill-provider" "^0.3.0"
     core-js-compat "^3.18.0"
 
+babel-plugin-polyfill-corejs3@^0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.0.tgz#f81371be3fe499d39e074e272a1ef86533f3d268"
+  integrity sha512-Hcrgnmkf+4JTj73GbK3bBhlVPiLL47owUAnoJIf69Hakl3q+KfodbDXiZWGMM7iqCZTxCG3Z2VRfPNYES4rXqQ==
+  dependencies:
+    "@babel/helper-define-polyfill-provider" "^0.3.0"
+    core-js-compat "^3.20.0"
+
 babel-plugin-polyfill-regenerator@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.0.tgz#9ebbcd7186e1a33e21c5e20cae4e7983949533be"
@@ -2101,6 +2109,11 @@ cidr-regex@1.0.6:
   resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1"
   integrity sha1-dKv9YZ3zcLnVSrFEdVaOl91kwME=
 
+classnames@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
+  integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
+
 clean-stack@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
@@ -2252,9 +2265,9 @@ colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.16:
   integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==
 
 colors@^1.1.2:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.1.tgz#57e798423c5fa9182f7531e4c587bfce9483175d"
-  integrity sha512-urbBmMVnD1vk0mUwCpnWv06P3f16EF+RMTtIXTkylJk5mAdfrMepu9B3hhSnL8DGkc1Ra6pENJHrXTKvcAZ0wA==
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
+  integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
 
 columnify@~1.5.4:
   version "1.5.4"
@@ -2414,7 +2427,7 @@ copy-webpack-plugin@^10.0.0:
     schema-utils "^4.0.0"
     serialize-javascript "^6.0.0"
 
-core-js-compat@^3.18.0, core-js-compat@^3.19.1:
+core-js-compat@^3.18.0, core-js-compat@^3.19.1, core-js-compat@^3.20.0:
   version "3.20.2"
   resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.20.2.tgz#d1ff6936c7330959b46b2e08b122a8b14e26140b"
   integrity sha512-qZEzVQ+5Qh6cROaTPFLNS4lkvQ6mBzE3R6A6EEpssj7Zr2egMHgsy4XapdifqJDGC9CBiNv7s+ejI96rLNQFdg==
@@ -2531,7 +2544,7 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3:
   dependencies:
     ms "2.1.2"
 
-debuglog@^1.0.1:
+debuglog@*, debuglog@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
   integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
@@ -2764,9 +2777,9 @@ ee-first@1.1.1:
   integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
 
 electron-to-chromium@^1.4.17:
-  version "1.4.38"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.38.tgz#10ea58d73d36b13e78d5024f3b74a352d3958d01"
-  integrity sha512-WhHt3sZazKj0KK/UpgsbGQnUUoFeAHVishzHFExMxagpZgjiGYSC9S0ZlbhCfSH2L2i+2A1yyqOIliTctMx7KQ==
+  version "1.4.41"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.41.tgz#0b2e126796e7fafb9fd71e29304468b9d0af5d65"
+  integrity sha512-VQEXEJc+8rJIva85H8EPtB5Ux9g8TzkNGBanqphM9ZWMZ34elueKJ+5g+BPhz3Lk8gkujfQRcIZ+fpA0btUIuw==
 
 emoji-regex@^7.0.1:
   version "7.0.3"
@@ -3314,9 +3327,9 @@ flush-write-stream@^1.0.0:
     readable-stream "^2.3.6"
 
 follow-redirects@^1.0.0:
-  version "1.14.6"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd"
-  integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==
+  version "1.14.7"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
+  integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
 
 forever-agent@~0.6.1:
   version "0.6.1"
@@ -3861,9 +3874,9 @@ husky@^7.0.4:
   integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==
 
 i18next@^21.5.4:
-  version "21.6.5"
-  resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.6.5.tgz#0031c62314f1a8a5e2911eecc6cdc9d044755634"
-  integrity sha512-1oimhzFEpkmxpY2yDyghdycyA1bCKrh9zf04qiB2HytKJCqlrA5e8JfL6KyK/oZvZABLP0GohsZ+tvhHmd+OTA==
+  version "21.6.6"
+  resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.6.6.tgz#65db9a32e4746d145b3ab3f12c59fc02ad7b5101"
+  integrity sha512-K1Pw8K+nHVco56PO6UrqNq4K/ZVbb2eqBQwPqmzYDm4tGQYXBjdz8jrnvuNvV5STaE8oGpWKQMxHOvh2zhVE7Q==
   dependencies:
     "@babel/runtime" "^7.12.0"
 
@@ -3996,7 +4009,7 @@ import-sort@^6.0.0:
     is-builtin-module "^3.0.0"
     resolve "^1.8.1"
 
-imurmurhash@^0.1.4:
+imurmurhash@*, imurmurhash@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
   integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
@@ -4785,6 +4798,11 @@ lockfile@^1.0.4:
   dependencies:
     signal-exit "^3.0.2"
 
+lodash._baseindexof@*:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c"
+  integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=
+
 lodash._baseuniq@~4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@@ -4793,11 +4811,33 @@ lodash._baseuniq@~4.6.0:
     lodash._createset "~4.0.0"
     lodash._root "~3.0.0"
 
+lodash._bindcallback@*:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
+  integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4=
+
+lodash._cacheindexof@*:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92"
+  integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=
+
+lodash._createcache@*:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093"
+  integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=
+  dependencies:
+    lodash._getnative "^3.0.0"
+
 lodash._createset@~4.0.0:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
   integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
 
+lodash._getnative@*, lodash._getnative@^3.0.0:
+  version "3.9.1"
+  resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
+  integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
+
 lodash._root@~3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
@@ -4823,6 +4863,11 @@ lodash.pick@^4.4.0:
   resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
   integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
 
+lodash.restparam@*:
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
+  integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=
+
 lodash.union@~4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
@@ -5238,9 +5283,9 @@ mute-stream@~0.0.4:
   integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
 
 nanoid@^3.1.30:
-  version "3.1.30"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
-  integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
+  version "3.1.31"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.31.tgz#f5b58a1ce1b7604da5f0605757840598d8974dc6"
+  integrity sha512-ZivnJm0o9bb13p2Ot5CpgC2rQdzB9Uxm/mFZweqm5eMViqOJe3PV6LU2E30SiLgheesmcPrjquqraoolONSA0A==
 
 natural-compare@^1.4.0:
   version "1.4.0"
@@ -6276,9 +6321,9 @@ qs@6.9.6:
   integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==
 
 qs@~6.5.2:
-  version "6.5.2"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
-  integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+  version "6.5.3"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
+  integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
 
 query-string@^6.1.0:
   version "6.14.1"
@@ -6428,7 +6473,7 @@ readable-stream@~1.1.10:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readdir-scoped-modules@^1.0.0:
+readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309"
   integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==
@@ -6701,9 +6746,9 @@ run-queue@^1.0.0, run-queue@^1.0.3:
     aproba "^1.1.1"
 
 rxjs@^7.4.0, rxjs@^7.5.1:
-  version "7.5.1"
-  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.1.tgz#af73df343cbcab37628197f43ea0c8256f54b157"
-  integrity sha512-KExVEeZWxMZnZhUZtsJcFwz8IvPvgu4G2Z2QyqjZQzUGr32KDYuSxrEYO4w3tFFNbfLozcrKUTvTPi+E9ywJkQ==
+  version "7.5.2"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.2.tgz#11e4a3a1dfad85dbf7fb6e33cbba17668497490b"
+  integrity sha512-PwDt186XaL3QN5qXj/H9DGyHhP3/RYYgZZwqBv9Tv8rsAaiwFH1IsJJlcgD37J7UW5a6O67qX0KWKS3/pu0m4w==
   dependencies:
     tslib "^2.1.0"
 
@@ -6773,9 +6818,9 @@ select-hose@^2.0.0:
   integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
 
 selfsigned@^1.10.11:
-  version "1.10.11"
-  resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.11.tgz#24929cd906fe0f44b6d01fb23999a739537acbe9"
-  integrity sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==
+  version "1.10.14"
+  resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.14.tgz#ee51d84d9dcecc61e07e4aba34f229ab525c1574"
+  integrity sha512-lkjaiAye+wBZDCBsu5BGi0XiLRxeUlsGod5ZP924CRSEoGuZAw/f7y9RKu28rwTfiHVhdavhB0qH0INV6P1lEA==
   dependencies:
     node-forge "^0.10.0"
 
@@ -7892,9 +7937,9 @@ webpack-node-externals@^3.0.0:
   integrity sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==
 
 webpack-sources@^3.2.2:
-  version "3.2.2"
-  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.2.tgz#d88e3741833efec57c4c789b6010db9977545260"
-  integrity sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==
+  version "3.2.3"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
+  integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
 
 webpack@5.65.0:
   version "5.65.0"