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,
76 import { CommentNodes } from "../comment/comment-nodes";
77 import { DataTypeSelect } from "../common/data-type-select";
78 import { HtmlTags } from "../common/html-tags";
79 import { Icon, Spinner } from "../common/icon";
80 import { ListingTypeSelect } from "../common/listing-type-select";
81 import { Paginator } from "../common/paginator";
82 import { SortSelect } from "../common/sort-select";
83 import { CommunityLink } from "../community/community-link";
84 import { PostListings } from "../post/post-listings";
85 import { SiteSidebar } from "./site-sidebar";
88 trendingCommunities: CommunityView[];
89 siteRes: GetSiteResponse;
91 comments: CommentView[];
92 showSubscribedMobile: boolean;
93 showTrendingMobile: boolean;
94 showSidebarMobile: boolean;
95 subscribedCollapsed: boolean;
100 interface HomeProps {
101 listingType: ListingType;
108 postsResponse?: GetPostsResponse;
109 commentsResponse?: GetCommentsResponse;
110 trendingResponse: ListCommunitiesResponse;
113 function getDataTypeFromQuery(type?: string): DataType {
114 return type ? DataType[type] : DataType.Post;
117 function getListingTypeFromQuery(type?: string): ListingType {
118 const myListingType =
119 UserService.Instance.myUserInfo?.local_user_view?.local_user
120 ?.default_listing_type;
122 return type ? (type as ListingType) : myListingType ?? "Local";
125 function getSortTypeFromQuery(type?: string): SortType {
127 UserService.Instance.myUserInfo?.local_user_view?.local_user
130 return type ? (type as SortType) : mySortType ?? "Active";
133 const getHomeQueryParams = () =>
134 getQueryParams<HomeProps>({
135 sort: getSortTypeFromQuery,
136 listingType: getListingTypeFromQuery,
137 page: getPageFromString,
138 dataType: getDataTypeFromQuery,
141 function fetchTrendingCommunities() {
142 const listCommunitiesForm: ListCommunities = {
145 limit: trendingFetchLimit,
148 WebSocketService.Instance.send(wsClient.listCommunities(listCommunitiesForm));
151 function fetchData() {
152 const auth = myAuth(false);
153 const { dataType, page, listingType, sort } = getHomeQueryParams();
156 if (dataType === DataType.Post) {
157 const getPostsForm: GetPosts = {
166 req = wsClient.getPosts(getPostsForm);
168 const getCommentsForm: GetComments = {
171 sort: postToCommentSortType(sort),
177 req = wsClient.getComments(getCommentsForm);
180 WebSocketService.Instance.send(req);
183 const MobileButton = ({
188 textKey: NoOptionI18nKeys;
190 onClick: MouseEventHandler<HTMLButtonElement>;
193 className="btn btn-secondary d-inline-block mb-2 mr-3"
196 {i18n.t(textKey)}{" "}
197 <Icon icon={show ? `minus-square` : `plus-square`} classes="icon-inline" />
201 const LinkButton = ({
206 translationKey: NoOptionI18nKeys;
208 <Link className="btn btn-secondary btn-block" to={path}>
209 {i18n.t(translationKey)}
213 function getRss(listingType: ListingType) {
214 const { sort } = getHomeQueryParams();
215 const auth = myAuth(false);
217 let rss: string | undefined = undefined;
219 switch (listingType) {
221 rss = `/feeds/all.xml?sort=${sort}`;
225 rss = `/feeds/local.xml?sort=${sort}`;
229 rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
237 <a href={rss} rel={relTags} title="RSS">
238 <Icon icon="rss" classes="text-muted small" />
240 <link rel="alternate" type="application/atom+xml" href={rss} />
246 export class Home extends Component<any, HomeState> {
247 private isoData = setIsoData<HomeData>(this.context);
248 private subscription?: Subscription;
250 trendingCommunities: [],
251 siteRes: this.isoData.site_res,
252 showSubscribedMobile: false,
253 showTrendingMobile: false,
254 showSidebarMobile: false,
255 subscribedCollapsed: false,
261 constructor(props: any, context: any) {
262 super(props, context);
264 this.handleSortChange = this.handleSortChange.bind(this);
265 this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
266 this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
267 this.handlePageChange = this.handlePageChange.bind(this);
269 this.parseMessage = this.parseMessage.bind(this);
270 this.subscription = wsSubscribe(this.parseMessage);
272 // Only fetch the data if coming from another route
273 if (this.isoData.path === this.context.router.route.match.url) {
274 const { trendingResponse, commentsResponse, postsResponse } =
275 this.isoData.routeData;
278 this.state = { ...this.state, posts: postsResponse.posts };
281 if (commentsResponse) {
282 this.state = { ...this.state, comments: commentsResponse.comments };
286 WebSocketService.Instance.send(
287 wsClient.communityJoin({ community_id: 0 })
290 const taglines = this.state?.siteRes?.taglines ?? [];
293 trendingCommunities: trendingResponse?.communities ?? [],
295 tagline: getRandomFromList(taglines)?.content,
298 fetchTrendingCommunities();
303 componentDidMount() {
304 // This means it hasn't been set up yet
305 if (!this.state.siteRes.site_view.local_site.site_setup) {
306 this.context.router.history.push("/setup");
311 componentWillUnmount() {
312 saveScrollPosition(this.context);
313 this.subscription?.unsubscribe();
316 static fetchInitialData({
319 query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
320 }: InitialFetchRequest<QueryParams<HomeProps>>): WithPromiseKeys<HomeData> {
321 const dataType = getDataTypeFromQuery(urlDataType);
323 // TODO figure out auth default_listingType, default_sort_type
324 const type_ = getListingTypeFromQuery(listingType);
325 const sort = getSortTypeFromQuery(urlSort);
327 const page = urlPage ? Number(urlPage) : 1;
329 const promises: Promise<any>[] = [];
331 let postsResponse: Promise<GetPostsResponse> | undefined = undefined;
332 let commentsResponse: Promise<GetCommentsResponse> | undefined = undefined;
334 if (dataType === DataType.Post) {
335 const getPostsForm: GetPosts = {
344 postsResponse = client.getPosts(getPostsForm);
346 const getCommentsForm: GetComments = {
349 sort: postToCommentSortType(sort),
355 commentsResponse = client.getComments(getCommentsForm);
358 const trendingCommunitiesForm: ListCommunities = {
361 limit: trendingFetchLimit,
364 promises.push(client.listCommunities(trendingCommunitiesForm));
367 trendingResponse: client.listCommunities(trendingCommunitiesForm),
373 get documentTitle(): string {
374 const { name, description } = this.state.siteRes.site_view.site;
376 return description ? `${name} - ${description}` : name;
384 local_site: { site_setup },
390 <div className="container-lg">
392 title={this.documentTitle}
393 path={this.context.router.route.match.url}
396 <div className="row">
397 <main role="main" className="col-12 col-md-8">
401 dangerouslySetInnerHTML={mdToHtml(tagline)}
404 <div className="d-block d-md-none">{this.mobileView}</div>
407 <aside className="d-none d-md-block col-md-4">
416 get hasFollows(): boolean {
417 const mui = UserService.Instance.myUserInfo;
418 return !!mui && mui.follows.length > 0;
424 site_view: { counts, site },
428 showSubscribedMobile,
434 <div className="row">
435 <div className="col-12">
436 {this.hasFollows && (
439 show={showSubscribedMobile}
440 onClick={linkEvent(this, this.handleShowSubscribedMobile)}
445 show={showTrendingMobile}
446 onClick={linkEvent(this, this.handleShowTrendingMobile)}
450 show={showSidebarMobile}
451 onClick={linkEvent(this, this.handleShowSidebarMobile)}
453 {showSidebarMobile && (
459 showLocal={showLocal(this.isoData)}
462 {showTrendingMobile && (
463 <div className="col-12 card border-secondary mb-3">
464 <div className="card-body">{this.trendingCommunities(true)}</div>
467 {showSubscribedMobile && (
468 <div className="col-12 card border-secondary mb-3">
469 <div className="card-body">{this.subscribedCommunities}</div>
480 site_view: { counts, site },
491 <div className="card border-secondary mb-3">
492 <div className="card-body">
493 {this.trendingCommunities()}
494 {canCreateCommunity(this.state.siteRes) && (
496 path="/create_community"
497 translationKey="create_a_community"
502 translationKey="explore_communities"
511 showLocal={showLocal(this.isoData)}
513 {this.hasFollows && (
514 <div className="card border-secondary mb-3">
515 <div className="card-body">{this.subscribedCommunities}</div>
524 trendingCommunities(isMobile = false) {
526 <div className={!isMobile ? "mb-2" : ""}>
528 <T i18nKey="trending_communities">
530 <Link className="text-body" to="/communities">
535 <ul className="list-inline mb-0">
536 {this.state.trendingCommunities.map(cv => (
538 key={cv.community.id}
539 className="list-inline-item d-inline-block"
541 <CommunityLink community={cv.community} />
549 get subscribedCommunities() {
550 const { subscribedCollapsed } = this.state;
555 <T class="d-inline" i18nKey="subscribed_to_communities">
557 <Link className="text-body" to="/communities">
562 className="btn btn-sm text-muted"
563 onClick={linkEvent(this, this.handleCollapseSubscribe)}
564 aria-label={i18n.t("collapse")}
565 data-tippy-content={i18n.t("collapse")}
568 icon={`${subscribedCollapsed ? "plus" : "minus"}-square`}
569 classes="icon-inline"
573 {!subscribedCollapsed && (
574 <ul className="list-inline mb-0">
575 {UserService.Instance.myUserInfo?.follows.map(cfv => (
577 key={cfv.community.id}
578 className="list-inline-item d-inline-block"
580 <CommunityLink community={cfv.community} />
589 updateUrl({ dataType, listingType, page, sort }: Partial<HomeProps>) {
591 dataType: urlDataType,
592 listingType: urlListingType,
595 } = getHomeQueryParams();
597 const queryParams: QueryParams<HomeProps> = {
598 dataType: getDataTypeString(dataType ?? urlDataType),
599 listingType: listingType ?? urlListingType,
600 page: (page ?? urlPage).toString(),
601 sort: sort ?? urlSort,
604 this.props.history.push({
606 search: getQueryString(queryParams),
619 const { page } = getHomeQueryParams();
622 <div className="main-content-wrapper">
623 {this.state.loading ? (
631 <Paginator page={page} onChange={this.handlePageChange} />
639 const { dataType } = getHomeQueryParams();
640 const { siteRes, posts, comments } = this.state;
642 return dataType === DataType.Post ? (
647 enableDownvotes={enableDownvotes(siteRes)}
648 enableNsfw={enableNsfw(siteRes)}
649 allLanguages={siteRes.all_languages}
650 siteLanguages={siteRes.discussion_languages}
654 nodes={commentsToFlatNodes(comments)}
655 viewType={CommentViewType.Flat}
659 enableDownvotes={enableDownvotes(siteRes)}
660 allLanguages={siteRes.all_languages}
661 siteLanguages={siteRes.discussion_languages}
667 const { listingType, dataType, sort } = getHomeQueryParams();
670 <div className="mb-3">
671 <span className="mr-3">
674 onChange={this.handleDataTypeChange}
677 <span className="mr-3">
680 showLocal={showLocal(this.isoData)}
682 onChange={this.handleListingTypeChange}
685 <span className="mr-2">
686 <SortSelect sort={sort} onChange={this.handleSortChange} />
688 {getRss(listingType)}
693 handleShowSubscribedMobile(i: Home) {
694 i.setState({ showSubscribedMobile: !i.state.showSubscribedMobile });
697 handleShowTrendingMobile(i: Home) {
698 i.setState({ showTrendingMobile: !i.state.showTrendingMobile });
701 handleShowSidebarMobile(i: Home) {
702 i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
705 handleCollapseSubscribe(i: Home) {
706 i.setState({ subscribedCollapsed: !i.state.subscribedCollapsed });
709 handlePageChange(page: number) {
710 this.updateUrl({ page });
711 window.scrollTo(0, 0);
714 handleSortChange(val: SortType) {
715 this.updateUrl({ sort: val, page: 1 });
716 window.scrollTo(0, 0);
719 handleListingTypeChange(val: ListingType) {
720 this.updateUrl({ listingType: val, page: 1 });
721 window.scrollTo(0, 0);
724 handleDataTypeChange(val: DataType) {
725 this.updateUrl({ dataType: val, page: 1 });
726 window.scrollTo(0, 0);
729 parseMessage(msg: any) {
730 const op = wsUserOp(msg);
734 toast(i18n.t(msg.error), "danger");
735 } else if (msg.reconnect) {
736 WebSocketService.Instance.send(
737 wsClient.communityJoin({ community_id: 0 })
742 case UserOperation.ListCommunities: {
743 const { communities } = wsJsonToRes<ListCommunitiesResponse>(msg);
744 this.setState({ trendingCommunities: communities });
749 case UserOperation.EditSite: {
750 const { site_view } = wsJsonToRes<SiteResponse>(msg);
751 this.setState(s => ((s.siteRes.site_view = site_view), s));
752 toast(i18n.t("site_saved"));
757 case UserOperation.GetPosts: {
758 const { posts } = wsJsonToRes<GetPostsResponse>(msg);
759 this.setState({ posts, loading: false });
760 WebSocketService.Instance.send(
761 wsClient.communityJoin({ community_id: 0 })
763 restoreScrollPosition(this.context);
769 case UserOperation.CreatePost: {
770 const { page, listingType } = getHomeQueryParams();
771 const { post_view } = wsJsonToRes<PostResponse>(msg);
773 // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
774 if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
775 const mui = UserService.Instance.myUserInfo;
776 const showPostNotifs =
777 mui?.local_user_view.local_user.show_new_post_notifs;
778 let shouldAddPost: boolean;
780 switch (listingType) {
782 // If you're on subscribed, only push it if you're subscribed.
783 shouldAddPost = !!mui?.follows.some(
784 ({ community: { id } }) => id === post_view.community.id
789 // If you're on the local view, only push it if its local
790 shouldAddPost = post_view.post.local;
794 shouldAddPost = true;
800 this.setState(({ posts }) => ({
801 posts: [post_view].concat(posts),
803 if (showPostNotifs) {
804 notifyPost(post_view, this.context.router);
812 case UserOperation.EditPost:
813 case UserOperation.DeletePost:
814 case UserOperation.RemovePost:
815 case UserOperation.LockPost:
816 case UserOperation.FeaturePost:
817 case UserOperation.SavePost: {
818 const { post_view } = wsJsonToRes<PostResponse>(msg);
819 editPostFindRes(post_view, this.state.posts);
820 this.setState(this.state);
825 case UserOperation.CreatePostLike: {
826 const { post_view } = wsJsonToRes<PostResponse>(msg);
827 createPostLikeFindRes(post_view, this.state.posts);
828 this.setState(this.state);
833 case UserOperation.AddAdmin: {
834 const { admins } = wsJsonToRes<AddAdminResponse>(msg);
835 this.setState(s => ((s.siteRes.admins = admins), s));
840 case UserOperation.BanPerson: {
846 } = wsJsonToRes<BanPersonResponse>(msg);
849 .filter(p => p.creator.id == id)
850 .forEach(p => (p.creator.banned = banned));
851 this.setState(this.state);
856 case UserOperation.GetComments: {
857 const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
858 this.setState({ comments, loading: false });
863 case UserOperation.EditComment:
864 case UserOperation.DeleteComment:
865 case UserOperation.RemoveComment: {
866 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
867 editCommentRes(comment_view, this.state.comments);
868 this.setState(this.state);
873 case UserOperation.CreateComment: {
874 const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
876 // Necessary since it might be a user reply
878 const { listingType } = getHomeQueryParams();
880 // If you're on subscribed, only push it if you're subscribed.
881 const shouldAddComment =
882 listingType === "Subscribed"
883 ? UserService.Instance.myUserInfo?.follows.some(
884 ({ community: { id } }) => id === comment_view.community.id
888 if (shouldAddComment) {
889 this.setState(({ comments }) => ({
890 comments: [comment_view].concat(comments),
898 case UserOperation.SaveComment: {
899 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
900 saveCommentRes(comment_view, this.state.comments);
901 this.setState(this.state);
906 case UserOperation.CreateCommentLike: {
907 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
908 createCommentLikeRes(comment_view, this.state.comments);
909 this.setState(this.state);
914 case UserOperation.BlockPerson: {
915 const data = wsJsonToRes<BlockPersonResponse>(msg);
916 updatePersonBlock(data);
921 case UserOperation.CreatePostReport: {
922 const data = wsJsonToRes<PostReportResponse>(msg);
924 toast(i18n.t("report_created"));
930 case UserOperation.CreateCommentReport: {
931 const data = wsJsonToRes<CommentReportResponse>(msg);
933 toast(i18n.t("report_created"));
939 case UserOperation.PurgePerson:
940 case UserOperation.PurgePost:
941 case UserOperation.PurgeComment:
942 case UserOperation.PurgeCommunity: {
943 const data = wsJsonToRes<PurgeItemResponse>(msg);
945 toast(i18n.t("purge_success"));
946 this.context.router.history.push(`/`);