1 import { Component, linkEvent } from "inferno";
2 import { T } from "inferno-i18next-dess";
3 import { Link } from "inferno-router";
18 ListCommunitiesResponse,
29 } from "lemmy-js-client";
30 import { Subscription } from "rxjs";
31 import { i18n } from "../../i18next";
36 } from "../../interfaces";
37 import { UserService, WebSocketService } from "../../services";
42 createPostLikeFindRes,
49 getListingTypeFromProps,
59 postToCommentSortType,
61 restoreScrollPosition,
73 import { CommentNodes } from "../comment/comment-nodes";
74 import { DataTypeSelect } from "../common/data-type-select";
75 import { HtmlTags } from "../common/html-tags";
76 import { Icon, Spinner } from "../common/icon";
77 import { ListingTypeSelect } from "../common/listing-type-select";
78 import { Paginator } from "../common/paginator";
79 import { SortSelect } from "../common/sort-select";
80 import { CommunityLink } from "../community/community-link";
81 import { PostListings } from "../post/post-listings";
82 import { SiteSidebar } from "./site-sidebar";
85 trendingCommunities: CommunityView[];
86 siteRes: GetSiteResponse;
88 comments: CommentView[];
89 listingType: ListingType;
93 showSubscribedMobile: boolean;
94 showTrendingMobile: boolean;
95 showSidebarMobile: boolean;
96 subscribedCollapsed: boolean;
101 interface HomeProps {
102 listingType: ListingType;
108 interface UrlParams {
109 listingType?: ListingType;
115 export class Home extends Component<any, HomeState> {
116 private isoData = setIsoData(this.context);
117 private subscription?: Subscription;
119 trendingCommunities: [],
120 siteRes: this.isoData.site_res,
121 showSubscribedMobile: false,
122 showTrendingMobile: false,
123 showSidebarMobile: false,
124 subscribedCollapsed: false,
128 listingType: getListingTypeFromProps(
131 this.isoData.site_res.site_view.local_site.default_post_listing_type
134 dataType: getDataTypeFromProps(this.props),
135 sort: getSortTypeFromProps(this.props),
136 page: getPageFromProps(this.props),
139 constructor(props: any, context: any) {
140 super(props, context);
142 this.handleSortChange = this.handleSortChange.bind(this);
143 this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
144 this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
145 this.handlePageChange = this.handlePageChange.bind(this);
147 this.parseMessage = this.parseMessage.bind(this);
148 this.subscription = wsSubscribe(this.parseMessage);
150 // Only fetch the data if coming from another route
151 if (this.isoData.path == this.context.router.route.match.url) {
152 let postsRes = this.isoData.routeData[0] as GetPostsResponse | undefined;
153 let commentsRes = this.isoData.routeData[1] as
154 | GetCommentsResponse
156 let trendingRes = this.isoData.routeData[2] as
157 | ListCommunitiesResponse
161 this.state = { ...this.state, posts: postsRes.posts };
165 this.state = { ...this.state, comments: commentsRes.comments };
169 WebSocketService.Instance.send(
170 wsClient.communityJoin({ community_id: 0 })
173 const taglines = this.state.siteRes.taglines;
176 trendingCommunities: trendingRes?.communities ?? [],
178 tagline: getRandomFromList(taglines)?.content,
181 this.fetchTrendingCommunities();
186 fetchTrendingCommunities() {
187 let listCommunitiesForm: ListCommunities = {
188 type_: ListingType.Local,
190 limit: trendingFetchLimit,
193 WebSocketService.Instance.send(
194 wsClient.listCommunities(listCommunitiesForm)
198 componentDidMount() {
199 // This means it hasn't been set up yet
200 if (!this.state.siteRes.site_view.local_site.site_setup) {
201 this.context.router.history.push("/setup");
206 componentWillUnmount() {
207 saveScrollPosition(this.context);
208 this.subscription?.unsubscribe();
211 static getDerivedStateFromProps(
216 listingType: getListingTypeFromProps(props, state.listingType),
217 dataType: getDataTypeFromProps(props),
218 sort: getSortTypeFromProps(props),
219 page: getPageFromProps(props),
223 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
224 let pathSplit = req.path.split("/");
225 let dataType: DataType = pathSplit[3]
226 ? DataType[pathSplit[3]]
228 let mui = UserService.Instance.myUserInfo;
231 // TODO figure out auth default_listingType, default_sort_type
232 let type_: ListingType = pathSplit[5]
233 ? ListingType[pathSplit[5]]
235 ? Object.values(ListingType)[
236 mui.local_user_view.local_user.default_listing_type
239 let sort: SortType = pathSplit[7]
240 ? SortType[pathSplit[7]]
242 ? (Object.values(SortType)[
243 mui.local_user_view.local_user.default_sort_type
247 let page = pathSplit[9] ? Number(pathSplit[9]) : 1;
249 let promises: Promise<any>[] = [];
251 if (dataType == DataType.Post) {
252 let getPostsForm: GetPosts = {
261 promises.push(req.client.getPosts(getPostsForm));
262 promises.push(Promise.resolve());
264 let getCommentsForm: GetComments = {
267 sort: postToCommentSortType(sort),
272 promises.push(Promise.resolve());
273 promises.push(req.client.getComments(getCommentsForm));
276 let trendingCommunitiesForm: ListCommunities = {
277 type_: ListingType.Local,
279 limit: trendingFetchLimit,
282 promises.push(req.client.listCommunities(trendingCommunitiesForm));
287 componentDidUpdate(_: any, lastState: HomeState) {
289 lastState.listingType !== this.state.listingType ||
290 lastState.dataType !== this.state.dataType ||
291 lastState.sort !== this.state.sort ||
292 lastState.page !== this.state.page
294 this.setState({ loading: true });
299 get documentTitle(): string {
300 let siteView = this.state.siteRes.site_view;
301 let desc = this.state.siteRes.site_view.site.description;
302 return desc ? `${siteView.site.name} - ${desc}` : siteView.site.name;
306 let tagline = this.state.tagline;
309 <div className="container-lg">
311 title={this.documentTitle}
312 path={this.context.router.route.match.url}
314 {this.state.siteRes.site_view.local_site.site_setup && (
315 <div className="row">
316 <main role="main" className="col-12 col-md-8">
320 dangerouslySetInnerHTML={mdToHtml(tagline)}
323 <div className="d-block d-md-none">{this.mobileView()}</div>
326 <aside className="d-none d-md-block col-md-4">
335 get hasFollows(): boolean {
336 let mui = UserService.Instance.myUserInfo;
337 return !!mui && mui.follows.length > 0;
341 let siteRes = this.state.siteRes;
342 let siteView = siteRes.site_view;
344 <div className="row">
345 <div className="col-12">
346 {this.hasFollows && (
348 className="btn btn-secondary d-inline-block mb-2 mr-3"
349 onClick={linkEvent(this, this.handleShowSubscribedMobile)}
351 {i18n.t("subscribed")}{" "}
354 this.state.showSubscribedMobile
358 classes="icon-inline"
363 className="btn btn-secondary d-inline-block mb-2 mr-3"
364 onClick={linkEvent(this, this.handleShowTrendingMobile)}
366 {i18n.t("trending")}{" "}
369 this.state.showTrendingMobile ? `minus-square` : `plus-square`
371 classes="icon-inline"
375 className="btn btn-secondary d-inline-block mb-2 mr-3"
376 onClick={linkEvent(this, this.handleShowSidebarMobile)}
378 {i18n.t("sidebar")}{" "}
381 this.state.showSidebarMobile ? `minus-square` : `plus-square`
383 classes="icon-inline"
386 {this.state.showSidebarMobile && (
389 admins={siteRes.admins}
390 counts={siteView.counts}
391 online={siteRes.online}
392 showLocal={showLocal(this.isoData)}
395 {this.state.showTrendingMobile && (
396 <div className="col-12 card border-secondary mb-3">
397 <div className="card-body">{this.trendingCommunities()}</div>
400 {this.state.showSubscribedMobile && (
401 <div className="col-12 card border-secondary mb-3">
402 <div className="card-body">{this.subscribedCommunities()}</div>
411 let siteRes = this.state.siteRes;
412 let siteView = siteRes.site_view;
415 {!this.state.loading && (
417 <div className="card border-secondary mb-3">
418 <div className="card-body">
419 {this.trendingCommunities()}
420 {canCreateCommunity(this.state.siteRes) &&
421 this.createCommunityButton()}
422 {this.exploreCommunitiesButton()}
427 admins={siteRes.admins}
428 counts={siteView.counts}
429 online={siteRes.online}
430 showLocal={showLocal(this.isoData)}
432 {this.hasFollows && (
433 <div className="card border-secondary mb-3">
434 <div className="card-body">{this.subscribedCommunities()}</div>
443 createCommunityButton() {
445 <Link className="mt-2 btn btn-secondary btn-block" to="/create_community">
446 {i18n.t("create_a_community")}
451 exploreCommunitiesButton() {
453 <Link className="btn btn-secondary btn-block" to="/communities">
454 {i18n.t("explore_communities")}
459 trendingCommunities() {
463 <T i18nKey="trending_communities">
465 <Link className="text-body" to="/communities">
470 <ul className="list-inline mb-0">
471 {this.state.trendingCommunities.map(cv => (
473 key={cv.community.id}
474 className="list-inline-item d-inline-block"
476 <CommunityLink community={cv.community} />
484 subscribedCommunities() {
488 <T class="d-inline" i18nKey="subscribed_to_communities">
490 <Link className="text-body" to="/communities">
495 className="btn btn-sm text-muted"
496 onClick={linkEvent(this, this.handleCollapseSubscribe)}
497 aria-label={i18n.t("collapse")}
498 data-tippy-content={i18n.t("collapse")}
500 {this.state.subscribedCollapsed ? (
501 <Icon icon="plus-square" classes="icon-inline" />
503 <Icon icon="minus-square" classes="icon-inline" />
507 {!this.state.subscribedCollapsed && (
508 <ul className="list-inline mb-0">
509 {UserService.Instance.myUserInfo?.follows.map(cfv => (
511 key={cfv.community.id}
512 className="list-inline-item d-inline-block"
514 <CommunityLink community={cfv.community} />
523 updateUrl(paramUpdates: UrlParams) {
524 const listingTypeStr = paramUpdates.listingType || this.state.listingType;
525 const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
526 const sortStr = paramUpdates.sort || this.state.sort;
527 const page = paramUpdates.page || this.state.page;
528 this.props.history.push(
529 `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
535 <div className="main-content-wrapper">
536 {this.state.loading ? (
545 page={this.state.page}
546 onChange={this.handlePageChange}
555 return this.state.dataType == DataType.Post ? (
557 posts={this.state.posts}
560 enableDownvotes={enableDownvotes(this.state.siteRes)}
561 enableNsfw={enableNsfw(this.state.siteRes)}
562 allLanguages={this.state.siteRes.all_languages}
563 siteLanguages={this.state.siteRes.discussion_languages}
567 nodes={commentsToFlatNodes(this.state.comments)}
568 viewType={CommentViewType.Flat}
572 enableDownvotes={enableDownvotes(this.state.siteRes)}
573 allLanguages={this.state.siteRes.all_languages}
574 siteLanguages={this.state.siteRes.discussion_languages}
580 let allRss = `/feeds/all.xml?sort=${this.state.sort}`;
581 let localRss = `/feeds/local.xml?sort=${this.state.sort}`;
582 let auth = myAuth(false);
584 ? `/feeds/front/${auth}.xml?sort=${this.state.sort}`
588 <div className="mb-3">
589 <span className="mr-3">
591 type_={this.state.dataType}
592 onChange={this.handleDataTypeChange}
595 <span className="mr-3">
597 type_={this.state.listingType}
598 showLocal={showLocal(this.isoData)}
600 onChange={this.handleListingTypeChange}
603 <span className="mr-2">
604 <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
606 {this.state.listingType == ListingType.All && (
608 <a href={allRss} rel={relTags} title="RSS">
609 <Icon icon="rss" classes="text-muted small" />
611 <link rel="alternate" type="application/atom+xml" href={allRss} />
614 {this.state.listingType == ListingType.Local && (
616 <a href={localRss} rel={relTags} title="RSS">
617 <Icon icon="rss" classes="text-muted small" />
619 <link rel="alternate" type="application/atom+xml" href={localRss} />
622 {this.state.listingType == ListingType.Subscribed && frontRss && (
624 <a href={frontRss} title="RSS" rel={relTags}>
625 <Icon icon="rss" classes="text-muted small" />
627 <link rel="alternate" type="application/atom+xml" href={frontRss} />
634 handleShowSubscribedMobile(i: Home) {
635 i.setState({ showSubscribedMobile: !i.state.showSubscribedMobile });
638 handleShowTrendingMobile(i: Home) {
639 i.setState({ showTrendingMobile: !i.state.showTrendingMobile });
642 handleShowSidebarMobile(i: Home) {
643 i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
646 handleCollapseSubscribe(i: Home) {
647 i.setState({ subscribedCollapsed: !i.state.subscribedCollapsed });
650 handlePageChange(page: number) {
651 this.updateUrl({ page });
652 window.scrollTo(0, 0);
655 handleSortChange(val: SortType) {
656 this.updateUrl({ sort: val, page: 1 });
657 window.scrollTo(0, 0);
660 handleListingTypeChange(val: ListingType) {
661 this.updateUrl({ listingType: val, page: 1 });
662 window.scrollTo(0, 0);
665 handleDataTypeChange(val: DataType) {
666 this.updateUrl({ dataType: DataType[val], page: 1 });
667 window.scrollTo(0, 0);
671 let auth = myAuth(false);
672 if (this.state.dataType == DataType.Post) {
673 let getPostsForm: GetPosts = {
674 page: this.state.page,
676 sort: this.state.sort,
678 type_: this.state.listingType,
682 WebSocketService.Instance.send(wsClient.getPosts(getPostsForm));
684 let getCommentsForm: GetComments = {
685 page: this.state.page,
687 sort: postToCommentSortType(this.state.sort),
689 type_: this.state.listingType,
692 WebSocketService.Instance.send(wsClient.getComments(getCommentsForm));
696 parseMessage(msg: any) {
697 let op = wsUserOp(msg);
700 toast(i18n.t(msg.error), "danger");
702 } else if (msg.reconnect) {
703 WebSocketService.Instance.send(
704 wsClient.communityJoin({ community_id: 0 })
707 } else if (op == UserOperation.ListCommunities) {
708 let data = wsJsonToRes<ListCommunitiesResponse>(msg);
709 this.setState({ trendingCommunities: data.communities });
710 } else if (op == UserOperation.EditSite) {
711 let data = wsJsonToRes<SiteResponse>(msg);
712 this.setState(s => ((s.siteRes.site_view = data.site_view), s));
713 toast(i18n.t("site_saved"));
714 } else if (op == UserOperation.GetPosts) {
715 let data = wsJsonToRes<GetPostsResponse>(msg);
716 this.setState({ posts: data.posts, loading: false });
717 WebSocketService.Instance.send(
718 wsClient.communityJoin({ community_id: 0 })
720 restoreScrollPosition(this.context);
722 } else if (op == UserOperation.CreatePost) {
723 let data = wsJsonToRes<PostResponse>(msg);
724 let mui = UserService.Instance.myUserInfo;
726 let showPostNotifs = mui?.local_user_view.local_user.show_new_post_notifs;
728 // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
730 this.state.page == 1 &&
731 nsfwCheck(data.post_view) &&
732 !isPostBlocked(data.post_view)
734 // If you're on subscribed, only push it if you're subscribed.
735 if (this.state.listingType == ListingType.Subscribed) {
738 .map(c => c.community.id)
739 .includes(data.post_view.community.id)
741 this.state.posts.unshift(data.post_view);
742 if (showPostNotifs) {
743 notifyPost(data.post_view, this.context.router);
746 } else if (this.state.listingType == ListingType.Local) {
747 // If you're on the local view, only push it if its local
748 if (data.post_view.post.local) {
749 this.state.posts.unshift(data.post_view);
750 if (showPostNotifs) {
751 notifyPost(data.post_view, this.context.router);
755 this.state.posts.unshift(data.post_view);
756 if (showPostNotifs) {
757 notifyPost(data.post_view, this.context.router);
760 this.setState(this.state);
763 op == UserOperation.EditPost ||
764 op == UserOperation.DeletePost ||
765 op == UserOperation.RemovePost ||
766 op == UserOperation.LockPost ||
767 op == UserOperation.FeaturePost ||
768 op == UserOperation.SavePost
770 let data = wsJsonToRes<PostResponse>(msg);
771 editPostFindRes(data.post_view, this.state.posts);
772 this.setState(this.state);
773 } else if (op == UserOperation.CreatePostLike) {
774 let data = wsJsonToRes<PostResponse>(msg);
775 createPostLikeFindRes(data.post_view, this.state.posts);
776 this.setState(this.state);
777 } else if (op == UserOperation.AddAdmin) {
778 let data = wsJsonToRes<AddAdminResponse>(msg);
779 this.setState(s => ((s.siteRes.admins = data.admins), s));
780 } else if (op == UserOperation.BanPerson) {
781 let data = wsJsonToRes<BanPersonResponse>(msg);
783 .filter(p => p.creator.id == data.person_view.person.id)
784 .forEach(p => (p.creator.banned = data.banned));
786 this.setState(this.state);
787 } else if (op == UserOperation.GetComments) {
788 let data = wsJsonToRes<GetCommentsResponse>(msg);
789 this.setState({ comments: data.comments, loading: false });
791 op == UserOperation.EditComment ||
792 op == UserOperation.DeleteComment ||
793 op == UserOperation.RemoveComment
795 let data = wsJsonToRes<CommentResponse>(msg);
796 editCommentRes(data.comment_view, this.state.comments);
797 this.setState(this.state);
798 } else if (op == UserOperation.CreateComment) {
799 let data = wsJsonToRes<CommentResponse>(msg);
801 // Necessary since it might be a user reply
803 // If you're on subscribed, only push it if you're subscribed.
804 if (this.state.listingType == ListingType.Subscribed) {
806 UserService.Instance.myUserInfo?.follows
807 .map(c => c.community.id)
808 .includes(data.comment_view.community.id)
810 this.state.comments.unshift(data.comment_view);
813 this.state.comments.unshift(data.comment_view);
815 this.setState(this.state);
817 } else if (op == UserOperation.SaveComment) {
818 let data = wsJsonToRes<CommentResponse>(msg);
819 saveCommentRes(data.comment_view, this.state.comments);
820 this.setState(this.state);
821 } else if (op == UserOperation.CreateCommentLike) {
822 let data = wsJsonToRes<CommentResponse>(msg);
823 createCommentLikeRes(data.comment_view, this.state.comments);
824 this.setState(this.state);
825 } else if (op == UserOperation.BlockPerson) {
826 let data = wsJsonToRes<BlockPersonResponse>(msg);
827 updatePersonBlock(data);
828 } else if (op == UserOperation.CreatePostReport) {
829 let data = wsJsonToRes<PostReportResponse>(msg);
831 toast(i18n.t("report_created"));
833 } else if (op == UserOperation.CreateCommentReport) {
834 let data = wsJsonToRes<CommentReportResponse>(msg);
836 toast(i18n.t("report_created"));
839 op == UserOperation.PurgePerson ||
840 op == UserOperation.PurgePost ||
841 op == UserOperation.PurgeComment ||
842 op == UserOperation.PurgeCommunity
844 let data = wsJsonToRes<PurgeItemResponse>(msg);
846 toast(i18n.t("purge_success"));
847 this.context.router.history.push(`/`);