]> Untitled Git - lemmy.git/blobdiff - ui/src/components/community.tsx
routes.api: fix get_captcha endpoint (#1135)
[lemmy.git] / ui / src / components / community.tsx
index 9f96ac51edc1e0e22feb3cf175c57395399483d1..f86562f8c23286fe84bb38f92b5a708fb8d028eb 100644 (file)
@@ -1,6 +1,8 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
+import { DataType } from '../interfaces';
 import {
   UserOperation,
   Community as CommunityI,
@@ -17,13 +19,39 @@ import {
   PostResponse,
   AddModToCommunityResponse,
   BanFromCommunityResponse,
+  Comment,
+  GetCommentsForm,
+  GetCommentsResponse,
+  CommentResponse,
   WebSocketJsonResponse,
-} from '../interfaces';
-import { WebSocketService, UserService } from '../services';
+  GetSiteResponse,
+  Site,
+} from 'lemmy-js-client';
+import { WebSocketService } from '../services';
 import { PostListings } from './post-listings';
+import { CommentNodes } from './comment-nodes';
 import { SortSelect } from './sort-select';
+import { DataTypeSelect } from './data-type-select';
 import { Sidebar } from './sidebar';
-import { wsJsonToRes, routeSortTypeToEnum, fetchLimit, toast } from '../utils';
+import { CommunityLink } from './community-link';
+import { BannerIconHeader } from './banner-icon-header';
+import {
+  wsJsonToRes,
+  fetchLimit,
+  toast,
+  getPageFromProps,
+  getSortTypeFromProps,
+  getDataTypeFromProps,
+  editCommentRes,
+  saveCommentRes,
+  createCommentLikeRes,
+  createPostLikeFindRes,
+  editPostFindRes,
+  commentsToFlatNodes,
+  setupTippy,
+  favIconUrl,
+  notifyPost,
+} from '../utils';
 import { i18n } from '../i18next';
 
 interface State {
@@ -35,8 +63,23 @@ interface State {
   online: number;
   loading: boolean;
   posts: Array<Post>;
+  comments: Array<Comment>;
+  dataType: DataType;
   sort: SortType;
   page: number;
+  site: Site;
+}
+
+interface CommunityProps {
+  dataType: DataType;
+  sort: SortType;
+  page: number;
+}
+
+interface UrlParams {
+  dataType?: string;
+  sort?: SortType;
+  page?: number;
 }
 
 export class Community extends Component<any, State> {
@@ -57,6 +100,11 @@ export class Community extends Component<any, State> {
       removed: null,
       nsfw: false,
       deleted: null,
+      local: null,
+      actor_id: null,
+      last_refreshed_at: null,
+      creator_actor_id: null,
+      creator_local: null,
     },
     moderators: [],
     admins: [],
@@ -65,27 +113,35 @@ export class Community extends Component<any, State> {
     online: null,
     loading: true,
     posts: [],
-    sort: this.getSortTypeFromProps(this.props),
-    page: this.getPageFromProps(this.props),
+    comments: [],
+    dataType: getDataTypeFromProps(this.props),
+    sort: getSortTypeFromProps(this.props),
+    page: getPageFromProps(this.props),
+    site: {
+      id: undefined,
+      name: undefined,
+      creator_id: undefined,
+      published: undefined,
+      creator_name: undefined,
+      number_of_users: undefined,
+      number_of_posts: undefined,
+      number_of_comments: undefined,
+      number_of_communities: undefined,
+      enable_downvotes: undefined,
+      open_registration: undefined,
+      enable_nsfw: undefined,
+      icon: undefined,
+      banner: undefined,
+      creator_preferred_username: undefined,
+    },
   };
 
-  getSortTypeFromProps(props: any): SortType {
-    return props.match.params.sort
-      ? routeSortTypeToEnum(props.match.params.sort)
-      : UserService.Instance.user
-      ? UserService.Instance.user.default_sort_type
-      : SortType.Hot;
-  }
-
-  getPageFromProps(props: any): number {
-    return props.match.params.page ? Number(props.match.params.page) : 1;
-  }
-
   constructor(props: any, context: any) {
     super(props, context);
 
     this.state = this.emptyState;
     this.handleSortChange = this.handleSortChange.bind(this);
+    this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
 
     this.subscription = WebSocketService.Instance.subject
       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
@@ -100,28 +156,55 @@ export class Community extends Component<any, State> {
       name: this.state.communityName ? this.state.communityName : null,
     };
     WebSocketService.Instance.getCommunity(form);
+    WebSocketService.Instance.getSite();
   }
 
   componentWillUnmount() {
     this.subscription.unsubscribe();
   }
 
-  // Necessary for back button for some reason
-  componentWillReceiveProps(nextProps: any) {
+  static getDerivedStateFromProps(props: any): CommunityProps {
+    return {
+      dataType: getDataTypeFromProps(props),
+      sort: getSortTypeFromProps(props),
+      page: getPageFromProps(props),
+    };
+  }
+
+  componentDidUpdate(_: any, lastState: State) {
     if (
-      nextProps.history.action == 'POP' ||
-      nextProps.history.action == 'PUSH'
+      lastState.dataType !== this.state.dataType ||
+      lastState.sort !== this.state.sort ||
+      lastState.page !== this.state.page
     ) {
-      this.state.sort = this.getSortTypeFromProps(nextProps);
-      this.state.page = this.getPageFromProps(nextProps);
-      this.setState(this.state);
-      this.fetchPosts();
+      this.setState({ loading: true });
+      this.fetchData();
     }
   }
 
+  get documentTitle(): string {
+    if (this.state.community.title) {
+      return `${this.state.community.title} - ${this.state.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
+  get favIcon(): string {
+    return this.state.site.icon ? this.state.site.icon : favIconUrl;
+  }
+
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle}>
+          <link
+            id="favicon"
+            rel="icon"
+            type="image/x-icon"
+            href={this.favIcon}
+          />
+        </Helmet>
         {this.state.loading ? (
           <h5>
             <svg class="icon icon-spinner spin">
@@ -131,21 +214,9 @@ export class Community extends Component<any, State> {
         ) : (
           <div class="row">
             <div class="col-12 col-md-8">
-              <h5>
-                {this.state.community.title}
-                {this.state.community.removed && (
-                  <small className="ml-2 text-muted font-italic">
-                    {i18n.t('removed')}
-                  </small>
-                )}
-                {this.state.community.nsfw && (
-                  <small className="ml-2 text-muted font-italic">
-                    {i18n.t('nsfw')}
-                  </small>
-                )}
-              </h5>
+              {this.communityInfo()}
               {this.selects()}
-              <PostListings posts={this.state.posts} />
+              {this.listings()}
               {this.paginator()}
             </div>
             <div class="col-12 col-md-4">
@@ -154,6 +225,7 @@ export class Community extends Component<any, State> {
                 moderators={this.state.moderators}
                 admins={this.state.admins}
                 online={this.state.online}
+                enableNsfw={this.state.site.enable_nsfw}
               />
             </div>
           </div>
@@ -162,17 +234,65 @@ export class Community extends Component<any, State> {
     );
   }
 
+  listings() {
+    return this.state.dataType == DataType.Post ? (
+      <PostListings
+        posts={this.state.posts}
+        removeDuplicates
+        sort={this.state.sort}
+        enableDownvotes={this.state.site.enable_downvotes}
+        enableNsfw={this.state.site.enable_nsfw}
+      />
+    ) : (
+      <CommentNodes
+        nodes={commentsToFlatNodes(this.state.comments)}
+        noIndent
+        sortType={this.state.sort}
+        showContext
+        enableDownvotes={this.state.site.enable_downvotes}
+      />
+    );
+  }
+
+  communityInfo() {
+    return (
+      <div>
+        <BannerIconHeader
+          banner={this.state.community.banner}
+          icon={this.state.community.icon}
+        />
+        <h5 class="mb-0">{this.state.community.title}</h5>
+        <CommunityLink
+          community={this.state.community}
+          realLink
+          useApubName
+          muted
+          hideAvatar
+        />
+        <hr />
+      </div>
+    );
+  }
+
   selects() {
     return (
-      <div class="mb-2">
-        <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
+      <div class="mb-3">
+        <span class="mr-3">
+          <DataTypeSelect
+            type_={this.state.dataType}
+            onChange={this.handleDataTypeChange}
+          />
+        </span>
+        <span class="mr-2">
+          <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
+        </span>
         <a
-          href={`/feeds/c/${this.state.communityName}.xml?sort=${
-            SortType[this.state.sort]
-          }`}
+          href={`/feeds/c/${this.state.communityName}.xml?sort=${this.state.sort}`}
           target="_blank"
+          title="RSS"
+          rel="noopener"
         >
-          <svg class="icon mx-2 text-muted small">
+          <svg class="icon text-muted small">
             <use xlinkHref="#icon-rss">#</use>
           </svg>
         </a>
@@ -185,15 +305,15 @@ export class Community extends Component<any, State> {
       <div class="my-2">
         {this.state.page > 1 && (
           <button
-            class="btn btn-sm btn-secondary mr-1"
+            class="btn btn-secondary mr-1"
             onClick={linkEvent(this, this.prevPage)}
           >
             {i18n.t('prev')}
           </button>
         )}
-        {this.state.posts.length == fetchLimit && (
+        {this.state.posts.length > 0 && (
           <button
-            class="btn btn-sm btn-secondary"
+            class="btn btn-secondary"
             onClick={linkEvent(this, this.nextPage)}
           >
             {i18n.t('next')}
@@ -204,47 +324,54 @@ export class Community extends Component<any, State> {
   }
 
   nextPage(i: Community) {
-    i.state.page++;
-    i.setState(i.state);
-    i.updateUrl();
-    i.fetchPosts();
+    i.updateUrl({ page: i.state.page + 1 });
     window.scrollTo(0, 0);
   }
 
   prevPage(i: Community) {
-    i.state.page--;
-    i.setState(i.state);
-    i.updateUrl();
-    i.fetchPosts();
+    i.updateUrl({ page: i.state.page - 1 });
     window.scrollTo(0, 0);
   }
 
   handleSortChange(val: SortType) {
-    this.state.sort = val;
-    this.state.page = 1;
-    this.state.loading = true;
-    this.setState(this.state);
-    this.updateUrl();
-    this.fetchPosts();
+    this.updateUrl({ sort: val, page: 1 });
     window.scrollTo(0, 0);
   }
 
-  updateUrl() {
-    let sortStr = SortType[this.state.sort].toLowerCase();
+  handleDataTypeChange(val: DataType) {
+    this.updateUrl({ dataType: DataType[val], page: 1 });
+    window.scrollTo(0, 0);
+  }
+
+  updateUrl(paramUpdates: UrlParams) {
+    const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
+    const sortStr = paramUpdates.sort || this.state.sort;
+    const page = paramUpdates.page || this.state.page;
     this.props.history.push(
-      `/c/${this.state.community.name}/sort/${sortStr}/page/${this.state.page}`
+      `/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${page}`
     );
   }
 
-  fetchPosts() {
-    let getPostsForm: GetPostsForm = {
-      page: this.state.page,
-      limit: fetchLimit,
-      sort: SortType[this.state.sort],
-      type_: ListingType[ListingType.Community],
-      community_id: this.state.community.id,
-    };
-    WebSocketService.Instance.getPosts(getPostsForm);
+  fetchData() {
+    if (this.state.dataType == DataType.Post) {
+      let getPostsForm: GetPostsForm = {
+        page: this.state.page,
+        limit: fetchLimit,
+        sort: this.state.sort,
+        type_: ListingType.Community,
+        community_id: this.state.community.id,
+      };
+      WebSocketService.Instance.getPosts(getPostsForm);
+    } else {
+      let getCommentsForm: GetCommentsForm = {
+        page: this.state.page,
+        limit: fetchLimit,
+        sort: this.state.sort,
+        type_: ListingType.Community,
+        community_id: this.state.community.id,
+      };
+      WebSocketService.Instance.getComments(getCommentsForm);
+    }
   }
 
   parseMessage(msg: WebSocketJsonResponse) {
@@ -254,16 +381,20 @@ export class Community extends Component<any, State> {
       toast(i18n.t(msg.error), 'danger');
       this.context.router.history.push('/');
       return;
+    } else if (msg.reconnect) {
+      this.fetchData();
     } else if (res.op == UserOperation.GetCommunity) {
       let data = res.data as GetCommunityResponse;
       this.state.community = data.community;
       this.state.moderators = data.moderators;
-      this.state.admins = data.admins;
       this.state.online = data.online;
-      document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`;
       this.setState(this.state);
-      this.fetchPosts();
-    } else if (res.op == UserOperation.EditCommunity) {
+      this.fetchData();
+    } else if (
+      res.op == UserOperation.EditCommunity ||
+      res.op == UserOperation.DeleteCommunity ||
+      res.op == UserOperation.RemoveCommunity
+    ) {
       let data = res.data as CommunityResponse;
       this.state.community = data.community;
       this.setState(this.state);
@@ -278,32 +409,25 @@ export class Community extends Component<any, State> {
       this.state.posts = data.posts;
       this.state.loading = false;
       this.setState(this.state);
-    } else if (res.op == UserOperation.EditPost) {
+      setupTippy();
+    } else if (
+      res.op == UserOperation.EditPost ||
+      res.op == UserOperation.DeletePost ||
+      res.op == UserOperation.RemovePost ||
+      res.op == UserOperation.LockPost ||
+      res.op == UserOperation.StickyPost
+    ) {
       let data = res.data as PostResponse;
-      let found = this.state.posts.find(c => c.id == data.post.id);
-
-      found.url = data.post.url;
-      found.name = data.post.name;
-      found.nsfw = data.post.nsfw;
-
+      editPostFindRes(data, this.state.posts);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreatePost) {
       let data = res.data as PostResponse;
       this.state.posts.unshift(data.post);
+      notifyPost(data.post, this.context.router);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreatePostLike) {
       let data = res.data as PostResponse;
-      let found = this.state.posts.find(c => c.id == data.post.id);
-
-      found.score = data.post.score;
-      found.upvotes = data.post.upvotes;
-      found.downvotes = data.post.downvotes;
-      if (data.post.my_vote !== null) {
-        found.my_vote = data.post.my_vote;
-        found.upvoteLoading = false;
-        found.downvoteLoading = false;
-      }
-
+      createPostLikeFindRes(data, this.state.posts);
       this.setState(this.state);
     } else if (res.op == UserOperation.AddModToCommunity) {
       let data = res.data as AddModToCommunityResponse;
@@ -317,6 +441,40 @@ export class Community extends Component<any, State> {
         .forEach(p => (p.banned = data.banned));
 
       this.setState(this.state);
+    } else if (res.op == UserOperation.GetComments) {
+      let data = res.data as GetCommentsResponse;
+      this.state.comments = data.comments;
+      this.state.loading = false;
+      this.setState(this.state);
+    } else if (
+      res.op == UserOperation.EditComment ||
+      res.op == UserOperation.DeleteComment ||
+      res.op == UserOperation.RemoveComment
+    ) {
+      let data = res.data as CommentResponse;
+      editCommentRes(data, this.state.comments);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.CreateComment) {
+      let data = res.data as CommentResponse;
+
+      // Necessary since it might be a user reply
+      if (data.recipient_ids.length == 0) {
+        this.state.comments.unshift(data.comment);
+        this.setState(this.state);
+      }
+    } else if (res.op == UserOperation.SaveComment) {
+      let data = res.data as CommentResponse;
+      saveCommentRes(data, this.state.comments);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.CreateCommentLike) {
+      let data = res.data as CommentResponse;
+      createCommentLikeRes(data, this.state.comments);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.GetSite) {
+      let data = res.data as GetSiteResponse;
+      this.state.site = data.site;
+      this.state.admins = data.admins;
+      this.setState(this.state);
     }
   }
 }