]> Untitled Git - lemmy.git/blobdiff - ui/src/components/search.tsx
routes.api: fix get_captcha endpoint (#1135)
[lemmy.git] / ui / src / components / search.tsx
index f310b80c892c119140a37699a0d4eea27f3526b8..8ab7f599b16426ad42f17838df719e32c9dadfb7 100644 (file)
@@ -1,5 +1,5 @@
 import { Component, linkEvent } from 'inferno';
-import { Link } from 'inferno-router';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import {
@@ -12,21 +12,30 @@ import {
   SearchForm,
   SearchResponse,
   SearchType,
-} from '../interfaces';
+  PostResponse,
+  CommentResponse,
+  WebSocketJsonResponse,
+  GetSiteResponse,
+  Site,
+} from 'lemmy-js-client';
 import { WebSocketService } from '../services';
 import {
   wsJsonToRes,
   fetchLimit,
   routeSearchTypeToEnum,
   routeSortTypeToEnum,
-  pictshareAvatarThumbnail,
-  showAvatars,
+  toast,
+  createCommentLikeRes,
+  createPostLikeFindRes,
+  commentsToFlatNodes,
+  getPageFromProps,
 } from '../utils';
 import { PostListing } from './post-listing';
+import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
 import { SortSelect } from './sort-select';
 import { CommentNodes } from './comment-nodes';
 import { i18n } from '../i18next';
-import { T } from 'inferno-i18next';
 
 interface SearchState {
   q: string;
@@ -35,15 +44,32 @@ interface SearchState {
   page: number;
   searchResponse: SearchResponse;
   loading: boolean;
+  site: Site;
+  searchText: string;
+}
+
+interface SearchProps {
+  q: string;
+  type_: SearchType;
+  sort: SortType;
+  page: number;
+}
+
+interface UrlParams {
+  q?: string;
+  type_?: SearchType;
+  sort?: SortType;
+  page?: number;
 }
 
 export class Search extends Component<any, SearchState> {
   private subscription: Subscription;
   private emptyState: SearchState = {
-    q: this.getSearchQueryFromProps(this.props),
-    type_: this.getSearchTypeFromProps(this.props),
-    sort: this.getSortTypeFromProps(this.props),
-    page: this.getPageFromProps(this.props),
+    q: Search.getSearchQueryFromProps(this.props),
+    type_: Search.getSearchTypeFromProps(this.props),
+    sort: Search.getSortTypeFromProps(this.props),
+    page: getPageFromProps(this.props),
+    searchText: Search.getSearchQueryFromProps(this.props),
     searchResponse: {
       type_: null,
       posts: [],
@@ -52,28 +78,38 @@ export class Search extends Component<any, SearchState> {
       users: [],
     },
     loading: false,
+    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,
+    },
   };
 
-  getSearchQueryFromProps(props: any): string {
+  static getSearchQueryFromProps(props: any): string {
     return props.match.params.q ? props.match.params.q : '';
   }
 
-  getSearchTypeFromProps(props: any): SearchType {
+  static getSearchTypeFromProps(props: any): SearchType {
     return props.match.params.type
       ? routeSearchTypeToEnum(props.match.params.type)
       : SearchType.All;
   }
 
-  getSortTypeFromProps(props: any): SortType {
+  static getSortTypeFromProps(props: any): SortType {
     return props.match.params.sort
       ? routeSortTypeToEnum(props.match.params.sort)
       : SortType.TopAll;
   }
 
-  getPageFromProps(props: any): number {
-    return props.match.params.page ? Number(props.match.params.page) : 1;
-  }
-
   constructor(props: any, context: any) {
     super(props, context);
 
@@ -88,6 +124,8 @@ export class Search extends Component<any, SearchState> {
         () => console.log('complete')
       );
 
+    WebSocketService.Instance.getSite();
+
     if (this.state.q) {
       this.search();
     }
@@ -97,47 +135,55 @@ export class Search extends Component<any, SearchState> {
     this.subscription.unsubscribe();
   }
 
-  // Necessary for back button for some reason
-  componentWillReceiveProps(nextProps: any) {
+  static getDerivedStateFromProps(props: any): SearchProps {
+    return {
+      q: Search.getSearchQueryFromProps(props),
+      type_: Search.getSearchTypeFromProps(props),
+      sort: Search.getSortTypeFromProps(props),
+      page: getPageFromProps(props),
+    };
+  }
+
+  componentDidUpdate(_: any, lastState: SearchState) {
     if (
-      nextProps.history.action == 'POP' ||
-      nextProps.history.action == 'PUSH'
+      lastState.q !== this.state.q ||
+      lastState.type_ !== this.state.type_ ||
+      lastState.sort !== this.state.sort ||
+      lastState.page !== this.state.page
     ) {
-      this.state = this.emptyState;
-      this.state.q = this.getSearchQueryFromProps(nextProps);
-      this.state.type_ = this.getSearchTypeFromProps(nextProps);
-      this.state.sort = this.getSortTypeFromProps(nextProps);
-      this.state.page = this.getPageFromProps(nextProps);
-      this.setState(this.state);
+      this.setState({ loading: true, searchText: this.state.q });
       this.search();
     }
   }
 
-  componentDidMount() {
-    document.title = `${i18n.t('search')} - ${
-      WebSocketService.Instance.site.name
-    }`;
+  get documentTitle(): string {
+    if (this.state.site.name) {
+      if (this.state.q) {
+        return `${i18n.t('search')} - ${this.state.q} - ${
+          this.state.site.name
+        }`;
+      } else {
+        return `${i18n.t('search')} - ${this.state.site.name}`;
+      }
+    } else {
+      return 'Lemmy';
+    }
   }
 
   render() {
     return (
       <div class="container">
-        <div class="row">
-          <div class="col-12">
-            <h5>
-              <T i18nKey="search">#</T>
-            </h5>
-            {this.selects()}
-            {this.searchForm()}
-            {this.state.type_ == SearchType.All && this.all()}
-            {this.state.type_ == SearchType.Comments && this.comments()}
-            {this.state.type_ == SearchType.Posts && this.posts()}
-            {this.state.type_ == SearchType.Communities && this.communities()}
-            {this.state.type_ == SearchType.Users && this.users()}
-            {this.noResults()}
-            {this.paginator()}
-          </div>
-        </div>
+        <Helmet title={this.documentTitle} />
+        <h5>{i18n.t('search')}</h5>
+        {this.selects()}
+        {this.searchForm()}
+        {this.state.type_ == SearchType.All && this.all()}
+        {this.state.type_ == SearchType.Comments && this.comments()}
+        {this.state.type_ == SearchType.Posts && this.posts()}
+        {this.state.type_ == SearchType.Communities && this.communities()}
+        {this.state.type_ == SearchType.Users && this.users()}
+        {this.resultsCount() == 0 && <span>{i18n.t('no_results')}</span>}
+        {this.paginator()}
       </div>
     );
   }
@@ -150,22 +196,20 @@ export class Search extends Component<any, SearchState> {
       >
         <input
           type="text"
-          class="form-control mr-2"
-          value={this.state.q}
+          class="form-control mr-2 mb-2"
+          value={this.state.searchText}
           placeholder={`${i18n.t('search')}...`}
           onInput={linkEvent(this, this.handleQChange)}
           required
           minLength={3}
         />
-        <button type="submit" class="btn btn-secondary mr-2">
+        <button type="submit" class="btn btn-secondary mr-2 mb-2">
           {this.state.loading ? (
             <svg class="icon icon-spinner spin">
               <use xlinkHref="#icon-spinner"></use>
             </svg>
           ) : (
-            <span>
-              <T i18nKey="search">#</T>
-            </span>
+            <span>{i18n.t('search')}</span>
           )}
         </button>
       </form>
@@ -178,26 +222,16 @@ export class Search extends Component<any, SearchState> {
         <select
           value={this.state.type_}
           onChange={linkEvent(this, this.handleTypeChange)}
-          class="custom-select custom-select-sm w-auto"
+          class="custom-select w-auto mb-2"
         >
-          <option disabled>
-            <T i18nKey="type">#</T>
-          </option>
-          <option value={SearchType.All}>
-            <T i18nKey="all">#</T>
-          </option>
-          <option value={SearchType.Comments}>
-            <T i18nKey="comments">#</T>
-          </option>
-          <option value={SearchType.Posts}>
-            <T i18nKey="posts">#</T>
-          </option>
+          <option disabled>{i18n.t('type')}</option>
+          <option value={SearchType.All}>{i18n.t('all')}</option>
+          <option value={SearchType.Comments}>{i18n.t('comments')}</option>
+          <option value={SearchType.Posts}>{i18n.t('posts')}</option>
           <option value={SearchType.Communities}>
-            <T i18nKey="communities">#</T>
-          </option>
-          <option value={SearchType.Users}>
-            <T i18nKey="users">#</T>
+            {i18n.t('communities')}
           </option>
+          <option value={SearchType.Users}>{i18n.t('users')}</option>
         </select>
         <span class="ml-2">
           <SortSelect
@@ -251,54 +285,47 @@ export class Search extends Component<any, SearchState> {
     return (
       <div>
         {combined.map(i => (
-          <div>
-            {i.type_ == 'posts' && (
-              <PostListing post={i.data as Post} showCommunity viewOnly />
-            )}
-            {i.type_ == 'comments' && (
-              <CommentNodes
-                nodes={[{ comment: i.data as Comment }]}
-                locked
-                noIndent
-              />
-            )}
-            {i.type_ == 'communities' && (
-              <div>
-                <span>
-                  <Link to={`/c/${(i.data as Community).name}`}>{`/c/${
-                    (i.data as Community).name
-                  }`}</Link>
-                </span>
-                <span>{` - ${(i.data as Community).title} - ${
-                  (i.data as Community).number_of_subscribers
-                } subscribers`}</span>
-              </div>
-            )}
-            {i.type_ == 'users' && (
-              <div>
-                <span>
-                  <Link
-                    className="text-info"
-                    to={`/u/${(i.data as UserView).name}`}
-                  >
-                    {(i.data as UserView).avatar && showAvatars() && (
-                      <img
-                        height="32"
-                        width="32"
-                        src={pictshareAvatarThumbnail(
-                          (i.data as UserView).avatar
-                        )}
-                        class="rounded-circle mr-1"
-                      />
-                    )}
-                    <span>{`/u/${(i.data as UserView).name}`}</span>
-                  </Link>
-                </span>
-                <span>{` - ${
-                  (i.data as UserView).comment_score
-                } comment karma`}</span>
-              </div>
-            )}
+          <div class="row">
+            <div class="col-12">
+              {i.type_ == 'posts' && (
+                <PostListing
+                  key={(i.data as Post).id}
+                  post={i.data as Post}
+                  showCommunity
+                  enableDownvotes={this.state.site.enable_downvotes}
+                  enableNsfw={this.state.site.enable_nsfw}
+                />
+              )}
+              {i.type_ == 'comments' && (
+                <CommentNodes
+                  key={(i.data as Comment).id}
+                  nodes={[{ comment: i.data as Comment }]}
+                  locked
+                  noIndent
+                  enableDownvotes={this.state.site.enable_downvotes}
+                />
+              )}
+              {i.type_ == 'communities' && (
+                <div>{this.communityListing(i.data as Community)}</div>
+              )}
+              {i.type_ == 'users' && (
+                <div>
+                  <span>
+                    <UserListing
+                      user={{
+                        name: (i.data as UserView).name,
+                        preferred_username: (i.data as UserView)
+                          .preferred_username,
+                        avatar: (i.data as UserView).avatar,
+                      }}
+                    />
+                  </span>
+                  <span>{` - ${i18n.t('number_of_comments', {
+                    count: (i.data as UserView).number_of_comments,
+                  })}`}</span>
+                </div>
+              )}
+            </div>
           </div>
         ))}
       </div>
@@ -307,55 +334,84 @@ export class Search extends Component<any, SearchState> {
 
   comments() {
     return (
-      <div>
-        {this.state.searchResponse.comments.map(comment => (
-          <CommentNodes nodes={[{ comment: comment }]} locked noIndent />
-        ))}
-      </div>
+      <CommentNodes
+        nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
+        locked
+        noIndent
+        enableDownvotes={this.state.site.enable_downvotes}
+      />
     );
   }
 
   posts() {
     return (
-      <div>
+      <>
         {this.state.searchResponse.posts.map(post => (
-          <PostListing post={post} showCommunity viewOnly />
+          <div class="row">
+            <div class="col-12">
+              <PostListing
+                post={post}
+                showCommunity
+                enableDownvotes={this.state.site.enable_downvotes}
+                enableNsfw={this.state.site.enable_nsfw}
+              />
+            </div>
+          </div>
         ))}
-      </div>
+      </>
     );
   }
 
   // Todo possibly create UserListing and CommunityListing
   communities() {
     return (
-      <div>
+      <>
         {this.state.searchResponse.communities.map(community => (
-          <div>
-            <span>
-              <Link to={`/c/${community.name}`}>{`/c/${community.name}`}</Link>
-            </span>
-            <span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span>
+          <div class="row">
+            <div class="col-12">{this.communityListing(community)}</div>
           </div>
         ))}
-      </div>
+      </>
+    );
+  }
+
+  communityListing(community: Community) {
+    return (
+      <>
+        <span>
+          <CommunityLink community={community} />
+        </span>
+        <span>{` - ${community.title} - 
+        ${i18n.t('number_of_subscribers', {
+          count: community.number_of_subscribers,
+        })}
+      `}</span>
+      </>
     );
   }
 
   users() {
     return (
-      <div>
+      <>
         {this.state.searchResponse.users.map(user => (
-          <div>
-            <span>
-              <Link
-                className="text-info"
-                to={`/u/${user.name}`}
-              >{`/u/${user.name}`}</Link>
-            </span>
-            <span>{` - ${user.comment_score} comment karma`}</span>
+          <div class="row">
+            <div class="col-12">
+              <span>
+                <UserListing
+                  user={{
+                    name: user.name,
+                    avatar: user.avatar,
+                    preferred_username: user.preferred_username,
+                  }}
+                />
+              </span>
+              <span>{` - ${i18n.t('number_of_comments', {
+                count: user.number_of_comments,
+              })}`}</span>
+            </div>
           </div>
         ))}
-      </div>
+      </>
     );
   }
 
@@ -364,58 +420,48 @@ export class Search extends Component<any, SearchState> {
       <div class="mt-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)}
           >
-            <T i18nKey="prev">#</T>
+            {i18n.t('prev')}
+          </button>
+        )}
+
+        {this.resultsCount() > 0 && (
+          <button
+            class="btn btn-secondary"
+            onClick={linkEvent(this, this.nextPage)}
+          >
+            {i18n.t('next')}
           </button>
         )}
-        <button
-          class="btn btn-sm btn-secondary"
-          onClick={linkEvent(this, this.nextPage)}
-        >
-          <T i18nKey="next">#</T>
-        </button>
       </div>
     );
   }
 
-  noResults() {
+  resultsCount(): number {
     let res = this.state.searchResponse;
     return (
-      <div>
-        {res &&
-          res.posts.length == 0 &&
-          res.comments.length == 0 &&
-          res.communities.length == 0 &&
-          res.users.length == 0 && (
-            <span>
-              <T i18nKey="no_results">#</T>
-            </span>
-          )}
-      </div>
+      res.posts.length +
+      res.comments.length +
+      res.communities.length +
+      res.users.length
     );
   }
 
   nextPage(i: Search) {
-    i.state.page++;
-    i.setState(i.state);
-    i.updateUrl();
-    i.search();
+    i.updateUrl({ page: i.state.page + 1 });
   }
 
   prevPage(i: Search) {
-    i.state.page--;
-    i.setState(i.state);
-    i.updateUrl();
-    i.search();
+    i.updateUrl({ page: i.state.page - 1 });
   }
 
   search() {
     let form: SearchForm = {
       q: this.state.q,
-      type_: SearchType[this.state.type_],
-      sort: SortType[this.state.sort],
+      type_: this.state.type_,
+      sort: this.state.sort,
       page: this.state.page,
       limit: fetchLimit,
     };
@@ -426,55 +472,64 @@ export class Search extends Component<any, SearchState> {
   }
 
   handleSortChange(val: SortType) {
-    this.state.sort = val;
-    this.state.page = 1;
-    this.setState(this.state);
-    this.updateUrl();
+    this.updateUrl({ sort: val, page: 1 });
   }
 
   handleTypeChange(i: Search, event: any) {
-    i.state.type_ = Number(event.target.value);
-    i.state.page = 1;
-    i.setState(i.state);
-    i.updateUrl();
+    i.updateUrl({
+      type_: SearchType[event.target.value],
+      page: 1,
+    });
   }
 
   handleSearchSubmit(i: Search, event: any) {
     event.preventDefault();
-    i.state.loading = true;
-    i.search();
-    i.setState(i.state);
-    i.updateUrl();
+    i.updateUrl({
+      q: i.state.searchText,
+      type_: i.state.type_,
+      sort: i.state.sort,
+      page: i.state.page,
+    });
   }
 
   handleQChange(i: Search, event: any) {
-    i.state.q = event.target.value;
-    i.setState(i.state);
+    i.setState({ searchText: event.target.value });
   }
 
-  updateUrl() {
-    let typeStr = SearchType[this.state.type_].toLowerCase();
-    let sortStr = SortType[this.state.sort].toLowerCase();
+  updateUrl(paramUpdates: UrlParams) {
+    const qStr = paramUpdates.q || this.state.q;
+    const typeStr = paramUpdates.type_ || this.state.type_;
+    const sortStr = paramUpdates.sort || this.state.sort;
+    const page = paramUpdates.page || this.state.page;
     this.props.history.push(
-      `/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
+      `/search/q/${qStr}/type/${typeStr}/sort/${sortStr}/page/${page}`
     );
   }
 
-  parseMessage(msg: any) {
+  parseMessage(msg: WebSocketJsonResponse) {
     console.log(msg);
     let res = wsJsonToRes(msg);
-    if (res.error) {
-      alert(i18n.t(res.error));
+    if (msg.error) {
+      toast(i18n.t(msg.error), 'danger');
       return;
     } else if (res.op == UserOperation.Search) {
       let data = res.data as SearchResponse;
       this.state.searchResponse = data;
       this.state.loading = false;
-      document.title = `${i18n.t('search')} - ${this.state.q} - ${
-        WebSocketService.Instance.site.name
-      }`;
       window.scrollTo(0, 0);
       this.setState(this.state);
+    } else if (res.op == UserOperation.CreateCommentLike) {
+      let data = res.data as CommentResponse;
+      createCommentLikeRes(data, this.state.searchResponse.comments);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.CreatePostLike) {
+      let data = res.data as PostResponse;
+      createPostLikeFindRes(data, this.state.searchResponse.posts);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.GetSite) {
+      let data = res.data as GetSiteResponse;
+      this.state.site = data.site;
+      this.setState(this.state);
     }
   }
 }