]> Untitled Git - lemmy-ui.git/commitdiff
Merge remote-tracking branch 'origin/main' into feat/vote-components
authorJay Sitter <jay@jaysitter.com>
Thu, 22 Jun 2023 20:56:08 +0000 (16:56 -0400)
committerJay Sitter <jay@jaysitter.com>
Thu, 22 Jun 2023 20:56:08 +0000 (16:56 -0400)
* origin/main: (26 commits)
  Adding jsit to codeowners.
  Cleanup, only check for /u/ if /c/ and /m/ checks fail
  Rename function to be more generic, since it parses users
  Typescript linter fixes
  bandaid fix our video embeds
  Remove pipe from community link regex
  Add missing classes
  Use shorter regex in community link parser
  Move regex pattern to config
  Update community link markdown parsing
  Fix avatar alignment issue (#1475)
  Omit user-scalable to use default
  Update getHttpBase dependency reference
  Enable users to zoom on mobile
  rethink it a bit
  rethink it a bit
  add fallback style tag
  Add community link class
  prettier
  Add local community link parser plugin for Markdown-It
  ...

src/shared/components/common/vote-buttons.tsx [new file with mode: 0644]
src/shared/components/post/post-listing.tsx

diff --git a/src/shared/components/common/vote-buttons.tsx b/src/shared/components/common/vote-buttons.tsx
new file mode 100644 (file)
index 0000000..dce46f5
--- /dev/null
@@ -0,0 +1,207 @@
+import { showScores } from "@utils/app";
+import { numToSI } from "@utils/helpers";
+import classNames from "classnames";
+import { Component, linkEvent } from "inferno";
+import { CommentAggregates, PostAggregates } from "lemmy-js-client";
+import { I18NextService } from "../../services";
+import { Icon, Spinner } from "../common/icon";
+import { PostListing } from "../post/post-listing";
+
+interface VoteButtonsProps {
+  postListing: PostListing;
+  enableDownvotes?: boolean;
+  upvoteLoading?: boolean;
+  downvoteLoading?: boolean;
+  handleUpvote: (i: PostListing) => void;
+  handleDownvote: (i: PostListing) => void;
+  counts: CommentAggregates | PostAggregates;
+  my_vote?: number;
+}
+
+interface VoteButtonsState {
+  upvoteLoading: boolean;
+  downvoteLoading: boolean;
+}
+
+export class VoteButtonsCompact extends Component<
+  VoteButtonsProps,
+  VoteButtonsState
+> {
+  state: VoteButtonsState = {
+    upvoteLoading: false,
+    downvoteLoading: false,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+  }
+
+  get pointsTippy(): string {
+    const points = I18NextService.i18n.t("number_of_points", {
+      count: Number(this.props.counts.score),
+      formattedCount: Number(this.props.counts.score),
+    });
+
+    const upvotes = I18NextService.i18n.t("number_of_upvotes", {
+      count: Number(this.props.counts.upvotes),
+      formattedCount: Number(this.props.counts.upvotes),
+    });
+
+    const downvotes = I18NextService.i18n.t("number_of_downvotes", {
+      count: Number(this.props.counts.downvotes),
+      formattedCount: Number(this.props.counts.downvotes),
+    });
+
+    return `${points} • ${upvotes} • ${downvotes}`;
+  }
+
+  get tippy() {
+    return showScores() ? { "data-tippy-content": this.pointsTippy } : {};
+  }
+
+  render() {
+    return (
+      <div>
+        <button
+          className={`btn-animate btn py-0 px-1 ${
+            this.props.my_vote === 1 ? "text-info" : "text-muted"
+          }`}
+          {...this.tippy}
+          onClick={linkEvent(this.props.postListing, this.props.handleUpvote)}
+          aria-label={I18NextService.i18n.t("upvote")}
+          aria-pressed={this.props.my_vote === 1}
+        >
+          {this.state.upvoteLoading ? (
+            <Spinner />
+          ) : (
+            <>
+              <Icon icon="arrow-up1" classes="icon-inline small" />
+              {showScores() && (
+                <span className="ms-2">
+                  {numToSI(this.props.counts.upvotes)}
+                </span>
+              )}
+            </>
+          )}
+        </button>
+        {this.props.enableDownvotes && (
+          <button
+            className={`ms-2 btn-animate btn py-0 px-1 ${
+              this.props.my_vote === -1 ? "text-danger" : "text-muted"
+            }`}
+            onClick={linkEvent(
+              this.props.postListing,
+              this.props.handleDownvote
+            )}
+            {...this.tippy}
+            aria-label={I18NextService.i18n.t("downvote")}
+            aria-pressed={this.props.my_vote === -1}
+          >
+            {this.state.downvoteLoading ? (
+              <Spinner />
+            ) : (
+              <>
+                <Icon icon="arrow-down1" classes="icon-inline small" />
+                {showScores() && (
+                  <span
+                    className={classNames("ms-2", {
+                      invisible: this.props.counts.downvotes === 0,
+                    })}
+                  >
+                    {numToSI(this.props.counts.downvotes)}
+                  </span>
+                )}
+              </>
+            )}
+          </button>
+        )}
+      </div>
+    );
+  }
+}
+
+export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
+  state: VoteButtonsState = {
+    upvoteLoading: false,
+    downvoteLoading: false,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+  }
+
+  get pointsTippy(): string {
+    const points = I18NextService.i18n.t("number_of_points", {
+      count: Number(this.props.counts.score),
+      formattedCount: Number(this.props.counts.score),
+    });
+
+    const upvotes = I18NextService.i18n.t("number_of_upvotes", {
+      count: Number(this.props.counts.upvotes),
+      formattedCount: Number(this.props.counts.upvotes),
+    });
+
+    const downvotes = I18NextService.i18n.t("number_of_downvotes", {
+      count: Number(this.props.counts.downvotes),
+      formattedCount: Number(this.props.counts.downvotes),
+    });
+
+    return `${points} • ${upvotes} • ${downvotes}`;
+  }
+
+  get tippy() {
+    return showScores() ? { "data-tippy-content": this.pointsTippy } : {};
+  }
+
+  render() {
+    return (
+      <div className={`vote-bar col-1 pe-0 small text-center`}>
+        <button
+          className={`btn-animate btn btn-link p-0 ${
+            this.props.my_vote == 1 ? "text-info" : "text-muted"
+          }`}
+          onClick={linkEvent(this.props.postListing, this.props.handleUpvote)}
+          data-tippy-content={I18NextService.i18n.t("upvote")}
+          aria-label={I18NextService.i18n.t("upvote")}
+          aria-pressed={this.props.my_vote === 1}
+        >
+          {this.state.upvoteLoading ? (
+            <Spinner />
+          ) : (
+            <Icon icon="arrow-up1" classes="upvote" />
+          )}
+        </button>
+        {showScores() ? (
+          <div
+            className={`unselectable pointer text-muted px-1 post-score`}
+            data-tippy-content={this.pointsTippy}
+          >
+            {numToSI(this.props.counts.score)}
+          </div>
+        ) : (
+          <div className="p-1"></div>
+        )}
+        {this.props.enableDownvotes && (
+          <button
+            className={`btn-animate btn btn-link p-0 ${
+              this.props.my_vote == -1 ? "text-danger" : "text-muted"
+            }`}
+            onClick={linkEvent(
+              this.props.postListing,
+              this.props.handleDownvote
+            )}
+            data-tippy-content={I18NextService.i18n.t("downvote")}
+            aria-label={I18NextService.i18n.t("downvote")}
+            aria-pressed={this.props.my_vote === -1}
+          >
+            {this.state.downvoteLoading ? (
+              <Spinner />
+            ) : (
+              <Icon icon="arrow-down1" classes="downvote" />
+            )}
+          </button>
+        )}
+      </div>
+    );
+  }
+}
index 93b8fff7c3ac2a3fe649bf98e8a857eae289eaf8..a913f534069ade1cde7edc50cc306329a2ab5e49 100644 (file)
@@ -1,11 +1,10 @@
-import { myAuthRequired, newVote, showScores } from "@utils/app";
+import { myAuthRequired, newVote } from "@utils/app";
 import { canShare, share } from "@utils/browser";
 import { getExternalHost, getHttpBase } from "@utils/env";
 import {
   capitalizeFirstLetter,
   futureDaysToUnixTime,
   hostname,
-  numToSI,
 } from "@utils/helpers";
 import { isImage, isVideo } from "@utils/media";
 import {
@@ -51,6 +50,7 @@ import { setupTippy } from "../../tippy";
 import { Icon, PurgeWarning, Spinner } from "../common/icon";
 import { MomentTime } from "../common/moment-time";
 import { PictrsImage } from "../common/pictrs-image";
+import { VoteButtons, VoteButtonsCompact } from "../common/vote-buttons";
 import { CommunityLink } from "../community/community-link";
 import { PersonListing } from "../person/person-listing";
 import { MetadataCard } from "./metadata-card";
@@ -430,55 +430,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     );
   }
 
