]> Untitled Git - lemmy.git/blob - ui/src/components/search.tsx
Spanish translations
[lemmy.git] / ui / src / components / search.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { Subscription } from "rxjs";
4 import { retryWhen, delay, take } from 'rxjs/operators';
5 import { UserOperation, Post, Comment, Community, UserView, SortType, SearchForm, SearchResponse, SearchType } from '../interfaces';
6 import { WebSocketService } from '../services';
7 import { msgOp, fetchLimit, routeSearchTypeToEnum, routeSortTypeToEnum } from '../utils';
8 import { PostListing } from './post-listing';
9 import { CommentNodes } from './comment-nodes';
10 import { i18n } from '../i18next';
11 import { T } from 'inferno-i18next';
12
13 interface SearchState {
14   q: string,
15   type_: SearchType,
16   sort: SortType,
17   page: number,
18   searchResponse: SearchResponse;
19   loading: boolean;
20 }
21
22 export class Search extends Component<any, SearchState> {
23
24   private subscription: Subscription;
25   private emptyState: SearchState = {
26     q: this.getSearchQueryFromProps(this.props),
27     type_: this.getSearchTypeFromProps(this.props),
28     sort: this.getSortTypeFromProps(this.props),
29     page: this.getPageFromProps(this.props),
30     searchResponse: {
31       op: null,
32       type_: null,
33       posts: [],
34       comments: [],
35       communities: [],
36       users: [],
37     },
38     loading: false,
39   }
40
41   getSearchQueryFromProps(props: any): string {
42     return (props.match.params.q) ? props.match.params.q : '';
43   }
44
45   getSearchTypeFromProps(props: any): SearchType {
46     return (props.match.params.type) ? 
47       routeSearchTypeToEnum(props.match.params.type) : 
48       SearchType.All;
49   }
50
51   getSortTypeFromProps(props: any): SortType {
52     return (props.match.params.sort) ? 
53       routeSortTypeToEnum(props.match.params.sort) : 
54       SortType.TopAll;
55   }
56
57   getPageFromProps(props: any): number {
58     return (props.match.params.page) ? Number(props.match.params.page) : 1;
59   }
60
61   constructor(props: any, context: any) {
62     super(props, context);
63
64     this.state = this.emptyState;
65
66     this.subscription = WebSocketService.Instance.subject
67     .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
68     .subscribe(
69       (msg) => this.parseMessage(msg),
70         (err) => console.error(err),
71         () => console.log('complete')
72     );
73     
74     if (this.state.q) {
75       this.search();
76     }
77
78   }
79
80   componentWillUnmount() {
81     this.subscription.unsubscribe();
82   }
83
84   // Necessary for back button for some reason
85   componentWillReceiveProps(nextProps: any) {
86     if (nextProps.history.action == 'POP' || nextProps.history.action == 'PUSH') {
87       this.state = this.emptyState;
88       this.state.q = this.getSearchQueryFromProps(nextProps);
89       this.state.type_ = this.getSearchTypeFromProps(nextProps);
90       this.state.sort = this.getSortTypeFromProps(nextProps);
91       this.state.page = this.getPageFromProps(nextProps);
92       this.setState(this.state);
93       this.search();
94     }
95   }
96
97   componentDidMount() {
98     document.title = `${i18n.t('search')} - ${WebSocketService.Instance.site.name}`;
99   }
100
101   render() {
102     return (
103       <div class="container">
104         <div class="row">
105           <div class="col-12">
106             <h5><T i18nKey="search">#</T></h5>
107             {this.selects()}
108             {this.searchForm()}
109             {this.state.type_ == SearchType.All &&
110               this.all()
111             }
112             {this.state.type_ == SearchType.Comments &&
113               this.comments()
114             }
115             {this.state.type_ == SearchType.Posts &&
116               this.posts()
117             }
118             {this.state.type_ == SearchType.Communities &&
119               this.communities()
120             }
121             {this.state.type_ == SearchType.Users &&
122               this.users()
123             }
124             {this.noResults()}
125             {this.paginator()}
126           </div>
127         </div>
128       </div>
129     )
130   }
131
132   searchForm() {
133     return (
134       <form class="form-inline" onSubmit={linkEvent(this, this.handleSearchSubmit)}>
135         <input type="text" class="form-control mr-2" value={this.state.q} placeholder={`${i18n.t('search')}...`} onInput={linkEvent(this, this.handleQChange)} required minLength={3} />
136         <button type="submit" class="btn btn-secondary mr-2">
137           {this.state.loading ?
138           <svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg> :
139           <span><T i18nKey="search">#</T></span>
140           }
141         </button>
142       </form>
143     )
144   }
145
146   selects() {
147     return (
148       <div className="mb-2">
149         <select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="custom-select custom-select-sm w-auto">
150           <option disabled><T i18nKey="type">#</T></option>
151           <option value={SearchType.All}><T i18nKey="all">#</T></option>
152           <option value={SearchType.Comments}><T i18nKey="comments">#</T></option>
153           <option value={SearchType.Posts}><T i18nKey="posts">#</T></option>
154           <option value={SearchType.Communities}><T i18nKey="communities">#</T></option>
155           <option value={SearchType.Users}><T i18nKey="users">#</T></option>
156         </select>
157         <select value={this.state.sort} onChange={linkEvent(this, this.handleSortChange)} class="custom-select custom-select-sm w-auto ml-2">
158           <option disabled><T i18nKey="sort_type">#</T></option>
159           <option value={SortType.New}><T i18nKey="new">#</T></option>
160           <option value={SortType.TopDay}><T i18nKey="top_day">#</T></option>
161           <option value={SortType.TopWeek}><T i18nKey="week">#</T></option>
162           <option value={SortType.TopMonth}><T i18nKey="month">#</T></option>
163           <option value={SortType.TopYear}><T i18nKey="year">#</T></option>
164           <option value={SortType.TopAll}><T i18nKey="all">#</T></option>
165         </select>
166       </div>
167     )
168
169   }
170
171   all() {
172     let combined: Array<{type_: string, data: Comment | Post | Community | UserView}> = [];
173     let comments = this.state.searchResponse.comments.map(e => {return {type_: "comments", data: e}});
174     let posts = this.state.searchResponse.posts.map(e => {return {type_: "posts", data: e}});
175     let communities = this.state.searchResponse.communities.map(e => {return {type_: "communities", data: e}});
176     let users = this.state.searchResponse.users.map(e => {return {type_: "users", data: e}});
177
178     combined.push(...comments);
179     combined.push(...posts);
180     combined.push(...communities);
181     combined.push(...users);
182
183     // Sort it
184     if (this.state.sort == SortType.New) {
185       combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
186     } else {
187       combined.sort((a, b) => ((b.data as Comment | Post).score 
188         | (b.data as Community).number_of_subscribers
189           | (b.data as UserView).comment_score) 
190           - ((a.data as Comment | Post).score 
191             | (a.data as Community).number_of_subscribers 
192               | (a.data as UserView).comment_score));
193     }
194
195     return (
196       <div>
197         {combined.map(i =>
198           <div>
199             {i.type_ == "posts" &&
200               <PostListing post={i.data as Post} showCommunity viewOnly />
201             }
202             {i.type_ == "comments" && 
203               <CommentNodes nodes={[{comment: i.data as Comment}]} viewOnly noIndent />
204             }
205             {i.type_ == "communities" && 
206               <div>
207                 <span><Link to={`/c/${(i.data as Community).name}`}>{`/c/${(i.data as Community).name}`}</Link></span>
208                 <span>{` - ${(i.data as Community).title} - ${(i.data as Community).number_of_subscribers} subscribers`}</span>
209               </div>
210             }
211             {i.type_ == "users" && 
212               <div>
213                 <span><Link className="text-info" to={`/u/${(i.data as UserView).name}`}>{`/u/${(i.data as UserView).name}`}</Link></span>
214                 <span>{` - ${(i.data as UserView).comment_score} comment karma`}</span>
215               </div>
216             }
217           </div>
218                      )
219         }
220       </div>
221     )
222   }
223
224   comments() {
225     return (
226       <div>
227         {this.state.searchResponse.comments.map(comment => 
228           <CommentNodes nodes={[{comment: comment}]} noIndent viewOnly />
229         )}
230       </div>
231     );
232   }
233
234   posts() {
235     return (
236       <div>
237         {this.state.searchResponse.posts.map(post => 
238           <PostListing post={post} showCommunity viewOnly />
239         )}
240       </div>
241     );
242   }
243
244   // Todo possibly create UserListing and CommunityListing
245   communities() {
246     return (
247       <div>
248         {this.state.searchResponse.communities.map(community => 
249           <div>
250             <span><Link to={`/c/${community.name}`}>{`/c/${community.name}`}</Link></span>
251             <span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span>
252           </div>
253         )}
254       </div>
255     );
256   }
257
258   users() {
259     return (
260       <div>
261         {this.state.searchResponse.users.map(user => 
262           <div>
263             <span><Link className="text-info" to={`/u/${user.name}`}>{`/u/${user.name}`}</Link></span>
264             <span>{` - ${user.comment_score} comment karma`}</span>
265           </div>
266         )}
267       </div>
268     );
269   }
270
271   paginator() {
272     return (
273       <div class="mt-2">
274         {this.state.page > 1 && 
275           <button class="btn btn-sm btn-secondary mr-1" onClick={linkEvent(this, this.prevPage)}><T i18nKey="prev">#</T></button>
276         }
277         <button class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.nextPage)}><T i18nKey="next">#</T></button>
278       </div>
279     );
280   }
281
282   noResults() {
283     let res = this.state.searchResponse;
284     return (
285       <div>
286         {res && res.op && res.posts.length == 0 && res.comments.length == 0 && 
287           <span><T i18nKey="no_results">#</T></span>
288         }
289       </div>
290     )
291   }
292
293   nextPage(i: Search) { 
294     i.state.page++;
295     i.setState(i.state);
296     i.updateUrl();
297     i.search();
298   }
299
300   prevPage(i: Search) { 
301     i.state.page--;
302     i.setState(i.state);
303     i.updateUrl();
304     i.search();
305   }
306
307   search() {
308     // TODO community
309     let form: SearchForm = {
310       q: this.state.q,
311       type_: SearchType[this.state.type_],
312       sort: SortType[this.state.sort],
313       page: this.state.page,
314       limit: fetchLimit,
315     };
316
317     if (this.state.q != '') {
318       WebSocketService.Instance.search(form);
319     }
320   }
321
322   handleSortChange(i: Search, event: any) {
323     i.state.sort = Number(event.target.value);
324     i.state.page = 1;
325     i.setState(i.state);
326     i.updateUrl();
327   }
328
329   handleTypeChange(i: Search, event: any) {
330     i.state.type_ = Number(event.target.value);
331     i.state.page = 1;
332     i.setState(i.state);
333     i.updateUrl();
334   }
335
336   handleSearchSubmit(i: Search, event: any) {
337     event.preventDefault();
338     i.state.loading = true;
339     i.search();
340     i.setState(i.state);
341     i.updateUrl();
342   }
343
344   handleQChange(i: Search, event: any) {
345     i.state.q = event.target.value;
346     i.setState(i.state);
347   }
348
349   updateUrl() {
350     let typeStr = SearchType[this.state.type_].toLowerCase();
351     let sortStr = SortType[this.state.sort].toLowerCase();
352     this.props.history.push(`/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`);
353   }
354
355   parseMessage(msg: any) {
356     console.log(msg);
357     let op: UserOperation = msgOp(msg);
358     if (msg.error) {
359       alert(i18n.t(msg.error));
360       return;
361     } else if (op == UserOperation.Search) {
362       let res: SearchResponse = msg;
363       this.state.searchResponse = res;
364       this.state.loading = false;
365       document.title = `${i18n.t('search')} - ${this.state.q} - ${WebSocketService.Instance.site.name}`;
366       window.scrollTo(0,0);
367       this.setState(this.state);
368     }
369   }
370 }
371