]> Untitled Git - lemmy.git/blobdiff - ui/src/components/main.tsx
routes.api: fix get_captcha endpoint (#1135)
[lemmy.git] / ui / src / components / main.tsx
index 366d3be8f1e408f962e40fcd0332fcee727d181f..286e84ca4c5e7596c4da8ebcd5b29e8a91c5c957 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Link } from 'inferno-router';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
@@ -12,7 +13,6 @@ import {
   SortType,
   GetSiteResponse,
   ListingType,
-  DataType,
   SiteResponse,
   GetPostsResponse,
   PostResponse,
@@ -25,7 +25,8 @@ import {
   AddAdminResponse,
   BanUserResponse,
   WebSocketJsonResponse,
-} from '../interfaces';
+} from 'lemmy-js-client';
+import { DataType } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { PostListings } from './post-listings';
 import { CommentNodes } from './comment-nodes';
@@ -34,6 +35,8 @@ import { ListingTypeSelect } from './listing-type-select';
 import { DataTypeSelect } from './data-type-select';
 import { SiteForm } from './site-form';
 import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
+import { BannerIconHeader } from './banner-icon-header';
 import {
   wsJsonToRes,
   repoUrl,
@@ -51,6 +54,8 @@ import {
   editPostFindRes,
   commentsToFlatNodes,
   setupTippy,
+  favIconUrl,
+  notifyPost,
 } from '../utils';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
@@ -69,6 +74,20 @@ interface MainState {
   page: number;
 }
 
+interface MainProps {
+  listingType: ListingType;
+  dataType: DataType;
+  sort: SortType;
+  page: number;
+}
+
+interface UrlParams {
+  listingType?: ListingType;
+  dataType?: string;
+  sort?: SortType;
+  page?: number;
+}
+
 export class Main extends Component<any, MainState> {
   private subscription: Subscription;
   private emptyState: MainState = {
@@ -88,10 +107,15 @@ export class Main extends Component<any, MainState> {
         enable_downvotes: null,
         open_registration: null,
         enable_nsfw: null,
+        icon: null,
+        banner: null,
+        creator_preferred_username: null,
       },
       admins: [],
       banned: [],
       online: null,
+      version: null,
+      federated_instances: null,
     },
     showEditSite: false,
     loading: true,
@@ -127,7 +151,7 @@ export class Main extends Component<any, MainState> {
     }
 
     let listCommunitiesForm: ListCommunitiesForm = {
-      sort: SortType[SortType.Hot],
+      sort: SortType.Hot,
       limit: 6,
     };
 
@@ -140,80 +164,105 @@ export class Main extends Component<any, MainState> {
     this.subscription.unsubscribe();
   }
 
-  // Necessary for back button for some reason
-  componentWillReceiveProps(nextProps: any) {
+  static getDerivedStateFromProps(props: any): MainProps {
+    return {
+      listingType: getListingTypeFromProps(props),
+      dataType: getDataTypeFromProps(props),
+      sort: getSortTypeFromProps(props),
+      page: getPageFromProps(props),
+    };
+  }
+
+  componentDidUpdate(_: any, lastState: MainState) {
     if (
-      nextProps.history.action == 'POP' ||
-      nextProps.history.action == 'PUSH'
+      lastState.listingType !== this.state.listingType ||
+      lastState.dataType !== this.state.dataType ||
+      lastState.sort !== this.state.sort ||
+      lastState.page !== this.state.page
     ) {
-      this.state.listingType = getListingTypeFromProps(nextProps);
-      this.state.dataType = getDataTypeFromProps(nextProps);
-      this.state.sort = getSortTypeFromProps(nextProps);
-      this.state.page = getPageFromProps(nextProps);
-      this.setState(this.state);
+      this.setState({ loading: true });
       this.fetchData();
     }
   }
 
+  get documentTitle(): string {
+    if (this.state.siteRes.site.name) {
+      return `${this.state.siteRes.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
+  get favIcon(): string {
+    return this.state.siteRes.site.icon
+      ? this.state.siteRes.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>
         <div class="row">
           <main role="main" class="col-12 col-md-8">
             {this.posts()}
           </main>
-          <aside class="col-12 col-md-4">{this.my_sidebar()}</aside>
+          <aside class="col-12 col-md-4">{this.mySidebar()}</aside>
         </div>
       </div>
     );
   }
 
-  my_sidebar() {
+  mySidebar() {
     return (
       <div>
         {!this.state.loading && (
           <div>
-            <div class="card border-secondary mb-3">
+            <div class="card bg-transparent border-secondary mb-3">
+              <div class="card-header bg-transparent border-secondary">
+                <div class="mb-2">
+                  {this.siteName()}
+                  {this.adminButtons()}
+                </div>
+                <BannerIconHeader banner={this.state.siteRes.site.banner} />
+              </div>
               <div class="card-body">
                 {this.trendingCommunities()}
-                {UserService.Instance.user &&
-                  this.state.subscribedCommunities.length > 0 && (
-                    <div>
-                      <h5>
-                        <T i18nKey="subscribed_to_communities">
-                          #
-                          <Link class="text-body" to="/communities">
-                            #
-                          </Link>
-                        </T>
-                      </h5>
-                      <ul class="list-inline">
-                        {this.state.subscribedCommunities.map(community => (
-                          <li class="list-inline-item">
-                            <Link to={`/c/${community.community_name}`}>
-                              {community.community_name}
-                            </Link>
-                          </li>
-                        ))}
-                      </ul>
-                    </div>
-                  )}
-                <Link
-                  class="btn btn-sm btn-secondary btn-block"
-                  to="/create_community"
-                >
-                  {i18n.t('create_a_community')}
-                </Link>
+                {this.createCommunityButton()}
+                {/*
+                {this.subscribedCommunities()}
+                */}
               </div>
             </div>
-            {this.sidebar()}
-            {this.landing()}
+
+            <div class="card bg-transparent border-secondary mb-3">
+              <div class="card-body">{this.sidebar()}</div>
+            </div>
+
+            <div class="card bg-transparent border-secondary">
+              <div class="card-body">{this.landing()}</div>
+            </div>
           </div>
         )}
       </div>
     );
   }
 
+  createCommunityButton() {
+    return (
+      <Link class="btn btn-secondary btn-block" to="/create_community">
+        {i18n.t('create_a_community')}
+      </Link>
+    );
+  }
+
   trendingCommunities() {
     return (
       <div>
@@ -228,7 +277,7 @@ export class Main extends Component<any, MainState> {
         <ul class="list-inline">
           {this.state.trendingCommunities.map(community => (
             <li class="list-inline-item">
-              <Link to={`/c/${community.name}`}>{community.name}</Link>
+              <CommunityLink community={community} />
             </li>
           ))}
         </ul>
@@ -236,6 +285,39 @@ export class Main extends Component<any, MainState> {
     );
   }
 
+  subscribedCommunities() {
+    return (
+      UserService.Instance.user &&
+      this.state.subscribedCommunities.length > 0 && (
+        <div>
+          <h5>
+            <T i18nKey="subscribed_to_communities">
+              #
+              <Link class="text-body" to="/communities">
+                #
+              </Link>
+            </T>
+          </h5>
+          <ul class="list-inline">
+            {this.state.subscribedCommunities.map(community => (
+              <li class="list-inline-item">
+                <CommunityLink
+                  community={{
+                    name: community.community_name,
+                    id: community.community_id,
+                    local: community.community_local,
+                    actor_id: community.community_actor_id,
+                    icon: community.community_icon,
+                  }}
+                />
+              </li>
+            ))}
+          </ul>
+        </div>
+      )
+    );
+  }
+
   sidebar() {
     return (
       <div>
@@ -251,139 +333,159 @@ export class Main extends Component<any, MainState> {
     );
   }
 
-  updateUrl() {
-    let listingTypeStr = ListingType[this.state.listingType].toLowerCase();
-    let dataTypeStr = DataType[this.state.dataType].toLowerCase();
-    let sortStr = SortType[this.state.sort].toLowerCase();
+  updateUrl(paramUpdates: UrlParams) {
+    const listingTypeStr = paramUpdates.listingType || this.state.listingType;
+    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(
-      `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${this.state.page}`
+      `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
     );
   }
 
   siteInfo() {
     return (
       <div>
-        <div class="card border-secondary mb-3">
-          <div class="card-body">
-            <h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>
-            {this.canAdmin && (
-              <ul class="list-inline mb-1 text-muted font-weight-bold">
-                <li className="list-inline-item-action">
-                  <span
-                    class="pointer"
-                    onClick={linkEvent(this, this.handleEditClick)}
-                    data-tippy-content={i18n.t('edit')}
-                  >
-                    <svg class="icon icon-inline">
-                      <use xlinkHref="#icon-edit"></use>
-                    </svg>
-                  </span>
-                </li>
-              </ul>
-            )}
-            <ul class="my-2 list-inline">
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_online', { count: this.state.siteRes.online })}
-              </li>
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_of_users', {
-                  count: this.state.siteRes.site.number_of_users,
-                })}
-              </li>
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_of_communities', {
-                  count: this.state.siteRes.site.number_of_communities,
-                })}
-              </li>
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_of_posts', {
-                  count: this.state.siteRes.site.number_of_posts,
-                })}
-              </li>
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_of_comments', {
-                  count: this.state.siteRes.site.number_of_comments,
-                })}
-              </li>
-              <li className="list-inline-item">
-                <Link className="badge badge-secondary" to="/modlog">
-                  {i18n.t('modlog')}
-                </Link>
-              </li>
-            </ul>
-            <ul class="mt-1 list-inline small mb-0">
-              <li class="list-inline-item">{i18n.t('admins')}:</li>
-              {this.state.siteRes.admins.map(admin => (
-                <li class="list-inline-item">
-                  <UserListing
-                    user={{
-                      name: admin.name,
-                      avatar: admin.avatar,
-                    }}
-                  />
-                </li>
-              ))}
-            </ul>
-          </div>
-        </div>
-        {this.state.siteRes.site.description && (
-          <div class="card border-secondary mb-3">
-            <div class="card-body">
-              <div
-                className="md-div"
-                dangerouslySetInnerHTML={mdToHtml(
-                  this.state.siteRes.site.description
-                )}
-              />
-            </div>
-          </div>
-        )}
+        {this.state.siteRes.site.description && this.siteDescription()}
+        {this.badges()}
+        {this.admins()}
       </div>
     );
   }
 
