--- /dev/null
+import { showScores } from "@utils/app";
+import { numToSI } from "@utils/helpers";
+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 className="input-group input-group-sm w-auto">
+ <button
+ className={`btn btn-sm btn-animate btn-outline-primary rounded-start py-0 ${
+ 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>
+ <span className="input-group-text small py-0">
+ {numToSI(this.props.counts.score)}
+ </span>
+ {this.props.enableDownvotes && (
+ <button
+ className={`btn btn-sm btn-animate btn-outline-primary rounded-end py-0 ${
+ 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="ms-2">
+ {numToSI(this.props.counts.downvotes)}
+ </span>
+ )}
+ </>
+ )}
+ </button>
+ )}
+ </div>
+ </>
+ );
+ }
+}
+
+export class VoteButtons extends Component<VotesProps, VotesState> {
+ state: VotesState = {
+ 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>
+ );
+ }
+}
-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 {
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";
);
}
- 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 (
<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()}
return (
<>
{this.saveButton}
- {this.crossPostButton}
{/**
* If there is a URL, or if the post has a body and we were told not to
</button>
<ul className="dropdown-menu" id="advancedButtonsDropdown">
+ <li>{this.crossPostButton}</li>
+ <li>
+ <hr className="dropdown-divider" />
+ </li>
+
{!this.myPost ? (
<>
<li>{this.reportButton}</li>
: 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
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",
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>
);
}
<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 />
{this.postTitleLine()}
</div>
<div className="col-4">
- {/* Post body prev or thumbnail */}
+ {/* Post thumbnail */}
{!this.state.imageExpanded && this.thumbnail()}
</div>
</div>
);
}
- showMobilePreview() {
+ bodyPreview() {
const { body, id } = this.postView.post;
return !this.showBody && body ? (
{/* 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()}
{/* 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>
<div className="col-12">
{this.postTitleLine()}
{this.createdLine()}
+ {this.bodyPreview()}
{this.commentsLine()}
{this.duplicatesLine()}
{this.userActionsLine()}