-  voteBar() {
-    return (
-      <div className={`vote-bar col-1 pe-0 small text-center`}>
-        <button
-          className={`btn-animate btn btn-link p-0 ${
-            this.postView.my_vote == 1 ? "text-info" : "text-muted"
-          }`}
-          onClick={linkEvent(this, this.handleUpvote)}
-          data-tippy-content={I18NextService.i18n.t("upvote")}
-          aria-label={I18NextService.i18n.t("upvote")}
-          aria-pressed={this.postView.my_vote === 1}
-        >
-          {this.state.upvoteLoading ? (
-            <Spinner />
-          ) : (
-            <Icon icon="arrow-up1" classes="upvote" />
-          )}
-        </button>
-        {showScores() ? (
-          <div
-            className={`unselectable pointer text-muted px-1 post-score`}
-            data-tippy-content={this.pointsTippy}
-          >
-            {numToSI(this.postView.counts.score)}
-          </div>
-        ) : (
-          <div className="p-1"></div>
-        )}
-        {this.props.enableDownvotes && (
-          <button
-            className={`btn-animate btn btn-link p-0 ${
-              this.postView.my_vote == -1 ? "text-danger" : "text-muted"
-            }`}
-            onClick={linkEvent(this, this.handleDownvote)}
-            data-tippy-content={I18NextService.i18n.t("downvote")}
-            aria-label={I18NextService.i18n.t("downvote")}
-            aria-pressed={this.postView.my_vote === -1}
-          >
-            {this.state.downvoteLoading ? (
-              <Spinner />
-            ) : (
-              <Icon icon="arrow-down1" classes="downvote" />
-            )}
-          </button>
-        )}
-      </div>
-    );
-  }
-
   get postLink() {
     const post = this.postView.post;
     return (
@@ -658,7 +609,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             <Icon icon="fedilink" inline />
           </a>
         )}