+  siteName() {
+    return <h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>;
+  }
+
+  admins() {
+    return (
+      <ul class="mt-1 list-inline small mb-0">
+        <li class="list-inline-item">{i18n.t('admins')}:</li>
+        {this.state.siteRes.admins.map(admin => (
+          <li class="list-inline-item">
+            <UserListing
+              user={{
+                name: admin.name,
+                preferred_username: admin.preferred_username,
+                avatar: admin.avatar,
+                local: admin.local,
+                actor_id: admin.actor_id,
+                id: admin.id,
+              }}
+            />
+          </li>
+        ))}
+      </ul>
+    );
+  }
+
+  badges() {
+    return (
+      <ul class="my-2 list-inline">
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_online', { count: this.state.siteRes.online })}
+        </li>
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_of_users', {
+            count: this.state.siteRes.site.number_of_users,
+          })}
+        </li>
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_of_communities', {
+            count: this.state.siteRes.site.number_of_communities,
+          })}
+        </li>
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_of_posts', {
+            count: this.state.siteRes.site.number_of_posts,
+          })}
+        </li>
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_of_comments', {
+            count: this.state.siteRes.site.number_of_comments,
+          })}
+        </li>
+        <li className="list-inline-item">
+          <Link className="badge badge-light" to="/modlog">
+            {i18n.t('modlog')}
+          </Link>
+        </li>
+      </ul>
+    );
+  }
+
+  adminButtons() {
+    return (
+      this.canAdmin && (
+        <ul class="list-inline mb-1 text-muted font-weight-bold">
+          <li className="list-inline-item-action">
+            <span
+              class="pointer"
+              onClick={linkEvent(this, this.handleEditClick)}
+              data-tippy-content={i18n.t('edit')}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-edit"></use>
+              </svg>
+            </span>
+          </li>
+        </ul>
+      )
+    );
+  }
+
+  siteDescription() {
+    return (
+      <div
+        className="md-div"
+        dangerouslySetInnerHTML={mdToHtml(this.state.siteRes.site.description)}
+      />
+    );
+  }
+
   landing() {
     return (
-      <div class="card border-secondary">
-        <div class="card-body">
-          <h5>
-            {i18n.t('powered_by')}
-            <svg class="icon mx-2">
-              <use xlinkHref="#icon-mouse">#</use>
-            </svg>
-            <a href={repoUrl}>
-              Lemmy<sup>beta</sup>
+      <>
+        <h5>
+          {i18n.t('powered_by')}
+          <svg class="icon mx-2">
+            <use xlinkHref="#icon-mouse">#</use>
+          </svg>
+          <a href={repoUrl}>
+            Lemmy<sup>beta</sup>
+          </a>
+        </h5>
+        <p class="mb-0">
+          <T i18nKey="landing_0">
+            #
+            <a href="https://en.wikipedia.org/wiki/Social_network_aggregation">
+              #
             </a>
-          </h5>
-          <p class="mb-0">
-            <T i18nKey="landing_0">
+            <a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
+            <br class="big"></br>
+            <code>#</code>
+            <br></br>
+            <b>#</b>
+            <br class="big"></br>
+            <a href={repoUrl}>#</a>
+            <br class="big"></br>
+            <a href="https://www.rust-lang.org">#</a>
+            <a href="https://actix.rs/">#</a>
+            <a href="https://infernojs.org">#</a>
+            <a href="https://www.typescriptlang.org/">#</a>
+            <br class="big"></br>
+            <a href="https://github.com/LemmyNet/lemmy/graphs/contributors?type=a">
               #
-              <a href="https://en.wikipedia.org/wiki/Social_network_aggregation">
-                #
-              </a>
-              <a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
-              <br></br>
-              <code>#</code>
-              <br></br>
-              <b>#</b>
-              <br></br>
-              <a href={repoUrl}>#</a>
-              <br></br>
-              <a href="https://www.rust-lang.org">#</a>
-              <a href="https://actix.rs/">#</a>
-              <a href="https://infernojs.org">#</a>
-              <a href="https://www.typescriptlang.org/">#</a>
-            </T>
-          </p>
-        </div>
-      </div>
+            </a>
+          </T>
+        </p>
+      </>
     );
   }
 
   posts() {
     return (
       <div class="main-content-wrapper">
-        {this.selects()}
         {this.state.loading ? (
           <h5>
             <svg class="icon icon-spinner spin">
@@ -392,6 +494,7 @@ export class Main extends Component<any, MainState> {
           </h5>
         ) : (
           <div>
+            {this.selects()}
             {this.listings()}
             {this.paginator()}
           </div>
@@ -407,6 +510,8 @@ export class Main extends Component<any, MainState> {
         showCommunity
         removeDuplicates
         sort={this.state.sort}
+        enableDownvotes={this.state.siteRes.site.enable_downvotes}
+        enableNsfw={this.state.siteRes.site.enable_nsfw}
       />
     ) : (
       <CommentNodes
@@ -415,6 +520,7 @@ export class Main extends Component<any, MainState> {
         showCommunity
         sortType={this.state.sort}
         showContext
+        enableDownvotes={this.state.siteRes.site.enable_downvotes}
       />
     );
   }
@@ -431,6 +537,10 @@ export class Main extends Component<any, MainState> {
         <span class="mr-3">
           <ListingTypeSelect
             type_={this.state.listingType}
+            showLocal={
+              this.state.siteRes.federated_instances &&
+              this.state.siteRes.federated_instances.length > 0
+            }
             onChange={this.handleListingTypeChange}
           />
         </span>
@@ -439,8 +549,9 @@ export class Main extends Component<any, MainState> {
         </span>
         {this.state.listingType == ListingType.All && (
           <a
-            href={`/feeds/all.xml?sort=${SortType[this.state.sort]}`}
+            href={`/feeds/all.xml?sort=${this.state.sort}`}
             target="_blank"
+            rel="noopener"
             title="RSS"
           >
             <svg class="icon text-muted small">
@@ -451,11 +562,10 @@ export class Main extends Component<any, MainState> {
         {UserService.Instance.user &&
           this.state.listingType == ListingType.Subscribed && (
             <a
-              href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${
-                SortType[this.state.sort]
-              }`}
+              href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${this.state.sort}`}
               target="_blank"
               title="RSS"
+              rel="noopener"
             >
               <svg class="icon text-muted small">
                 <use xlinkHref="#icon-rss">#</use>
@@ -471,15 +581,15 @@ export class Main extends Component<any, MainState> {
       <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')}
@@ -509,50 +619,27 @@ export class Main extends Component<any, MainState> {
   }
 
   nextPage(i: Main) {
-    i.state.page++;
-    i.state.loading = true;
-    i.setState(i.state);
-    i.updateUrl();
-    i.fetchData();
+    i.updateUrl({ page: i.state.page + 1 });
     window.scrollTo(0, 0);
   }
 
   prevPage(i: Main) {
-    i.state.page--;
-    i.state.loading = true;
-    i.setState(i.state);
-    i.updateUrl();
-    i.fetchData();
+    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.fetchData();
+    this.updateUrl({ sort: val, page: 1 });
     window.scrollTo(0, 0);
   }
 
   handleListingTypeChange(val: ListingType) {
-    this.state.listingType = val;
-    this.state.page = 1;
-    this.state.loading = true;
-    this.setState(this.state);
-    this.updateUrl();
-    this.fetchData();
+    this.updateUrl({ listingType: val, page: 1 });
     window.scrollTo(0, 0);
   }
 
   handleDataTypeChange(val: DataType) {
-    this.state.dataType = val;
-    this.state.page = 1;
-    this.state.loading = true;
-    this.setState(this.state);
-    this.updateUrl();
-    this.fetchData();
+    this.updateUrl({ dataType: DataType[val], page: 1 });
     window.scrollTo(0, 0);
   }
 
@@ -561,16 +648,16 @@ export class Main extends Component<any, MainState> {
       let getPostsForm: GetPostsForm = {
         page: this.state.page,
         limit: fetchLimit,
-        sort: SortType[this.state.sort],
-        type_: ListingType[this.state.listingType],
+        sort: this.state.sort,
+        type_: this.state.listingType,
       };
       WebSocketService.Instance.getPosts(getPostsForm);
     } else {
       let getCommentsForm: GetCommentsForm = {
         page: this.state.page,
         limit: fetchLimit,
-        sort: SortType[this.state.sort],
-        type_: ListingType[this.state.listingType],
+        sort: this.state.sort,
+        type_: this.state.listingType,
       };
       WebSocketService.Instance.getComments(getCommentsForm);
     }
@@ -604,7 +691,6 @@ export class Main extends Component<any, MainState> {
       this.state.siteRes.banned = data.banned;
       this.state.siteRes.online = data.online;
       this.setState(this.state);
-      document.title = `${WebSocketService.Instance.site.name}`;
     } else if (res.op == UserOperation.EditSite) {
       let data = res.data as SiteResponse;
       this.state.siteRes.site = data.site;
@@ -628,6 +714,7 @@ export class Main extends Component<any, MainState> {
             .includes(data.post.community_id)
         ) {
           this.state.posts.unshift(data.post);
+          notifyPost(data.post, this.context.router);
         }
       } else {
         // NSFW posts
@@ -641,6 +728,7 @@ export class Main extends Component<any, MainState> {
             UserService.Instance.user.show_nsfw)
         ) {
           this.state.posts.unshift(data.post);
+          notifyPost(data.post, this.context.router);
         }
       }
       this.setState(this.state);
@@ -679,7 +767,11 @@ export class Main extends Component<any, MainState> {
       this.state.comments = data.comments;
       this.state.loading = false;
       this.setState(this.state);
-    } else if (res.op == UserOperation.EditComment) {
+    } 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);