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 {
SearchForm,
SearchResponse,
SearchType,
+ PostResponse,
+ CommentResponse,
WebSocketJsonResponse,
-} from '../interfaces';
+ 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;
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: [],
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);
() => console.log('complete')
);
+ WebSocketService.Instance.getSite();
+
if (this.state.q) {
this.search();
}
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>
);
}
>
<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>
<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
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>
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>
+ </>
);
}
<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,
};
}
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: 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);
}
}
}