-import { myAuthRequired, newVote, showScores } from "@utils/app";
+import { myAuthRequired } 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 {
TransferCommunity,
} from "lemmy-js-client";
import { relTags } from "../../config";
-import { BanType, PostFormParams, PurgeType, VoteType } from "../../interfaces";
+import {
+ BanType,
+ PostFormParams,
+ PurgeType,
+ VoteContentType,
+} from "../../interfaces";
import { mdNoImages, mdToHtml, mdToHtmlInline } from "../../markdown";
import { I18NextService, UserService } from "../../services";
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";
showBody: boolean;
showReportDialog: boolean;
reportReason?: string;
- upvoteLoading: boolean;
- downvoteLoading: boolean;
reportLoading: boolean;
blockLoading: boolean;
lockLoading: boolean;
showMoreMobile: false,
showBody: false,
showReportDialog: false,
- upvoteLoading: false,
- downvoteLoading: false,
purgeLoading: false,
reportLoading: false,
blockLoading: false,
componentWillReceiveProps(nextProps: PostListingProps) {
if (this.props !== nextProps) {
this.setState({
- upvoteLoading: false,
- downvoteLoading: false,
purgeLoading: false,
reportLoading: false,
blockLoading: false,
</a>
</div>
<div className="my-2 d-block d-sm-none">
- <a
- className="d-inline-block"
+ <button
+ type="button"
+ className="p-0 border-0 bg-transparent d-inline-block"
onClick={linkEvent(this, this.handleImageExpandClick)}
>
<PictrsImage src={this.imageSrc} />
- </a>
+ </button>
</div>
</>
);
const { post } = this.postView;
const { url } = post;
+ // if direct video link
if (url && isVideo(url)) {
return (
<div className="embed-responsive mt-3">
);
}
+ // if embedded video link
+ if (url && post.embed_video_url) {
+ return (
+ <div className="ratio ratio-16x9">
+ <iframe
+ allowFullScreen
+ className="post-metadata-iframe"
+ src={post.embed_video_url}
+ title={post.embed_title}
+ ></iframe>
+ </div>
+ );
+ }
+
return <></>;
}
</a>
);
} else if (url) {
- if (!this.props.hideImage && isVideo(url)) {
+ if ((!this.props.hideImage && isVideo(url)) || post.embed_video_url) {
return (
<a
className="text-body"
const post_view = this.postView;
return (
<span className="small">
- <PersonListing person={post_view.creator} />
+ <PersonListing person={post_view.creator} muted={true} />
{this.creatorIsMod_ && (
<span className="mx-1 badge text-bg-light">
{I18NextService.i18n.t("mod")}
);
}
- 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 (
<Link
className={`d-inline ${
!post.featured_community && !post.featured_local
- ? "text-body"
- : "text-primary"
+ ? "link-dark"
+ : "link-primary"
}`}
to={`/post/${post.id}`}
title={I18NextService.i18n.t("comments")}
<a
className={
!post.featured_community && !post.featured_local
- ? "text-body"
- : "text-primary"
+ ? "link-dark"
+ : "link-primary"
}
href={url}
title={url}
)}
{post.deleted && (
<small
- className="unselectable pointer ms-2 text-muted font-italic"
+ className="unselectable pointer ms-2 text-muted fst-italic"
data-tippy-content={I18NextService.i18n.t("deleted")}
>
<Icon icon="trash" classes="icon-inline text-danger" />
)}
{post.locked && (
<small
- className="unselectable pointer ms-2 text-muted font-italic"
+ className="unselectable pointer ms-2 text-muted fst-italic"
data-tippy-content={I18NextService.i18n.t("locked")}
>
<Icon icon="lock" classes="icon-inline text-danger" />
)}
{post.featured_community && (
<small
- className="unselectable pointer ms-2 text-muted font-italic"
+ className="unselectable pointer ms-2 text-muted fst-italic"
data-tippy-content={I18NextService.i18n.t(
"featured_in_community"
)}
)}
{post.featured_local && (
<small
- className="unselectable pointer ms-2 text-muted font-italic"
+ className="unselectable pointer ms-2 text-muted fst-italic"
data-tippy-content={I18NextService.i18n.t("featured_in_local")}
aria-label={I18NextService.i18n.t("featured_in_local")}
>
const url = post.url;
return (
- <p className="d-flex text-muted align-items-center gap-1 small m-0">
+ <p className="small m-0">
{url && !(hostname(url) === getExternalHost()) && (
<a
- className="text-muted font-italic"
+ className="fst-italic link-dark link-opacity-75 link-opacity-100-hover"
href={url}
title={url}
rel={relTags}
<Icon icon="fedilink" inline />
</a>
)}
- {mobile && !this.props.viewOnly && this.mobileVotes}
+ {mobile && !this.props.viewOnly && (
+ <VoteButtonsCompact
+ voteContentType={VoteContentType.Post}
+ id={this.postView.post.id}
+ onVote={this.props.onPostVote}
+ enableDownvotes={this.props.enableDownvotes}
+ counts={this.postView.counts}
+ my_vote={this.postView.my_vote}
+ />
+ )}
{UserService.Instance.myUserInfo &&
!this.props.viewOnly &&
this.postActions()}
data-tippy-content={I18NextService.i18n.t("more")}
data-bs-toggle="dropdown"
aria-expanded="false"
- aria-controls="advancedButtonsDropdown"
+ aria-controls={`advancedButtonsDropdown${post.id}`}
aria-label={I18NextService.i18n.t("more")}
>
<Icon icon="more-vertical" inline />
</button>
- <ul className="dropdown-menu" id="advancedButtonsDropdown">
+ <ul
+ className="dropdown-menu"
+ id={`advancedButtonsDropdown${post.id}`}
+ >
{!this.myPost ? (
<>
<li>{this.reportButton}</li>
<Icon icon="message-square" classes="me-1" inline />
{post_view.counts.comments}
{this.unreadCount && (
- <span className="text-muted fst-italic">
- ({this.unreadCount} {I18NextService.i18n.t("new")})
- </span>
+ <>
+ {" "}
+ <span className="fst-italic">
+ ({this.unreadCount} {I18NextService.i18n.t("new")})
+ </span>
+ </>
)}
</Link>
);
: 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
<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 />
value={this.state.banReason}
onInput={linkEvent(this, this.handleModBanReasonChange)}
/>
- <label className="col-form-label" htmlFor={`mod-ban-expires`}>
+ <label className="col-form-label" htmlFor="mod-ban-expires">
{I18NextService.i18n.t("expires")}
</label>
<input
type="number"
- id={`mod-ban-expires`}
+ id="mod-ban-expires"
className="form-control me-2"
placeholder={I18NextService.i18n.t("number_of_days")}
value={this.state.banExpireDays}
{this.postTitleLine()}
</div>
<div className="col-4">
- {/* Post body prev or thumbnail */}
+ {/* Post thumbnail */}
{!this.state.imageExpanded && this.thumbnail()}
</div>
</div>
{/* The larger view*/}
<div className="d-none d-sm-block">
<article className="row post-container">
- {!this.props.viewOnly && this.voteBar()}
- <div className="col-sm-2 pe-0 post-media">
- <div className="">{this.thumbnail()}</div>
- </div>
- <div className="col-12 col-sm-9">
+ {!this.props.viewOnly && (
+ <div className="col flex-grow-0">
+ <VoteButtons
+ voteContentType={VoteContentType.Post}
+ id={this.postView.post.id}
+ onVote={this.props.onPostVote}
+ enableDownvotes={this.props.enableDownvotes}
+ counts={this.postView.counts}
+ my_vote={this.postView.my_vote}
+ />
+ </div>
+ )}
+ <div className="col flex-grow-1">
<div className="row">
- <div className="col-12">
+ <div className="col-sm-3 col-lg-2 pe-0 post-media">
+ <div className="">{this.thumbnail()}</div>
+ </div>
+ <div className="col-12 col-sm-9 col-lg-10">
{this.postTitleLine()}
{this.createdLine()}
{this.showBodyPreview()}
setupTippy();
}
- handleUpvote(i: PostListing) {
- i.setState({ upvoteLoading: true });
- i.props.onPostVote({
- post_id: i.postView.post.id,
- score: newVote(VoteType.Upvote, i.props.post_view.my_vote),
- auth: myAuthRequired(),
- });
- }
-
- handleDownvote(i: PostListing) {
- i.setState({ downvoteLoading: true });
- i.props.onPostVote({
- post_id: i.postView.post.id,
- score: newVote(VoteType.Downvote, i.props.post_view.my_vote),
- auth: myAuthRequired(),
- });
- }
-
get pointsTippy(): string {
const points = I18NextService.i18n.t("number_of_points", {
count: Number(this.postView.counts.score),