1 import { Component, linkEvent } from 'inferno';
2 import { Link } from 'inferno-router';
3 import { Subscription } from 'rxjs';
7 GetFollowedCommunitiesResponse,
9 ListCommunitiesResponse,
25 } from 'lemmy-js-client';
26 import { DataType, InitialFetchRequest } from '../interfaces';
27 import { WebSocketService, UserService } from '../services';
28 import { PostListings } from './post-listings';
29 import { CommentNodes } from './comment-nodes';
30 import { SortSelect } from './sort-select';
31 import { ListingTypeSelect } from './listing-type-select';
32 import { DataTypeSelect } from './data-type-select';
33 import { SiteForm } from './site-form';
34 import { UserListing } from './user-listing';
35 import { CommunityLink } from './community-link';
36 import { BannerIconHeader } from './banner-icon-header';
42 getListingTypeFromProps,
49 createPostLikeFindRes,
59 import { i18n } from '../i18next';
60 import { T } from 'inferno-i18next';
61 import { HtmlTags } from './html-tags';
64 subscribedCommunities: CommunityFollowerView[];
65 trendingCommunities: CommunityView[];
66 siteRes: GetSiteResponse;
67 showEditSite: boolean;
70 comments: CommentView[];
71 listingType: ListingType;
78 listingType: ListingType;
85 listingType?: ListingType;
91 export class Main extends Component<any, MainState> {
92 private isoData = setIsoData(this.context);
93 private subscription: Subscription;
94 private emptyState: MainState = {
95 subscribedCommunities: [],
96 trendingCommunities: [],
97 siteRes: this.isoData.site_res,
102 listingType: getListingTypeFromProps(this.props),
103 dataType: getDataTypeFromProps(this.props),
104 sort: getSortTypeFromProps(this.props),
105 page: getPageFromProps(this.props),
108 constructor(props: any, context: any) {
109 super(props, context);
111 this.state = this.emptyState;
112 this.handleEditCancel = this.handleEditCancel.bind(this);
113 this.handleSortChange = this.handleSortChange.bind(this);
114 this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
115 this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
117 this.parseMessage = this.parseMessage.bind(this);
118 this.subscription = wsSubscribe(this.parseMessage);
120 // Only fetch the data if coming from another route
121 if (this.isoData.path == this.context.router.route.match.url) {
122 if (this.state.dataType == DataType.Post) {
123 this.state.posts = this.isoData.routeData[0].posts;
125 this.state.comments = this.isoData.routeData[0].comments;
127 this.state.trendingCommunities = this.isoData.routeData[1].communities;
128 if (UserService.Instance.user) {
129 this.state.subscribedCommunities = this.isoData.routeData[2].communities;
131 this.state.loading = false;
133 this.fetchTrendingCommunities();
135 if (UserService.Instance.user) {
136 WebSocketService.Instance.client.getFollowedCommunities({
137 auth: UserService.Instance.authField(),
145 fetchTrendingCommunities() {
146 let listCommunitiesForm: ListCommunities = {
149 auth: UserService.Instance.authField(false),
151 WebSocketService.Instance.client.listCommunities(listCommunitiesForm);
154 componentDidMount() {
155 // This means it hasn't been set up yet
156 if (!this.state.siteRes.site_view) {
157 this.context.router.history.push('/setup');
160 WebSocketService.Instance.client.communityJoin({ community_id: 0 });
163 componentWillUnmount() {
165 this.subscription.unsubscribe();
166 window.isoData.path = undefined;
170 static getDerivedStateFromProps(props: any): MainProps {
172 listingType: getListingTypeFromProps(props),
173 dataType: getDataTypeFromProps(props),
174 sort: getSortTypeFromProps(props),
175 page: getPageFromProps(props),
179 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
180 let pathSplit = req.path.split('/');
181 let dataType: DataType = pathSplit[3]
182 ? DataType[pathSplit[3]]
185 // TODO figure out auth default_listingType, default_sort_type
186 let type_: ListingType = pathSplit[5]
187 ? ListingType[pathSplit[5]]
188 : UserService.Instance.user
189 ? Object.values(ListingType)[
190 UserService.Instance.user.default_listing_type
193 let sort: SortType = pathSplit[7]
194 ? SortType[pathSplit[7]]
195 : UserService.Instance.user
196 ? Object.values(SortType)[UserService.Instance.user.default_sort_type]
199 let page = pathSplit[9] ? Number(pathSplit[9]) : 1;
201 let promises: Promise<any>[] = [];
203 if (dataType == DataType.Post) {
204 let getPostsForm: GetPosts = {
211 promises.push(req.client.getPosts(getPostsForm));
213 let getCommentsForm: GetComments = {
220 promises.push(req.client.getComments(getCommentsForm));
223 let trendingCommunitiesForm: ListCommunities = {
227 promises.push(req.client.listCommunities(trendingCommunitiesForm));
230 promises.push(req.client.getFollowedCommunities({ auth: req.auth }));
236 componentDidUpdate(_: any, lastState: MainState) {
238 lastState.listingType !== this.state.listingType ||
239 lastState.dataType !== this.state.dataType ||
240 lastState.sort !== this.state.sort ||
241 lastState.page !== this.state.page
243 this.setState({ loading: true });
248 get documentTitle(): string {
250 this.state.siteRes.site_view
251 ? this.state.siteRes.site_view.site.name
258 <div class="container">
260 title={this.documentTitle}
261 path={this.context.router.route.match.url}
263 {this.state.siteRes.site_view.site && (
265 <main role="main" class="col-12 col-md-8">
268 <aside class="col-12 col-md-4">{this.mySidebar()}</aside>
278 {!this.state.loading && (
280 <div class="card border-secondary mb-3">
281 <div class="card-body">
282 {this.trendingCommunities()}
283 {this.createCommunityButton()}
287 {UserService.Instance.user &&
288 this.state.subscribedCommunities.length > 0 && (
289 <div class="card border-secondary mb-3">
290 <div class="card-body">{this.subscribedCommunities()}</div>
294 <div class="card border-secondary mb-3">
295 <div class="card-body">{this.sidebar()}</div>
303 createCommunityButton() {
305 <Link className="btn btn-secondary btn-block" to="/create_community">
306 {i18n.t('create_a_community')}
311 trendingCommunities() {
315 <T i18nKey="trending_communities">
317 <Link className="text-body" to="/communities">
322 <ul class="list-inline">
323 {this.state.trendingCommunities.map(cv => (
324 <li class="list-inline-item d-inline">
325 <CommunityLink community={cv.community} />
333 subscribedCommunities() {
337 <T i18nKey="subscribed_to_communities">
339 <Link className="text-body" to="/communities">
344 <ul class="list-inline mb-0">
345 {this.state.subscribedCommunities.map(cfv => (
346 <li class="list-inline-item d-inline">
347 <CommunityLink community={cfv.community} />
356 let site = this.state.siteRes.site_view.site;
359 {!this.state.showEditSite ? (
363 {this.adminButtons()}
365 <BannerIconHeader banner={site.banner} />
369 <SiteForm site={site} onCancel={this.handleEditCancel} />
375 updateUrl(paramUpdates: UrlParams) {
376 const listingTypeStr = paramUpdates.listingType || this.state.listingType;
377 const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
378 const sortStr = paramUpdates.sort || this.state.sort;
379 const page = paramUpdates.page || this.state.page;
380 this.props.history.push(
381 `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
388 {this.state.siteRes.site_view.site.description &&
389 this.siteDescription()}
397 return <h5 class="mb-0">{`${this.documentTitle}`}</h5>;
402 <ul class="mt-1 list-inline small mb-0">
403 <li class="list-inline-item">{i18n.t('admins')}:</li>
404 {this.state.siteRes.admins.map(av => (
405 <li class="list-inline-item">
406 <UserListing user={av.user} />
414 let site_view = this.state.siteRes.site_view;
416 <ul class="my-2 list-inline">
417 <li className="list-inline-item badge badge-secondary">
418 {i18n.t('number_online', { count: this.state.siteRes.online })}
420 <li className="list-inline-item badge badge-secondary">
421 {i18n.t('number_of_users', {
422 count: site_view.counts.users,
425 <li className="list-inline-item badge badge-secondary">
426 {i18n.t('number_of_communities', {
427 count: site_view.counts.communities,
430 <li className="list-inline-item badge badge-secondary">
431 {i18n.t('number_of_posts', {
432 count: site_view.counts.posts,
435 <li className="list-inline-item badge badge-secondary">
436 {i18n.t('number_of_comments', {
437 count: site_view.counts.comments,
440 <li className="list-inline-item">
441 <Link className="badge badge-secondary" to="/modlog">
452 <ul class="list-inline mb-1 text-muted font-weight-bold">
453 <li className="list-inline-item-action">
456 onClick={linkEvent(this, this.handleEditClick)}
457 data-tippy-content={i18n.t('edit')}
459 <svg class="icon icon-inline">
460 <use xlinkHref="#icon-edit"></use>
473 dangerouslySetInnerHTML={mdToHtml(
474 this.state.siteRes.site_view.site.description
482 <div class="main-content-wrapper">
483 {this.state.loading ? (
485 <svg class="icon icon-spinner spin">
486 <use xlinkHref="#icon-spinner"></use>
501 let site = this.state.siteRes.site_view.site;
502 return this.state.dataType == DataType.Post ? (
504 posts={this.state.posts}
507 sort={this.state.sort}
508 enableDownvotes={site.enable_downvotes}
509 enableNsfw={site.enable_nsfw}
513 nodes={commentsToFlatNodes(this.state.comments)}
516 sortType={this.state.sort}
518 enableDownvotes={site.enable_downvotes}
525 <div className="mb-3">
528 type_={this.state.dataType}
529 onChange={this.handleDataTypeChange}
534 type_={this.state.listingType}
535 showLocal={this.showLocal}
536 onChange={this.handleListingTypeChange}
540 <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
542 {this.state.listingType == ListingType.All && (
544 href={`/feeds/all.xml?sort=${this.state.sort}`}
549 <svg class="icon text-muted small">
550 <use xlinkHref="#icon-rss">#</use>
554 {this.state.listingType == ListingType.Local && (
556 href={`/feeds/local.xml?sort=${this.state.sort}`}
561 <svg class="icon text-muted small">
562 <use xlinkHref="#icon-rss">#</use>
566 {UserService.Instance.user &&
567 this.state.listingType == ListingType.Subscribed && (
569 href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${this.state.sort}`}
574 <svg class="icon text-muted small">
575 <use xlinkHref="#icon-rss">#</use>
586 {this.state.page > 1 && (
588 class="btn btn-secondary mr-1"
589 onClick={linkEvent(this, this.prevPage)}
594 {this.state.posts.length > 0 && (
596 class="btn btn-secondary"
597 onClick={linkEvent(this, this.nextPage)}
606 get showLocal(): boolean {
608 this.isoData.site_res.federated_instances !== null &&
609 this.isoData.site_res.federated_instances.length > 0
613 get canAdmin(): boolean {
615 UserService.Instance.user &&
616 this.state.siteRes.admins
618 .includes(UserService.Instance.user.id)
622 handleEditClick(i: Main) {
623 i.state.showEditSite = true;
628 this.state.showEditSite = false;
629 this.setState(this.state);
633 i.updateUrl({ page: i.state.page + 1 });
634 window.scrollTo(0, 0);
638 i.updateUrl({ page: i.state.page - 1 });
639 window.scrollTo(0, 0);
642 handleSortChange(val: SortType) {
643 this.updateUrl({ sort: val, page: 1 });
644 window.scrollTo(0, 0);
647 handleListingTypeChange(val: ListingType) {
648 this.updateUrl({ listingType: val, page: 1 });
649 window.scrollTo(0, 0);
652 handleDataTypeChange(val: DataType) {
653 this.updateUrl({ dataType: DataType[val], page: 1 });
654 window.scrollTo(0, 0);
658 if (this.state.dataType == DataType.Post) {
659 let getPostsForm: GetPosts = {
660 page: this.state.page,
662 sort: this.state.sort,
663 type_: this.state.listingType,
664 auth: UserService.Instance.authField(false),
666 WebSocketService.Instance.client.getPosts(getPostsForm);
668 let getCommentsForm: GetComments = {
669 page: this.state.page,
671 sort: this.state.sort,
672 type_: this.state.listingType,
673 auth: UserService.Instance.authField(false),
675 WebSocketService.Instance.client.getComments(getCommentsForm);
679 parseMessage(msg: any) {
680 let op = wsUserOp(msg);
682 toast(i18n.t(msg.error), 'danger');
684 } else if (msg.reconnect) {
685 WebSocketService.Instance.client.communityJoin({ community_id: 0 });
687 } else if (op == UserOperation.GetFollowedCommunities) {
688 let data = wsJsonToRes<GetFollowedCommunitiesResponse>(msg).data;
689 this.state.subscribedCommunities = data.communities;
690 this.setState(this.state);
691 } else if (op == UserOperation.ListCommunities) {
692 let data = wsJsonToRes<ListCommunitiesResponse>(msg).data;
693 this.state.trendingCommunities = data.communities;
694 this.setState(this.state);
695 } else if (op == UserOperation.EditSite) {
696 let data = wsJsonToRes<SiteResponse>(msg).data;
697 this.state.siteRes.site_view = data.site_view;
698 this.state.showEditSite = false;
699 this.setState(this.state);
700 toast(i18n.t('site_saved'));
701 } else if (op == UserOperation.GetPosts) {
702 let data = wsJsonToRes<GetPostsResponse>(msg).data;
703 this.state.posts = data.posts;
704 this.state.loading = false;
705 this.setState(this.state);
707 } else if (op == UserOperation.CreatePost) {
708 let data = wsJsonToRes<PostResponse>(msg).data;
710 // If you're on subscribed, only push it if you're subscribed.
711 if (this.state.listingType == ListingType.Subscribed) {
713 this.state.subscribedCommunities
714 .map(c => c.community.id)
715 .includes(data.post_view.community.id)
717 this.state.posts.unshift(data.post_view);
718 notifyPost(data.post_view, this.context.router);
722 let nsfw = data.post_view.post.nsfw || data.post_view.community.nsfw;
724 // Don't push the post if its nsfw, and don't have that setting on
728 UserService.Instance.user &&
729 UserService.Instance.user.show_nsfw)
731 this.state.posts.unshift(data.post_view);
732 notifyPost(data.post_view, this.context.router);
735 this.setState(this.state);
737 op == UserOperation.EditPost ||
738 op == UserOperation.DeletePost ||
739 op == UserOperation.RemovePost ||
740 op == UserOperation.LockPost ||
741 op == UserOperation.StickyPost ||
742 op == UserOperation.SavePost
744 let data = wsJsonToRes<PostResponse>(msg).data;
745 editPostFindRes(data.post_view, this.state.posts);
746 this.setState(this.state);
747 } else if (op == UserOperation.CreatePostLike) {
748 let data = wsJsonToRes<PostResponse>(msg).data;
749 createPostLikeFindRes(data.post_view, this.state.posts);
750 this.setState(this.state);
751 } else if (op == UserOperation.AddAdmin) {
752 let data = wsJsonToRes<AddAdminResponse>(msg).data;
753 this.state.siteRes.admins = data.admins;
754 this.setState(this.state);
755 } else if (op == UserOperation.BanUser) {
756 let data = wsJsonToRes<BanUserResponse>(msg).data;
757 let found = this.state.siteRes.banned.find(
758 u => (u.user.id = data.user_view.user.id)
761 // Remove the banned if its found in the list, and the action is an unban
762 if (found && !data.banned) {
763 this.state.siteRes.banned = this.state.siteRes.banned.filter(
764 i => i.user.id !== data.user_view.user.id
767 this.state.siteRes.banned.push(data.user_view);
771 .filter(p => p.creator.id == data.user_view.user.id)
772 .forEach(p => (p.creator.banned = data.banned));
774 this.setState(this.state);
775 } else if (op == UserOperation.GetComments) {
776 let data = wsJsonToRes<GetCommentsResponse>(msg).data;
777 this.state.comments = data.comments;
778 this.state.loading = false;
779 this.setState(this.state);
781 op == UserOperation.EditComment ||
782 op == UserOperation.DeleteComment ||
783 op == UserOperation.RemoveComment
785 let data = wsJsonToRes<CommentResponse>(msg).data;
786 editCommentRes(data.comment_view, this.state.comments);
787 this.setState(this.state);
788 } else if (op == UserOperation.CreateComment) {
789 let data = wsJsonToRes<CommentResponse>(msg).data;
791 // Necessary since it might be a user reply
792 if (data.recipient_ids.length == 0) {
793 // If you're on subscribed, only push it if you're subscribed.
794 if (this.state.listingType == ListingType.Subscribed) {
796 this.state.subscribedCommunities
797 .map(c => c.community.id)
798 .includes(data.comment_view.community.id)
800 this.state.comments.unshift(data.comment_view);
803 this.state.comments.unshift(data.comment_view);
805 this.setState(this.state);
807 } else if (op == UserOperation.SaveComment) {
808 let data = wsJsonToRes<CommentResponse>(msg).data;
809 saveCommentRes(data.comment_view, this.state.comments);
810 this.setState(this.state);
811 } else if (op == UserOperation.CreateCommentLike) {
812 let data = wsJsonToRes<CommentResponse>(msg).data;
813 createCommentLikeRes(data.comment_view, this.state.comments);
814 this.setState(this.state);