1 import { NoOptionI18nKeys } from "i18next";
2 import { Component, linkEvent, MouseEventHandler } 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";
43 createPostLikeFindRes,
60 postToCommentSortType,
63 restoreScrollPosition,
65 routeListingTypeToEnum,
78 import { CommentNodes } from "../comment/comment-nodes";
79 import { DataTypeSelect } from "../common/data-type-select";
80 import { HtmlTags } from "../common/html-tags";
81 import { Icon, Spinner } from "../common/icon";
82 import { ListingTypeSelect } from "../common/listing-type-select";
83 import { Paginator } from "../common/paginator";
84 import { SortSelect } from "../common/sort-select";
85 import { CommunityLink } from "../community/community-link";
86 import { PostListings } from "../post/post-listings";
87 import { SiteSidebar } from "./site-sidebar";
90 trendingCommunities: CommunityView[];
91 siteRes: GetSiteResponse;
93 comments: CommentView[];
94 showSubscribedMobile: boolean;
95 showTrendingMobile: boolean;
96 showSidebarMobile: boolean;
97 subscribedCollapsed: boolean;
102 interface HomeProps {
103 listingType: ListingType;
109 const getDataTypeFromQuery = (type?: string) =>
110 routeDataTypeToEnum(type ?? "", DataType.Post);
112 function getListingTypeFromQuery(type?: string) {
113 const mui = UserService.Instance.myUserInfo;
115 return routeListingTypeToEnum(
118 ? Object.values(ListingType)[
119 mui.local_user_view.local_user.default_listing_type
125 function getSortTypeFromQuery(type?: string) {
126 const mui = UserService.Instance.myUserInfo;
128 return routeSortTypeToEnum(
131 ? Object.values(SortType)[
132 mui.local_user_view.local_user.default_listing_type
138 const getHomeQueryParams = () =>
139 getQueryParams<HomeProps>({
140 sort: getSortTypeFromQuery,
141 listingType: getListingTypeFromQuery,
142 page: getPageFromString,
143 dataType: getDataTypeFromQuery,
146 function fetchTrendingCommunities() {
147 const listCommunitiesForm: ListCommunities = {
148 type_: ListingType.Local,
150 limit: trendingFetchLimit,
153 WebSocketService.Instance.send(wsClient.listCommunities(listCommunitiesForm));
156 function fetchData() {
157 const auth = myAuth(false);
158 const { dataType, page, listingType, sort } = getHomeQueryParams();
161 if (dataType === DataType.Post) {
162 const getPostsForm: GetPosts = {
171 req = wsClient.getPosts(getPostsForm);
173 const getCommentsForm: GetComments = {
176 sort: postToCommentSortType(sort),
182 req = wsClient.getComments(getCommentsForm);
185 WebSocketService.Instance.send(req);
188 const MobileButton = ({
193 textKey: NoOptionI18nKeys;
195 onClick: MouseEventHandler<HTMLButtonElement>;
198 className="btn btn-secondary d-inline-block mb-2 mr-3"
201 {i18n.t(textKey)}{" "}
202 <Icon icon={show ? `minus-square` : `plus-square`} classes="icon-inline" />
206 const LinkButton = ({
211 translationKey: NoOptionI18nKeys;
213 <Link className="btn btn-secondary btn-block" to={path}>
214 {i18n.t(translationKey)}
218 function getRss(listingType: ListingType) {
219 const { sort } = getHomeQueryParams();
220 const auth = myAuth(false);
222 let rss: string | undefined = undefined;
224 switch (listingType) {
225 case ListingType.All: {
226 rss = `/feeds/all.xml?sort=${sort}`;
229 case ListingType.Local: {
230 rss = `/feeds/local.xml?sort=${sort}`;
233 case ListingType.Subscribed: {
234 rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
242 <a href={rss} rel={relTags} title="RSS">
243 <Icon icon="rss" classes="text-muted small" />
245 <link rel="alternate" type="application/atom+xml" href={rss} />
251 export class Home extends Component<any, HomeState> {
252 private isoData = setIsoData(this.context);
253 private subscription?: Subscription;
255 trendingCommunities: [],
256 siteRes: this.isoData.site_res,
257 showSubscribedMobile: false,
258 showTrendingMobile: false,
259 showSidebarMobile: false,
260 subscribedCollapsed: false,
266 constructor(props: any, context: any) {
267 super(props, context);
269 this.handleSortChange = this.handleSortChange.bind(this);
270 this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
271 this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
272 this.handlePageChange = this.handlePageChange.bind(this);
274 this.parseMessage = this.parseMessage.bind(this);
275 this.subscription = wsSubscribe(this.parseMessage);
277 // Only fetch the data if coming from another route
278 if (this.isoData.path === this.context.router.route.match.url) {
279 const postsRes = this.isoData.routeData[0] as
282 const commentsRes = this.isoData.routeData[1] as
283 | GetCommentsResponse
285 const trendingRes = this.isoData.routeData[2] as
286 | ListCommunitiesResponse
290 this.state = { ...this.state, posts: postsRes.posts };
294 this.state = { ...this.state, comments: commentsRes.comments };
298 WebSocketService.Instance.send(
299 wsClient.communityJoin({ community_id: 0 })
302 const taglines = this.state?.siteRes?.taglines ?? [];
305 trendingCommunities: trendingRes?.communities ?? [],
307 tagline: getRandomFromList(taglines)?.content,
310 fetchTrendingCommunities();
315 componentDidMount() {
316 // This means it hasn't been set up yet
317 if (!this.state.siteRes.site_view.local_site.site_setup) {
318 this.context.router.history.push("/setup");
323 componentWillUnmount() {
324 saveScrollPosition(this.context);
325 this.subscription?.unsubscribe();
328 static fetchInitialData({
331 query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
332 }: InitialFetchRequest<QueryParams<HomeProps>>): Promise<any>[] {
333 const dataType = getDataTypeFromQuery(urlDataType);
335 // TODO figure out auth default_listingType, default_sort_type
336 const type_ = getListingTypeFromQuery(listingType);
337 const sort = getSortTypeFromQuery(urlSort);
339 const page = urlPage ? Number(urlPage) : 1;
341 const promises: Promise<any>[] = [];
343 if (dataType === DataType.Post) {
344 const getPostsForm: GetPosts = {
353 promises.push(client.getPosts(getPostsForm));
354 promises.push(Promise.resolve());
356 const getCommentsForm: GetComments = {
359 sort: postToCommentSortType(sort),
364 promises.push(Promise.resolve());
365 promises.push(client.getComments(getCommentsForm));
368 const trendingCommunitiesForm: ListCommunities = {
369 type_: ListingType.Local,
371 limit: trendingFetchLimit,
374 promises.push(client.listCommunities(trendingCommunitiesForm));
379 get documentTitle(): string {
380 const { name, description } = this.state.siteRes.site_view.site;
382 return description ? `${name} - ${description}` : name;
390 local_site: { site_setup },
396 <div className="container-lg">
398 title={this.documentTitle}
399 path={this.context.router.route.match.url}
402 <div className="row">
403 <main role="main" className="col-12 col-md-8">
407 dangerouslySetInnerHTML={mdToHtml(tagline)}
410 <div className="d-block d-md-none">{this.mobileView}</div>
413 <aside className="d-none d-md-block col-md-4">
422 get hasFollows(): boolean {
423 const mui = UserService.Instance.myUserInfo;
424 return !!mui && mui.follows.length > 0;
430 site_view: { counts, site },
434 showSubscribedMobile,
440 <div className="row">
441 <div className="col-12">
442 {this.hasFollows && (
445 show={showSubscribedMobile}
446 onClick={linkEvent(this, this.handleShowSubscribedMobile)}
451 show={showTrendingMobile}
452 onClick={linkEvent(this, this.handleShowTrendingMobile)}
456 show={showSidebarMobile}
457 onClick={linkEvent(this, this.handleShowSidebarMobile)}
459 {showSidebarMobile && (
465 showLocal={showLocal(this.isoData)}
468 {showTrendingMobile && (
469 <div className="col-12 card border-secondary mb-3">
470 <div className="card-body">{this.trendingCommunities(true)}</div>
473 {showSubscribedMobile && (
474 <div className="col-12 card border-secondary mb-3">
475 <div className="card-body">{this.subscribedCommunities}</div>
486 site_view: { counts, site },
497 <div className="card border-secondary mb-3">
498 <div className="card-body">
499 {this.trendingCommunities()}
500 {canCreateCommunity(this.state.siteRes) && (
502 path="/create_community"
503 translationKey="create_a_community"
508 translationKey="explore_communities"
517 showLocal={showLocal(this.isoData)}
519 {this.hasFollows && (
520 <div className="card border-secondary mb-3">
521 <div className="card-body">{this.subscribedCommunities}</div>
530 trendingCommunities(isMobile = false) {
532 <div className={!isMobile ? "mb-2" : ""}>
534 <T i18nKey="trending_communities">
536 <Link className="text-body" to="/communities">
541 <ul className="list-inline mb-0">
542 {this.state.trendingCommunities.map(cv => (
544 key={cv.community.id}
545 className="list-inline-item d-inline-block"
547 <CommunityLink community={cv.community} />
555 get subscribedCommunities() {
556 const { subscribedCollapsed } = this.state;
561 <T class="d-inline" i18nKey="subscribed_to_communities">
563 <Link className="text-body" to="/communities">
568 className="btn btn-sm text-muted"
569 onClick={linkEvent(this, this.handleCollapseSubscribe)}
570 aria-label={i18n.t("collapse")}
571 data-tippy-content={i18n.t("collapse")}
574 icon={`${subscribedCollapsed ? "plus" : "minus"}-square`}
575 classes="icon-inline"
579 {!subscribedCollapsed && (
580 <ul className="list-inline mb-0">
581 {UserService.Instance.myUserInfo?.follows.map(cfv => (
583 key={cfv.community.id}
584 className="list-inline-item d-inline-block"
586 <CommunityLink community={cfv.community} />
595 updateUrl({ dataType, listingType, page, sort }: Partial<HomeProps>) {
597 dataType: urlDataType,
598 listingType: urlListingType,
601 } = getHomeQueryParams();
603 const queryParams: QueryParams<HomeProps> = {
604 dataType: getDataTypeString(dataType ?? urlDataType),
605 listingType: listingType ?? urlListingType,
606 page: (page ?? urlPage).toString(),
607 sort: sort ?? urlSort,
610 this.props.history.push({
612 search: getQueryString(queryParams),
625 const { page } = getHomeQueryParams();
628 <div className="main-content-wrapper">
629 {this.state.loading ? (
637 <Paginator page={page} onChange={this.handlePageChange} />
645 const { dataType } = getHomeQueryParams();
646 const { siteRes, posts, comments } = this.state;
648 return dataType === DataType.Post ? (
653 enableDownvotes={enableDownvotes(siteRes)}
654 enableNsfw={enableNsfw(siteRes)}
655 allLanguages={siteRes.all_languages}
656 siteLanguages={siteRes.discussion_languages}
660 nodes={commentsToFlatNodes(comments)}
661 viewType={CommentViewType.Flat}
665 enableDownvotes={enableDownvotes(siteRes)}
666 allLanguages={siteRes.all_languages}
667 siteLanguages={siteRes.discussion_languages}
673 const { listingType, dataType, sort } = getHomeQueryParams();
676 <div className="mb-3">
677 <span className="mr-3">
680 onChange={this.handleDataTypeChange}
683 <span className="mr-3">
686 showLocal={showLocal(this.isoData)}
688 onChange={this.handleListingTypeChange}
691 <span className="mr-2">
692 <SortSelect sort={sort} onChange={this.handleSortChange} />
694 {getRss(listingType)}
699 handleShowSubscribedMobile(i: Home) {
700 i.setState({ showSubscribedMobile: !i.state.showSubscribedMobile });
703 handleShowTrendingMobile(i: Home) {
704 i.setState({ showTrendingMobile: !i.state.showTrendingMobile });
707 handleShowSidebarMobile(i: Home) {
708 i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
711 handleCollapseSubscribe(i: Home) {
712 i.setState({ subscribedCollapsed: !i.state.subscribedCollapsed });
715 handlePageChange(page: number) {
716 this.updateUrl({ page });
717 window.scrollTo(0, 0);
720 handleSortChange(val: SortType) {
721 this.updateUrl({ sort: val, page: 1 });
722 window.scrollTo(0, 0);
725 handleListingTypeChange(val: ListingType) {
726 this.updateUrl({ listingType: val, page: 1 });
727 window.scrollTo(0, 0);
730 handleDataTypeChange(val: DataType) {
731 this.updateUrl({ dataType: val, page: 1 });
732 window.scrollTo(0, 0);
735 parseMessage(msg: any) {
736 const op = wsUserOp(msg);
740 toast(i18n.t(msg.error), "danger");
741 } else if (msg.reconnect) {
742 WebSocketService.Instance.send(
743 wsClient.communityJoin({ community_id: 0 })
748 case UserOperation.ListCommunities: {
749 const { communities } = wsJsonToRes<ListCommunitiesResponse>(msg);
750 this.setState({ trendingCommunities: communities });
755 case UserOperation.EditSite: {
756 const { site_view } = wsJsonToRes<SiteResponse>(msg);
757 this.setState(s => ((s.siteRes.site_view = site_view), s));
758 toast(i18n.t("site_saved"));
763 case UserOperation.GetPosts: {
764 const { posts } = wsJsonToRes<GetPostsResponse>(msg);
765 this.setState({ posts, loading: false });
766 WebSocketService.Instance.send(
767 wsClient.communityJoin({ community_id: 0 })
769 restoreScrollPosition(this.context);
775 case UserOperation.CreatePost: {
776 const { page, listingType } = getHomeQueryParams();
777 const { post_view } = wsJsonToRes<PostResponse>(msg);
779 // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
780 if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
781 const mui = UserService.Instance.myUserInfo;
782 const showPostNotifs =
783 mui?.local_user_view.local_user.show_new_post_notifs;
784 let shouldAddPost: boolean;
786 switch (listingType) {
787 case ListingType.Subscribed: {
788 // If you're on subscribed, only push it if you're subscribed.
789 shouldAddPost = !!mui?.follows.some(
790 ({ community: { id } }) => id === post_view.community.id
794 case ListingType.Local: {
795 // If you're on the local view, only push it if its local
796 shouldAddPost = post_view.post.local;
800 shouldAddPost = true;
806 this.setState(({ posts }) => ({
807 posts: [post_view].concat(posts),
809 if (showPostNotifs) {
810 notifyPost(post_view, this.context.router);
818 case UserOperation.EditPost:
819 case UserOperation.DeletePost:
820 case UserOperation.RemovePost:
821 case UserOperation.LockPost:
822 case UserOperation.FeaturePost:
823 case UserOperation.SavePost: {
824 const { post_view } = wsJsonToRes<PostResponse>(msg);
825 editPostFindRes(post_view, this.state.posts);
826 this.setState(this.state);
831 case UserOperation.CreatePostLike: {
832 const { post_view } = wsJsonToRes<PostResponse>(msg);
833 createPostLikeFindRes(post_view, this.state.posts);
834 this.setState(this.state);
839 case UserOperation.AddAdmin: {
840 const { admins } = wsJsonToRes<AddAdminResponse>(msg);
841 this.setState(s => ((s.siteRes.admins = admins), s));
846 case UserOperation.BanPerson: {
852 } = wsJsonToRes<BanPersonResponse>(msg);
855 .filter(p => p.creator.id == id)
856 .forEach(p => (p.creator.banned = banned));
857 this.setState(this.state);
862 case UserOperation.GetComments: {
863 const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
864 this.setState({ comments, loading: false });
869 case UserOperation.EditComment:
870 case UserOperation.DeleteComment:
871 case UserOperation.RemoveComment: {
872 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
873 editCommentRes(comment_view, this.state.comments);
874 this.setState(this.state);
879 case UserOperation.CreateComment: {
880 const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
882 // Necessary since it might be a user reply
884 const { listingType } = getHomeQueryParams();
886 // If you're on subscribed, only push it if you're subscribed.
887 const shouldAddComment =
888 listingType === ListingType.Subscribed
889 ? UserService.Instance.myUserInfo?.follows.some(
890 ({ community: { id } }) => id === comment_view.community.id
894 if (shouldAddComment) {
895 this.setState(({ comments }) => ({
896 comments: [comment_view].concat(comments),
904 case UserOperation.SaveComment: {
905 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
906 saveCommentRes(comment_view, this.state.comments);
907 this.setState(this.state);
912 case UserOperation.CreateCommentLike: {
913 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
914 createCommentLikeRes(comment_view, this.state.comments);
915 this.setState(this.state);
920 case UserOperation.BlockPerson: {
921 const data = wsJsonToRes<BlockPersonResponse>(msg);
922 updatePersonBlock(data);
927 case UserOperation.CreatePostReport: {
928 const data = wsJsonToRes<PostReportResponse>(msg);
930 toast(i18n.t("report_created"));
936 case UserOperation.CreateCommentReport: {
937 const data = wsJsonToRes<CommentReportResponse>(msg);
939 toast(i18n.t("report_created"));
945 case UserOperation.PurgePerson:
946 case UserOperation.PurgePost:
947 case UserOperation.PurgeComment:
948 case UserOperation.PurgeCommunity: {
949 const data = wsJsonToRes<PurgeItemResponse>(msg);
951 toast(i18n.t("purge_success"));
952 this.context.router.history.push(`/`);