]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/post/post-listing.tsx
Add open links in new tab (#2032)
[lemmy-ui.git] / src / shared / components / post / post-listing.tsx
index bc853b37f846594aaf1c2f044a3034eb6799c118..c92663501e474ecae19526a25c55a2f089e3c3ef 100644 (file)
@@ -1,4 +1,4 @@
-import { myAuthRequired } from "@utils/app";
+import { myAuth, myAuthRequired } from "@utils/app";
 import { canShare, share } from "@utils/browser";
 import { getExternalHost, getHttpBase } from "@utils/env";
 import {
@@ -34,6 +34,7 @@ import {
   FeaturePost,
   Language,
   LockPost,
+  MarkPostAsRead,
   PersonView,
   PostView,
   PurgePerson,
@@ -130,6 +131,7 @@ interface PostListingProps {
   onAddModToCommunity(form: AddModToCommunity): void;
   onAddAdmin(form: AddAdmin): void;
   onTransferCommunity(form: TransferCommunity): void;
+  onMarkPostAsRead(form: MarkPostAsRead): void;
 }
 
 export class PostListing extends Component<PostListingProps, PostListingState> {
@@ -310,16 +312,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     const url = post.url;
     const thumbnail = post.thumbnail_url;
 
-    if (url && isImage(url)) {
-      if (url.includes("pictrs")) {
-        return url;
-      } else if (thumbnail) {
-        return thumbnail;
-      } else {
-        return url;
-      }
-    } else if (thumbnail) {
+    if (thumbnail) {
       return thumbnail;
+    } else if (url && isImage(url)) {
+      return url;
     } else {
       return undefined;
     }
@@ -353,6 +349,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           href={url}
           rel={relTags}
           title={url}
+          target={this.linkTarget}
         >
           {this.imgThumb(this.imageSrc)}
           <Icon
@@ -372,6 +369,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             data-tippy-content={I18NextService.i18n.t("expand_here")}
             onClick={linkEvent(this, this.handleImageExpandClick)}
             aria-label={I18NextService.i18n.t("expand_here")}
+            target={this.linkTarget}
           >
             <div className="thumbnail rounded bg-light d-flex justify-content-center">
               <Icon icon="play" classes="d-flex align-items-center" />
@@ -380,7 +378,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         );
       } else {
         return (
-          <a className="text-body" href={url} title={url} rel={relTags}>
+          <a
+            className="text-body"
+            href={url}
+            title={url}
+            rel={relTags}
+            target={this.linkTarget}
+          >
             <div className="thumbnail rounded bg-light d-flex justify-content-center">
               <Icon icon="external-link" classes="d-flex align-items-center" />
             </div>
@@ -393,6 +397,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           className="text-body"
           to={`/post/${post.id}`}
           title={I18NextService.i18n.t("comments")}
+          target={this.linkTarget}
         >
           <div className="thumbnail rounded bg-light d-flex justify-content-center">
             <Icon icon="message-square" classes="d-flex align-items-center" />
@@ -425,7 +430,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           <span className="mx-1 badge text-bg-light">
             {
               this.props.allLanguages.find(
-                lang => lang.id === post_view.post.language_id
+                lang => lang.id === post_view.post.language_id,
               )?.name
             }
           </span>
@@ -465,8 +470,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
     return (
       <>
-        <div className="post-title overflow-hidden">
-          <h5 className="d-inline">
+        <div className="post-title">
+          <h1 className="h5 d-inline text-break">
             {url && this.props.showBody ? (
               <a
                 className={
@@ -482,15 +487,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             ) : (
               this.postLink
             )}
-          </h5>
+          </h1>
 
           {/**
-           * If there is a URL, an embed title, and we were not told to show the
-           * body by the parent component, show the MetadataCard/body toggle.
+           * If there is (a) a URL and an embed title, or (b) a post body, and
+           * we were not told to show the body by the parent component, show the
+           * MetadataCard/body toggle.
            */}
           {!this.props.showBody &&
-            post.url &&
-            post.embed_title &&
+            ((post.url && post.embed_title) || post.body) &&
             this.showPreviewButton()}
 
           {post.removed && (
@@ -521,7 +526,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             <small
               className="unselectable pointer ms-2 text-muted fst-italic"
               data-tippy-content={I18NextService.i18n.t(
-                "featured_in_community"
+                "featured_in_community",
               )}
               aria-label={I18NextService.i18n.t("featured_in_community")}
             >
@@ -602,7 +607,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         {this.commentsButton}
         {canShare() && (
           <button
-            className="btn btn-sm btn-animate text-muted py-0"
+            className="btn btn-sm btn-link btn-animate text-muted py-0"
             onClick={linkEvent(this, this.handleShare)}
             type="button"
           >
@@ -611,7 +616,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         )}
         {!post.local && (
           <a
-            className="btn btn-sm btn-animate text-muted py-0"
+            className="btn btn-sm btn-link btn-animate text-muted py-0"
             title={I18NextService.i18n.t("link")}
             href={post.ap_id}
           >
@@ -650,7 +655,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
         <div className="dropdown">
           <button
-            className="btn btn-sm btn-animate text-muted py-0 dropdown-toggle"
+            className="btn btn-sm btn-link btn-animate text-muted py-0 dropdown-toggle"
             onClick={linkEvent(this, this.handleShowAdvanced)}
             data-tippy-content={I18NextService.i18n.t("more")}
             data-bs-toggle="dropdown"
@@ -741,6 +746,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     );
   }
 
+  public get linkTarget(): string {
+    return UserService.Instance.myUserInfo?.local_user_view.local_user
+      .open_links_in_new_tab
+      ? "_blank"
+      : // _self is the default target on links when the field is not specified
+        "_self";
+  }
+
   get commentsButton() {
     const post_view = this.postView;
     const title = I18NextService.i18n.t("number_of_comments", {
@@ -754,6 +767,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         title={title}
         to={`/post/${post_view.post.id}?scrollToComments=true`}
         data-tippy-content={title}
+        target={this.linkTarget}
       >
         <Icon icon="message-square" classes="me-1" inline />
         {post_view.counts.comments}
@@ -771,7 +785,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   get unreadCount(): number | undefined {
     const pv = this.postView;
-    return pv.unread_comments == pv.counts.comments || pv.unread_comments == 0
+    return pv.unread_comments === pv.counts.comments || pv.unread_comments === 0
       ? undefined
       : pv.unread_comments;
   }
@@ -783,7 +797,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       : I18NextService.i18n.t("save");
     return (
       <button
-        className="btn btn-sm btn-animate text-muted py-0"
+        className="btn btn-sm btn-link btn-animate text-muted py-0"
         onClick={linkEvent(this, this.handleSavePostClick)}
         data-tippy-content={label}
         aria-label={label}
@@ -804,7 +818,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 btn-link btn-animate text-muted py-0"
         to={{
           /* Empty string properties are required to satisfy type*/
           pathname: "/create_post",
@@ -894,7 +908,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   get viewSourceButton() {
     return (
       <button
-        className="btn btn-sm btn-animate text-muted py-0"
+        className="btn btn-sm btn-link btn-animate text-muted py-0"
         onClick={linkEvent(this, this.handleViewSource)}
         data-tippy-content={I18NextService.i18n.t("view_source")}
         aria-label={I18NextService.i18n.t("view_source")}
@@ -1121,7 +1135,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
         onClick={linkEvent(
           this,
-          !removed ? this.handleModRemoveShow : this.handleModRemoveSubmit
+          !removed ? this.handleModRemoveShow : this.handleModRemoveSubmit,
         )}
       >
         {/* TODO: Find an icon for this. */}
@@ -1142,7 +1156,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   removeAndBanDialogs() {
     const post = this.postView;
     const purgeTypeText =
-      this.state.purgeType == PurgeType.Post
+      this.state.purgeType === PurgeType.Post
         ? I18NextService.i18n.t("purge_post")
         : `${I18NextService.i18n.t("purge")} ${post.creator.name}`;
     return (
@@ -1194,7 +1208,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               className="btn btn-link btn-animate text-muted py-0 d-inline-block"
               onClick={linkEvent(
                 this,
-                this.handleCancelShowConfirmTransferCommunity
+                this.handleCancelShowConfirmTransferCommunity,
               )}
               aria-label={I18NextService.i18n.t("no")}
             >
@@ -1327,10 +1341,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     const post = this.postView.post;
     return post.thumbnail_url || (post.url && isImage(post.url)) ? (
       <div className="row">
-        <div className={`${this.state.imageExpanded ? "col-12" : "col-8"}`}>
+        <div className={`${this.state.imageExpanded ? "col-12" : "col-9"}`}>
           {this.postTitleLine()}
         </div>
-        <div className="col-4">
+        <div className="col-3 mobile-thumbnail-container">
           {/* Post thumbnail */}
           {!this.state.imageExpanded && this.thumbnail()}
         </div>
@@ -1411,10 +1425,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   private get myPost(): boolean {
     return (
-      this.postView.creator.id ==
+      this.postView.creator.id ===
       UserService.Instance.myUserInfo?.local_user_view.person.id
     );
   }
+
   handleEditClick(i: PostListing) {
     i.setState({ showEdit: true });
   }
@@ -1538,6 +1553,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       post_id: i.postView.post.id,
       removed: !i.postView.post.removed,
       auth: myAuthRequired(),
+      reason: i.state.removeReason,
     });
   }
 
@@ -1609,13 +1625,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   handlePurgeSubmit(i: PostListing, event: any) {
     event.preventDefault();
     i.setState({ purgeLoading: true });
-    if (i.state.purgeType == PurgeType.Person) {
+    if (i.state.purgeType === PurgeType.Person) {
       i.props.onPurgePerson({
         person_id: i.postView.creator.id,
         reason: i.state.purgeReason,
         auth: myAuthRequired(),
       });
-    } else if (i.state.purgeType == PurgeType.Post) {
+    } else if (i.state.purgeType === PurgeType.Post) {
       i.props.onPurgePost({
         post_id: i.postView.post.id,
         reason: i.state.purgeReason,
@@ -1648,7 +1664,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
     const ban = !i.props.post_view.creator_banned_from_community;
     // If its an unban, restore all their data
-    if (ban == false) {
+    if (ban === false) {
       i.setState({ removeData: false });
     }
     const person_id = i.props.post_view.creator.id;
@@ -1656,7 +1672,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     const reason = i.state.banReason;
     const expires = futureDaysToUnixTime(i.state.banExpireDays);
 
-    if (i.state.banType == BanType.Community) {
+    if (i.state.banType === BanType.Community) {
       const community_id = i.postView.community.id;
       i.props.onBanPersonFromCommunity({
         community_id,
@@ -1727,6 +1743,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     event.preventDefault();
     i.setState({ imageExpanded: !i.state.imageExpanded });
     setupTippy();
+
+    const auth = myAuth();
+    if (auth && !i.props.post_view.read) {
+      i.props.onMarkPostAsRead({
+        post_id: i.props.post_view.post.id,
+        read: true,
+        auth: auth,
+      });
+    }
   }
 
   handleViewSource(i: PostListing) {
@@ -1776,7 +1801,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       this.props.moderators,
       this.props.admins,
       undefined,
-      true
+      true,
     );
   }
 
@@ -1784,7 +1809,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     return canMod(
       this.postView.creator.id,
       this.props.moderators,
-      this.props.admins
+      this.props.admins,
     );
   }