1 import { None, Option, Some } from "@sniptt/monads";
2 import { Component, linkEvent } from "inferno";
3 import { T } from "inferno-i18next-dess";
4 import { Link } from "inferno-router";
19 ListCommunitiesResponse,
30 } from "lemmy-js-client";
31 import { Subscription } from "rxjs";
32 import { i18n } from "../../i18next";
37 } from "../../interfaces";
38 import { UserService, WebSocketService } from "../../services";
44 createPostLikeFindRes,
51 getListingTypeFromProps,
56 postToCommentSortType,
58 restoreScrollPosition,
70 import { CommentNodes } from "../comment/comment-nodes";
71 import { DataTypeSelect } from "../common/data-type-select";
72 import { HtmlTags } from "../common/html-tags";
73 import { Icon, Spinner } from "../common/icon";
74 import { ListingTypeSelect } from "../common/listing-type-select";
75 import { Paginator } from "../common/paginator";
76 import { SortSelect } from "../common/sort-select";
77 import { CommunityLink } from "../community/community-link";
78 import { PostListings } from "../post/post-listings";
79 import { SiteSidebar } from "./site-sidebar";
82 trendingCommunities: CommunityView[];
83 siteRes: GetSiteResponse;
85 comments: CommentView[];
86 listingType: ListingType;
90 showSubscribedMobile: boolean;
91 showTrendingMobile: boolean;
92 showSidebarMobile: boolean;
93 subscribedCollapsed: boolean;
98 listingType: ListingType;
104 interface UrlParams {
105 listingType?: ListingType;
111 export class Home extends Component<any, HomeState> {
112 private isoData = setIsoData(
116 ListCommunitiesResponse
118 private subscription: Subscription;
119 private emptyState: HomeState = {
120 trendingCommunities: [],
121 siteRes: this.isoData.site_res,
122 showSubscribedMobile: false,
123 showTrendingMobile: false,
124 showSidebarMobile: false,
125 subscribedCollapsed: false,
129 listingType: getListingTypeFromProps(
132 this.isoData.site_res.site_view.match({
133 some: type_ => type_.site.default_post_listing_type,
134 none: ListingType.Local,
138 dataType: getDataTypeFromProps(this.props),
139 sort: getSortTypeFromProps(this.props),
140 page: getPageFromProps(this.props),
143 constructor(props: any, context: any) {
144 super(props, context);
146 this.state = this.emptyState;
147 this.handleSortChange = this.handleSortChange.bind(this);
148 this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
149 this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
150 this.handlePageChange = this.handlePageChange.bind(this);
152 this.parseMessage = this.parseMessage.bind(this);
153 this.subscription = wsSubscribe(this.parseMessage);
155 // Only fetch the data if coming from another route
156 if (this.isoData.path == this.context.router.route.match.url) {
157 let postsRes = Some(this.isoData.routeData[0] as GetPostsResponse);
158 let commentsRes = Some(this.isoData.routeData[1] as GetCommentsResponse);
159 let trendingRes = this.isoData.routeData[2] as ListCommunitiesResponse;
161 if (postsRes.isSome()) {
162 this.state = { ...this.state, posts: postsRes.unwrap().posts };
165 if (commentsRes.isSome()) {
166 this.state = { ...this.state, comments: commentsRes.unwrap().comments };
170 WebSocketService.Instance.send(
171 wsClient.communityJoin({ community_id: 0 })
176 trendingCommunities: trendingRes.communities,
180 this.fetchTrendingCommunities();
185 fetchTrendingCommunities() {
186 let listCommunitiesForm = new ListCommunities({
187 type_: Some(ListingType.Local),
188 sort: Some(SortType.Hot),
189 limit: Some(trendingFetchLimit),
191 auth: auth(false).ok(),
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.isNone()) {
201 this.context.router.history.push("/setup");
206 componentWillUnmount() {
207 saveScrollPosition(this.context);
208 this.subscription.unsubscribe();
211 static getDerivedStateFromProps(props: any): HomeProps {
213 listingType: getListingTypeFromProps(props, ListingType.Local),
214 dataType: getDataTypeFromProps(props),
215 sort: getSortTypeFromProps(props),
216 page: getPageFromProps(props),
220 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
221 let pathSplit = req.path.split("/");
222 let dataType: DataType = pathSplit[3]
223 ? DataType[pathSplit[3]]
226 // TODO figure out auth default_listingType, default_sort_type
227 let type_: Option<ListingType> = Some(
229 ? ListingType[pathSplit[5]]
230 : UserService.Instance.myUserInfo.match({
232 Object.values(ListingType)[
233 mui.local_user_view.local_user.default_listing_type
235 none: ListingType.Local,
238 let sort: Option<SortType> = Some(
240 ? SortType[pathSplit[7]]
241 : UserService.Instance.myUserInfo.match({
243 Object.values(SortType)[
244 mui.local_user_view.local_user.default_sort_type
246 none: SortType.Active,
250 let page = Some(pathSplit[9] ? Number(pathSplit[9]) : 1);
252 let promises: Promise<any>[] = [];
254 if (dataType == DataType.Post) {
255 let getPostsForm = new GetPosts({
257 community_name: None,
260 limit: Some(fetchLimit),
262 saved_only: Some(false),
266 promises.push(req.client.getPosts(getPostsForm));
267 promises.push(Promise.resolve());
269 let getCommentsForm = new GetComments({
271 community_name: None,
273 limit: Some(fetchLimit),
275 sort: sort.map(postToCommentSortType),
277 saved_only: Some(false),
282 promises.push(Promise.resolve());
283 promises.push(req.client.getComments(getCommentsForm));
286 let trendingCommunitiesForm = new ListCommunities({
287 type_: Some(ListingType.Local),
288 sort: Some(SortType.Hot),
289 limit: Some(trendingFetchLimit),
293 promises.push(req.client.listCommunities(trendingCommunitiesForm));
298 componentDidUpdate(_: any, lastState: HomeState) {
300 lastState.listingType !== this.state.listingType ||
301 lastState.dataType !== this.state.dataType ||
302 lastState.sort !== this.state.sort ||
303 lastState.page !== this.state.page
305 this.setState({ loading: true });
310 get documentTitle(): string {
311 return this.state.siteRes.site_view.match({
313 siteView.site.description.match({
314 some: desc => `${siteView.site.name} - ${desc}`,
315 none: siteView.site.name,
323 <div className="container">
325 title={this.documentTitle}
326 path={this.context.router.route.match.url}
330 {this.state.siteRes.site_view.isSome() && (
331 <div className="row">
332 <main role="main" className="col-12 col-md-8">
333 <div className="d-block d-md-none">{this.mobileView()}</div>
336 <aside className="d-none d-md-block col-md-4">
345 get hasFollows(): boolean {
346 return UserService.Instance.myUserInfo.match({
347 some: mui => mui.follows.length > 0,
353 let siteRes = this.state.siteRes;
355 <div className="row">
356 <div className="col-12">
357 {this.hasFollows && (
359 className="btn btn-secondary d-inline-block mb-2 mr-3"
360 onClick={linkEvent(this, this.handleShowSubscribedMobile)}
362 {i18n.t("subscribed")}{" "}
365 this.state.showSubscribedMobile
369 classes="icon-inline"
374 className="btn btn-secondary d-inline-block mb-2 mr-3"
375 onClick={linkEvent(this, this.handleShowTrendingMobile)}
377 {i18n.t("trending")}{" "}
380 this.state.showTrendingMobile ? `minus-square` : `plus-square`
382 classes="icon-inline"
386 className="btn btn-secondary d-inline-block mb-2 mr-3"
387 onClick={linkEvent(this, this.handleShowSidebarMobile)}
389 {i18n.t("sidebar")}{" "}
392 this.state.showSidebarMobile ? `minus-square` : `plus-square`
394 classes="icon-inline"
397 {this.state.showSidebarMobile &&
398 siteRes.site_view.match({
402 admins={Some(siteRes.admins)}
403 counts={Some(siteView.counts)}
404 online={Some(siteRes.online)}
405 showLocal={showLocal(this.isoData)}
410 {this.state.showTrendingMobile && (
411 <div className="col-12 card border-secondary mb-3">
412 <div className="card-body">{this.trendingCommunities()}</div>
415 {this.state.showSubscribedMobile && (
416 <div className="col-12 card border-secondary mb-3">
417 <div className="card-body">{this.subscribedCommunities()}</div>
426 let siteRes = this.state.siteRes;
429 {!this.state.loading && (
431 <div className="card border-secondary mb-3">
432 <div className="card-body">
433 {this.trendingCommunities()}
434 {canCreateCommunity(this.state.siteRes) &&
435 this.createCommunityButton()}
436 {this.exploreCommunitiesButton()}
439 {siteRes.site_view.match({
443 admins={Some(siteRes.admins)}
444 counts={Some(siteView.counts)}
445 online={Some(siteRes.online)}
446 showLocal={showLocal(this.isoData)}
451 {this.hasFollows && (
452 <div className="card border-secondary mb-3">
453 <div className="card-body">{this.subscribedCommunities()}</div>
462 createCommunityButton() {
464 <Link className="mt-2 btn btn-secondary btn-block" to="/create_community">
465 {i18n.t("create_a_community")}
470 exploreCommunitiesButton() {
472 <Link className="btn btn-secondary btn-block" to="/communities">
473 {i18n.t("explore_communities")}
478 trendingCommunities() {
482 <T i18nKey="trending_communities">
484 <Link className="text-body" to="/communities">
489 <ul className="list-inline mb-0">
490 {this.state.trendingCommunities.map(cv => (
492 key={cv.community.id}
493 className="list-inline-item d-inline-block"
495 <CommunityLink community={cv.community} />
503 subscribedCommunities() {
507 <T class="d-inline" i18nKey="subscribed_to_communities">
509 <Link className="text-body" to="/communities">
514 className="btn btn-sm text-muted"
515 onClick={linkEvent(this, this.handleCollapseSubscribe)}
516 aria-label={i18n.t("collapse")}
517 data-tippy-content={i18n.t("collapse")}
519 {this.state.subscribedCollapsed ? (
520 <Icon icon="plus-square" classes="icon-inline" />
522 <Icon icon="minus-square" classes="icon-inline" />
526 {!this.state.subscribedCollapsed && (
527 <ul className="list-inline mb-0">
528 {UserService.Instance.myUserInfo
533 key={cfv.community.id}
534 className="list-inline-item d-inline-block"
536 <CommunityLink community={cfv.community} />
545 updateUrl(paramUpdates: UrlParams) {
546 const listingTypeStr = paramUpdates.listingType || this.state.listingType;
547 const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
548 const sortStr = paramUpdates.sort || this.state.sort;
549 const page = paramUpdates.page || this.state.page;
550 this.props.history.push(
551 `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
557 <div className="main-content-wrapper">
558 {this.state.loading ? (
567 page={this.state.page}
568 onChange={this.handlePageChange}
577 return this.state.dataType == DataType.Post ? (
579 posts={this.state.posts}
582 enableDownvotes={enableDownvotes(this.state.siteRes)}
583 enableNsfw={enableNsfw(this.state.siteRes)}
584 allLanguages={this.state.siteRes.all_languages}
588 nodes={commentsToFlatNodes(this.state.comments)}
589 viewType={CommentViewType.Flat}
592 maxCommentsShown={None}
596 enableDownvotes={enableDownvotes(this.state.siteRes)}
597 allLanguages={this.state.siteRes.all_languages}
603 let allRss = `/feeds/all.xml?sort=${this.state.sort}`;
604 let localRss = `/feeds/local.xml?sort=${this.state.sort}`;
605 let frontRss = auth(false)
607 .map(auth => `/feeds/front/${auth}.xml?sort=${this.state.sort}`);
610 <div className="mb-3">
611 <span className="mr-3">
613 type_={this.state.dataType}
614 onChange={this.handleDataTypeChange}
617 <span className="mr-3">
619 type_={this.state.listingType}
620 showLocal={showLocal(this.isoData)}
622 onChange={this.handleListingTypeChange}
625 <span className="mr-2">
626 <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
628 {this.state.listingType == ListingType.All && (
630 <a href={allRss} rel={relTags} title="RSS">
631 <Icon icon="rss" classes="text-muted small" />
633 <link rel="alternate" type="application/atom+xml" href={allRss} />
636 {this.state.listingType == ListingType.Local && (
638 <a href={localRss} rel={relTags} title="RSS">
639 <Icon icon="rss" classes="text-muted small" />
641 <link rel="alternate" type="application/atom+xml" href={localRss} />
644 {this.state.listingType == ListingType.Subscribed &&
648 <a href={rss} title="RSS" rel={relTags}>
649 <Icon icon="rss" classes="text-muted small" />
651 <link rel="alternate" type="application/atom+xml" href={rss} />
660 handleShowSubscribedMobile(i: Home) {
661 i.setState({ showSubscribedMobile: !i.state.showSubscribedMobile });
664 handleShowTrendingMobile(i: Home) {
665 i.setState({ showTrendingMobile: !i.state.showTrendingMobile });
668 handleShowSidebarMobile(i: Home) {
669 i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
672 handleCollapseSubscribe(i: Home) {
673 i.setState({ subscribedCollapsed: !i.state.subscribedCollapsed });
676 handlePageChange(page: number) {
677 this.updateUrl({ page });
678 window.scrollTo(0, 0);
681 handleSortChange(val: SortType) {
682 this.updateUrl({ sort: val, page: 1 });
683 window.scrollTo(0, 0);
686 handleListingTypeChange(val: ListingType) {
687 this.updateUrl({ listingType: val, page: 1 });
688 window.scrollTo(0, 0);
691 handleDataTypeChange(val: DataType) {
692 this.updateUrl({ dataType: DataType[val], page: 1 });
693 window.scrollTo(0, 0);
697 if (this.state.dataType == DataType.Post) {
698 let getPostsForm = new GetPosts({
700 community_name: None,
701 page: Some(this.state.page),
702 limit: Some(fetchLimit),
703 sort: Some(this.state.sort),
704 saved_only: Some(false),
705 auth: auth(false).ok(),
706 type_: Some(this.state.listingType),
709 WebSocketService.Instance.send(wsClient.getPosts(getPostsForm));
711 let getCommentsForm = new GetComments({
713 community_name: None,
714 page: Some(this.state.page),
715 limit: Some(fetchLimit),
717 sort: Some(postToCommentSortType(this.state.sort)),
718 saved_only: Some(false),
721 auth: auth(false).ok(),
722 type_: Some(this.state.listingType),
724 WebSocketService.Instance.send(wsClient.getComments(getCommentsForm));
728 parseMessage(msg: any) {
729 let op = wsUserOp(msg);
732 toast(i18n.t(msg.error), "danger");
734 } else if (msg.reconnect) {
735 WebSocketService.Instance.send(
736 wsClient.communityJoin({ community_id: 0 })
739 } else if (op == UserOperation.ListCommunities) {
740 let data = wsJsonToRes<ListCommunitiesResponse>(
742 ListCommunitiesResponse
744 this.setState({ trendingCommunities: data.communities });
745 } else if (op == UserOperation.EditSite) {
746 let data = wsJsonToRes<SiteResponse>(msg, SiteResponse);
747 this.setState(s => ((s.siteRes.site_view = Some(data.site_view)), s));
748 toast(i18n.t("site_saved"));
749 } else if (op == UserOperation.GetPosts) {
750 let data = wsJsonToRes<GetPostsResponse>(msg, GetPostsResponse);
751 this.setState({ posts: data.posts, loading: false });
752 WebSocketService.Instance.send(
753 wsClient.communityJoin({ community_id: 0 })
755 restoreScrollPosition(this.context);
757 } else if (op == UserOperation.CreatePost) {
758 let data = wsJsonToRes<PostResponse>(msg, PostResponse);
760 let nsfw = data.post_view.post.nsfw || data.post_view.community.nsfw;
764 UserService.Instance.myUserInfo
765 .map(m => m.local_user_view.local_user.show_nsfw)
768 let showPostNotifs = UserService.Instance.myUserInfo
769 .map(m => m.local_user_view.local_user.show_new_post_notifs)
772 // Only push these if you're on the first page, and you pass the nsfw check
773 if (this.state.page == 1 && nsfwCheck) {
774 // If you're on subscribed, only push it if you're subscribed.
775 if (this.state.listingType == ListingType.Subscribed) {
777 UserService.Instance.myUserInfo
780 .map(c => c.community.id)
781 .includes(data.post_view.community.id)
783 this.state.posts.unshift(data.post_view);
784 if (showPostNotifs) {
785 notifyPost(data.post_view, this.context.router);
788 } else if (this.state.listingType == ListingType.Local) {
789 // If you're on the local view, only push it if its local
790 if (data.post_view.post.local) {
791 this.state.posts.unshift(data.post_view);
792 if (showPostNotifs) {
793 notifyPost(data.post_view, this.context.router);
797 this.state.posts.unshift(data.post_view);
798 if (showPostNotifs) {
799 notifyPost(data.post_view, this.context.router);
802 this.setState(this.state);
805 op == UserOperation.EditPost ||
806 op == UserOperation.DeletePost ||
807 op == UserOperation.RemovePost ||
808 op == UserOperation.LockPost ||
809 op == UserOperation.StickyPost ||
810 op == UserOperation.SavePost
812 let data = wsJsonToRes<PostResponse>(msg, PostResponse);
813 editPostFindRes(data.post_view, this.state.posts);
814 this.setState(this.state);
815 } else if (op == UserOperation.CreatePostLike) {
816 let data = wsJsonToRes<PostResponse>(msg, PostResponse);
817 createPostLikeFindRes(data.post_view, this.state.posts);
818 this.setState(this.state);
819 } else if (op == UserOperation.AddAdmin) {
820 let data = wsJsonToRes<AddAdminResponse>(msg, AddAdminResponse);
821 this.setState(s => ((s.siteRes.admins = data.admins), s));
822 } else if (op == UserOperation.BanPerson) {
823 let data = wsJsonToRes<BanPersonResponse>(msg, BanPersonResponse);
825 .filter(p => p.creator.id == data.person_view.person.id)
826 .forEach(p => (p.creator.banned = data.banned));
828 this.setState(this.state);
829 } else if (op == UserOperation.GetComments) {
830 let data = wsJsonToRes<GetCommentsResponse>(msg, GetCommentsResponse);
831 this.setState({ comments: data.comments, loading: false });
833 op == UserOperation.EditComment ||
834 op == UserOperation.DeleteComment ||
835 op == UserOperation.RemoveComment
837 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
838 editCommentRes(data.comment_view, this.state.comments);
839 this.setState(this.state);
840 } else if (op == UserOperation.CreateComment) {
841 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
843 // Necessary since it might be a user reply
845 // If you're on subscribed, only push it if you're subscribed.
846 if (this.state.listingType == ListingType.Subscribed) {
848 UserService.Instance.myUserInfo
851 .map(c => c.community.id)
852 .includes(data.comment_view.community.id)
854 this.state.comments.unshift(data.comment_view);
857 this.state.comments.unshift(data.comment_view);
859 this.setState(this.state);
861 } else if (op == UserOperation.SaveComment) {
862 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
863 saveCommentRes(data.comment_view, this.state.comments);
864 this.setState(this.state);
865 } else if (op == UserOperation.CreateCommentLike) {
866 let data = wsJsonToRes<CommentResponse>(msg, CommentResponse);
867 createCommentLikeRes(data.comment_view, this.state.comments);
868 this.setState(this.state);
869 } else if (op == UserOperation.BlockPerson) {
870 let data = wsJsonToRes<BlockPersonResponse>(msg, BlockPersonResponse);
871 updatePersonBlock(data);
872 } else if (op == UserOperation.CreatePostReport) {
873 let data = wsJsonToRes<PostReportResponse>(msg, PostReportResponse);
875 toast(i18n.t("report_created"));
877 } else if (op == UserOperation.CreateCommentReport) {
878 let data = wsJsonToRes<CommentReportResponse>(msg, CommentReportResponse);
880 toast(i18n.t("report_created"));
883 op == UserOperation.PurgePerson ||
884 op == UserOperation.PurgePost ||
885 op == UserOperation.PurgeComment ||
886 op == UserOperation.PurgeCommunity
888 let data = wsJsonToRes<PurgeItemResponse>(msg, PurgeItemResponse);
890 toast(i18n.t("purge_success"));
891 this.context.router.history.push(`/`);