-        {mobile && !this.props.viewOnly && this.mobileVotes}
+        {mobile && !this.props.viewOnly && (
+          <VoteButtonsCompact
+            postListing={this}
+            enableDownvotes={this.props.enableDownvotes}
+            handleUpvote={this.handleUpvote}
+            handleDownvote={this.handleDownvote}
+            counts={this.postView.counts}
+            my_vote={this.postView.my_vote}
+          />
+        )}
         {UserService.Instance.myUserInfo &&
           !this.props.viewOnly &&
           this.postActions()}
@@ -696,7 +656,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     return (
       <>
         {this.saveButton}
-        {this.crossPostButton}
 
         {/**
          * If there is a URL, or if the post has a body and we were told not to
@@ -721,6 +680,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           </button>
 
           <ul className="dropdown-menu" id="advancedButtonsDropdown">
+            <li>{this.crossPostButton}</li>
+            <li>
+              <hr className="dropdown-divider" />
+            </li>
+
             {!this.myPost ? (
               <>
                 <li>{this.reportButton}</li>
@@ -787,69 +751,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       : pv.unread_comments;
   }
 
-  get mobileVotes() {
-    // TODO: make nicer
-    const tippy = showScores()
-      ? { "data-tippy-content": this.pointsTippy }
-      : {};
-    return (
-      <>
-        <div>
-          <button
-            className={`btn-animate btn py-0 px-1 ${
-              this.postView.my_vote === 1 ? "text-info" : "text-muted"
-            }`}
-            {...tippy}
-            onClick={linkEvent(this, this.handleUpvote)}
-            aria-label={I18NextService.i18n.t("upvote")}
-            aria-pressed={this.postView.my_vote === 1}
-          >
-            {this.state.upvoteLoading ? (
-              <Spinner />
-            ) : (
-              <>
-                <Icon icon="arrow-up1" classes="icon-inline small" />
-                {showScores() && (
-                  <span className="ms-2">
-                    {numToSI(this.postView.counts.upvotes)}
-                  </span>
-                )}
-              </>
-            )}
-          </button>
-          {this.props.enableDownvotes && (
-            <button
-              className={`ms-2 btn-animate btn py-0 px-1 ${
-                this.postView.my_vote === -1 ? "text-danger" : "text-muted"
-              }`}
-              onClick={linkEvent(this, this.handleDownvote)}
-              {...tippy}
-              aria-label={I18NextService.i18n.t("downvote")}
-              aria-pressed={this.postView.my_vote === -1}
-            >
-              {this.state.downvoteLoading ? (
-                <Spinner />
-              ) : (
-                <>
-                  <Icon icon="arrow-down1" classes="icon-inline small" />
-                  {showScores() && (
-                    <span
-                      className={classNames("ms-2", {
-                        invisible: this.postView.counts.downvotes === 0,
-                      })}
-                    >
-                      {numToSI(this.postView.counts.downvotes)}
-                    </span>
-                  )}
-                </>
-              )}
-            </button>
-          )}
-        </div>
-      </>
-    );
-  }
-
   get saveButton() {
     const saved = this.postView.saved;
     const label = saved
@@ -878,7 +779,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   get crossPostButton() {
     return (
       <Link
-        className="btn btn-sm btn-animate text-muted py-0"
+        className="btn btn-sm d-flex align-items-center rounded-0 dropdown-item"
         to={{
           /* Empty string properties are required to satisfy type*/
           pathname: "/create_post",
@@ -891,7 +792,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         data-tippy-content={I18NextService.i18n.t("cross_post")}
         aria-label={I18NextService.i18n.t("cross_post")}
       >
-        <Icon icon="copy" inline />
+        <Icon classes="me-1" icon="copy" inline />
+        {I18NextService.i18n.t("cross_post")}
       </Link>
     );
   }
@@ -948,7 +850,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       <button
         className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
         onClick={linkEvent(this, this.handleDeleteClick)}
-        aria-label={label}
       >
         {this.state.deleteLoading ? (
           <Spinner />
@@ -1451,7 +1352,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           {this.postTitleLine()}
         </div>
         <div className="col-4">
-          {/* Post body prev or thumbnail */}
+          {/* Post thumbnail */}
           {!this.state.imageExpanded && this.thumbnail()}
         </div>
       </div>
@@ -1460,7 +1361,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     );
   }
 
-  showMobilePreview() {
+  bodyPreview() {
     const { body, id } = this.postView.post;
 
     return !this.showBody && body ? (
@@ -1484,10 +1385,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               {/* If it has a thumbnail, do a right aligned thumbnail */}
               {this.mobileThumbnail()}
 
-              {/* Show a preview of the post body */}
-              {this.showMobilePreview()}
-
-              {this.commentsLine(true)}
+              <div className="mt-2">
+                {this.bodyPreview()}
+                {this.commentsLine(true)}
+              </div>
               {this.userActionsLine()}
               {this.duplicatesLine()}
               {this.removeAndBanDialogs()}
@@ -1498,7 +1399,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         {/* The larger view*/}
         <div className="d-none d-sm-block">
           <article className="row post-container">
-            {!this.props.viewOnly && this.voteBar()}
+            {!this.props.viewOnly && (
+              <VoteButtons
+                postListing={this}
+                enableDownvotes={this.props.enableDownvotes}
+                handleUpvote={this.handleUpvote}
+                handleDownvote={this.handleDownvote}
+                counts={this.postView.counts}
+                my_vote={this.postView.my_vote}
+              />
+            )}
             <div className="col-sm-2 pe-0 post-media">
               <div className="">{this.thumbnail()}</div>
             </div>
@@ -1507,6 +1417,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 <div className="col-12">
                   {this.postTitleLine()}
                   {this.createdLine()}
+                  {this.bodyPreview()}
                   {this.commentsLine()}
                   {this.duplicatesLine()}
                   {this.userActionsLine()}