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,
75 import { CommentNodes } from "../comment/comment-nodes";
76 import { DataTypeSelect } from "../common/data-type-select";
77 import { HtmlTags } from "../common/html-tags";
78 import { Icon, Spinner } from "../common/icon";
79 import { ListingTypeSelect } from "../common/listing-type-select";
80 import { Paginator } from "../common/paginator";
81 import { SortSelect } from "../common/sort-select";
82 import { CommunityLink } from "../community/community-link";
83 import { PostListings } from "../post/post-listings";
84 import { SiteSidebar } from "./site-sidebar";
87 trendingCommunities: CommunityView[];
88 siteRes: GetSiteResponse;
90 comments: CommentView[];
91 showSubscribedMobile: boolean;
92 showTrendingMobile: boolean;
93 showSidebarMobile: boolean;
94 subscribedCollapsed: boolean;
100 listingType: ListingType;
106 function getDataTypeFromQuery(type?: string): DataType {
107 return type ? DataType[type] : DataType.Post;
110 function getListingTypeFromQuery(type?: string): ListingType {
111 const myListingType =
112 UserService.Instance.myUserInfo?.local_user_view?.local_user
113 ?.default_listing_type;
115 return type ? (type as ListingType) : myListingType ?? "Local";
118 function getSortTypeFromQuery(type?: string): SortType {
120 UserService.Instance.myUserInfo?.local_user_view?.local_user
123 return type ? (type as SortType) : mySortType ?? "Active";
126 const getHomeQueryParams = () =>
127 getQueryParams<HomeProps>({
128 sort: getSortTypeFromQuery,
129 listingType: getListingTypeFromQuery,
130 page: getPageFromString,
131 dataType: getDataTypeFromQuery,
134 function fetchTrendingCommunities() {
135 const listCommunitiesForm: ListCommunities = {
138 limit: trendingFetchLimit,
141 WebSocketService.Instance.send(wsClient.listCommunities(listCommunitiesForm));
144 function fetchData() {
145 const auth = myAuth(false);
146 const { dataType, page, listingType, sort } = getHomeQueryParams();
149 if (dataType === DataType.Post) {
150 const getPostsForm: GetPosts = {
159 req = wsClient.getPosts(getPostsForm);
161 const getCommentsForm: GetComments = {
164 sort: postToCommentSortType(sort),
170 req = wsClient.getComments(getCommentsForm);
173 WebSocketService.Instance.send(req);
176 const MobileButton = ({
181 textKey: NoOptionI18nKeys;
183 onClick: MouseEventHandler<HTMLButtonElement>;
186 className="btn btn-secondary d-inline-block mb-2 mr-3"
189 {i18n.t(textKey)}{" "}
190 <Icon icon={show ? `minus-square` : `plus-square`} classes="icon-inline" />
194 const LinkButton = ({
199 translationKey: NoOptionI18nKeys;
201 <Link className="btn btn-secondary btn-block" to={path}>
202 {i18n.t(translationKey)}
206 function getRss(listingType: ListingType) {
207 const { sort } = getHomeQueryParams();
208 const auth = myAuth(false);
210 let rss: string | undefined = undefined;
212 switch (listingType) {
214 rss = `/feeds/all.xml?sort=${sort}`;
218 rss = `/feeds/local.xml?sort=${sort}`;
222 rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
230 <a href={rss} rel={relTags} title="RSS">
231 <Icon icon="rss" classes="text-muted small" />
233 <link rel="alternate" type="application/atom+xml" href={rss} />
239 export class Home extends Component<any, HomeState> {
240 private isoData = setIsoData(this.context);
241 private subscription?: Subscription;
243 trendingCommunities: [],
244 siteRes: this.isoData.site_res,
245 showSubscribedMobile: false,
246 showTrendingMobile: false,
247 showSidebarMobile: false,
248 subscribedCollapsed: false,
254 constructor(props: any, context: any) {
255 super(props, context);
257 this.handleSortChange = this.handleSortChange.bind(this);
258 this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
259 this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
260 this.handlePageChange = this.handlePageChange.bind(this);
262 this.parseMessage = this.parseMessage.bind(this);
263 this.subscription = wsSubscribe(this.parseMessage);
265 // Only fetch the data if coming from another route
266 if (this.isoData.path === this.context.router.route.match.url) {
267 const postsRes = this.isoData.routeData[0] as
270 const commentsRes = this.isoData.routeData[1] as
271 | GetCommentsResponse
273 const trendingRes = this.isoData.routeData[2] as
274 | ListCommunitiesResponse
278 this.state = { ...this.state, posts: postsRes.posts };
282 this.state = { ...this.state, comments: commentsRes.comments };
286 WebSocketService.Instance.send(
287 wsClient.communityJoin({ community_id: 0 })
290 const taglines = this.state?.siteRes?.taglines ?? [];
293 trendingCommunities: trendingRes?.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>>): Promise<any>[] {
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 ? BigInt(urlPage) : 1n;
329 const promises: Promise<any>[] = [];
331 if (dataType === DataType.Post) {
332 const getPostsForm: GetPosts = {
341 promises.push(client.getPosts(getPostsForm));
342 promises.push(Promise.resolve());
344 const getCommentsForm: GetComments = {
347 sort: postToCommentSortType(sort),
352 promises.push(Promise.resolve());
353 promises.push(client.getComments(getCommentsForm));
356 const trendingCommunitiesForm: ListCommunities = {
359 limit: trendingFetchLimit,
362 promises.push(client.listCommunities(trendingCommunitiesForm));
367 get documentTitle(): string {
368 const { name, description } = this.state.siteRes.site_view.site;
370 return description ? `${name} - ${description}` : name;
378 local_site: { site_setup },
384 <div className="container-lg">
386 title={this.documentTitle}
387 path={this.context.router.route.match.url}
390 <div className="row">
391 <main role="main" className="col-12 col-md-8">
395 dangerouslySetInnerHTML={mdToHtml(tagline)}
398 <div className="d-block d-md-none">{this.mobileView}</div>
401 <aside className="d-none d-md-block col-md-4">
410 get hasFollows(): boolean {
411 const mui = UserService.Instance.myUserInfo;
412 return !!mui && mui.follows.length > 0;
418 site_view: { counts, site },
422 showSubscribedMobile,
428 <div className="row">
429 <div className="col-12">
430 {this.hasFollows && (
433 show={showSubscribedMobile}
434 onClick={linkEvent(this, this.handleShowSubscribedMobile)}
439 show={showTrendingMobile}
440 onClick={linkEvent(this, this.handleShowTrendingMobile)}
444 show={showSidebarMobile}
445 onClick={linkEvent(this, this.handleShowSidebarMobile)}
447 {showSidebarMobile && (
453 showLocal={showLocal(this.isoData)}
456 {showTrendingMobile && (
457 <div className="col-12 card border-secondary mb-3">
458 <div className="card-body">{this.trendingCommunities(true)}</div>
461 {showSubscribedMobile && (
462 <div className="col-12 card border-secondary mb-3">
463 <div className="card-body">{this.subscribedCommunities}</div>
474 site_view: { counts, site },
485 <div className="card border-secondary mb-3">
486 <div className="card-body">
487 {this.trendingCommunities()}
488 {canCreateCommunity(this.state.siteRes) && (
490 path="/create_community"
491 translationKey="create_a_community"
496 translationKey="explore_communities"
505 showLocal={showLocal(this.isoData)}
507 {this.hasFollows && (
508 <div className="card border-secondary mb-3">
509 <div className="card-body">{this.subscribedCommunities}</div>
518 trendingCommunities(isMobile = false) {
520 <div className={!isMobile ? "mb-2" : ""}>
522 <T i18nKey="trending_communities">
524 <Link className="text-body" to="/communities">
529 <ul className="list-inline mb-0">
530 {this.state.trendingCommunities.map(cv => (
532 key={cv.community.id}
533 className="list-inline-item d-inline-block"
535 <CommunityLink community={cv.community} />
543 get subscribedCommunities() {
544 const { subscribedCollapsed } = this.state;
549 <T class="d-inline" i18nKey="subscribed_to_communities">
551 <Link className="text-body" to="/communities">
556 className="btn btn-sm text-muted"
557 onClick={linkEvent(this, this.handleCollapseSubscribe)}
558 aria-label={i18n.t("collapse")}
559 data-tippy-content={i18n.t("collapse")}
562 icon={`${subscribedCollapsed ? "plus" : "minus"}-square`}
563 classes="icon-inline"
567 {!subscribedCollapsed && (
568 <ul className="list-inline mb-0">
569 {UserService.Instance.myUserInfo?.follows.map(cfv => (
571 key={cfv.community.id}
572 className="list-inline-item d-inline-block"
574 <CommunityLink community={cfv.community} />
583 updateUrl({ dataType, listingType, page, sort }: Partial<HomeProps>) {
585 dataType: urlDataType,
586 listingType: urlListingType,
589 } = getHomeQueryParams();
591 const queryParams: QueryParams<HomeProps> = {
592 dataType: getDataTypeString(dataType ?? urlDataType),
593 listingType: listingType ?? urlListingType,
594 page: (page ?? urlPage).toString(),
595 sort: sort ?? urlSort,
598 this.props.history.push({
600 search: getQueryString(queryParams),
613 const { page } = getHomeQueryParams();
616 <div className="main-content-wrapper">
617 {this.state.loading ? (
625 <Paginator page={page} onChange={this.handlePageChange} />
633 const { dataType } = getHomeQueryParams();
634 const { siteRes, posts, comments } = this.state;
636 return dataType === DataType.Post ? (
641 enableDownvotes={enableDownvotes(siteRes)}
642 enableNsfw={enableNsfw(siteRes)}
643 allLanguages={siteRes.all_languages}
644 siteLanguages={siteRes.discussion_languages}
648 nodes={commentsToFlatNodes(comments)}
649 viewType={CommentViewType.Flat}
653 enableDownvotes={enableDownvotes(siteRes)}
654 allLanguages={siteRes.all_languages}
655 siteLanguages={siteRes.discussion_languages}
661 const { listingType, dataType, sort } = getHomeQueryParams();
664 <div className="mb-3">
665 <span className="mr-3">
668 onChange={this.handleDataTypeChange}
671 <span className="mr-3">
674 showLocal={showLocal(this.isoData)}
676 onChange={this.handleListingTypeChange}
679 <span className="mr-2">
680 <SortSelect sort={sort} onChange={this.handleSortChange} />
682 {getRss(listingType)}
687 handleShowSubscribedMobile(i: Home) {
688 i.setState({ showSubscribedMobile: !i.state.showSubscribedMobile });
691 handleShowTrendingMobile(i: Home) {
692 i.setState({ showTrendingMobile: !i.state.showTrendingMobile });
695 handleShowSidebarMobile(i: Home) {
696 i.setState({ showSidebarMobile: !i.state.showSidebarMobile });
699 handleCollapseSubscribe(i: Home) {
700 i.setState({ subscribedCollapsed: !i.state.subscribedCollapsed });
703 handlePageChange(page: bigint) {
704 this.updateUrl({ page });
705 window.scrollTo(0, 0);
708 handleSortChange(val: SortType) {
709 this.updateUrl({ sort: val, page: 1n });
710 window.scrollTo(0, 0);
713 handleListingTypeChange(val: ListingType) {
714 this.updateUrl({ listingType: val, page: 1n });
715 window.scrollTo(0, 0);
718 handleDataTypeChange(val: DataType) {
719 this.updateUrl({ dataType: val, page: 1n });
720 window.scrollTo(0, 0);
723 parseMessage(msg: any) {
724 const op = wsUserOp(msg);
728 toast(i18n.t(msg.error), "danger");
729 } else if (msg.reconnect) {
730 WebSocketService.Instance.send(
731 wsClient.communityJoin({ community_id: 0 })
736 case UserOperation.ListCommunities: {
737 const { communities } = wsJsonToRes<ListCommunitiesResponse>(msg);
738 this.setState({ trendingCommunities: communities });
743 case UserOperation.EditSite: {
744 const { site_view } = wsJsonToRes<SiteResponse>(msg);
745 this.setState(s => ((s.siteRes.site_view = site_view), s));
746 toast(i18n.t("site_saved"));
751 case UserOperation.GetPosts: {
752 const { posts } = wsJsonToRes<GetPostsResponse>(msg);
753 this.setState({ posts, loading: false });
754 WebSocketService.Instance.send(
755 wsClient.communityJoin({ community_id: 0 })
757 restoreScrollPosition(this.context);
763 case UserOperation.CreatePost: {
764 const { page, listingType } = getHomeQueryParams();
765 const { post_view } = wsJsonToRes<PostResponse>(msg);
767 // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
770 nsfwCheck(post_view) &&
771 !isPostBlocked(post_view)
773 const mui = UserService.Instance.myUserInfo;
774 const showPostNotifs =
775 mui?.local_user_view.local_user.show_new_post_notifs;
776 let shouldAddPost: boolean;
778 switch (listingType) {
780 // If you're on subscribed, only push it if you're subscribed.
781 shouldAddPost = !!mui?.follows.some(
782 ({ community: { id } }) => id === post_view.community.id
787 // If you're on the local view, only push it if its local
788 shouldAddPost = post_view.post.local;
792 shouldAddPost = true;
798 this.setState(({ posts }) => ({
799 posts: [post_view].concat(posts),
801 if (showPostNotifs) {
802 notifyPost(post_view, this.context.router);
810 case UserOperation.EditPost:
811 case UserOperation.DeletePost:
812 case UserOperation.RemovePost:
813 case UserOperation.LockPost:
814 case UserOperation.FeaturePost:
815 case UserOperation.SavePost: {
816 const { post_view } = wsJsonToRes<PostResponse>(msg);
817 editPostFindRes(post_view, this.state.posts);
818 this.setState(this.state);
823 case UserOperation.CreatePostLike: {
824 const { post_view } = wsJsonToRes<PostResponse>(msg);
825 createPostLikeFindRes(post_view, this.state.posts);
826 this.setState(this.state);
831 case UserOperation.AddAdmin: {
832 const { admins } = wsJsonToRes<AddAdminResponse>(msg);
833 this.setState(s => ((s.siteRes.admins = admins), s));
838 case UserOperation.BanPerson: {
844 } = wsJsonToRes<BanPersonResponse>(msg);
847 .filter(p => p.creator.id == id)
848 .forEach(p => (p.creator.banned = banned));
849 this.setState(this.state);
854 case UserOperation.GetComments: {
855 const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
856 this.setState({ comments, loading: false });
861 case UserOperation.EditComment:
862 case UserOperation.DeleteComment:
863 case UserOperation.RemoveComment: {
864 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
865 editCommentRes(comment_view, this.state.comments);
866 this.setState(this.state);
871 case UserOperation.CreateComment: {
872 const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
874 // Necessary since it might be a user reply
876 const { listingType } = getHomeQueryParams();
878 // If you're on subscribed, only push it if you're subscribed.
879 const shouldAddComment =
880 listingType === "Subscribed"
881 ? UserService.Instance.myUserInfo?.follows.some(
882 ({ community: { id } }) => id === comment_view.community.id
886 if (shouldAddComment) {
887 this.setState(({ comments }) => ({
888 comments: [comment_view].concat(comments),
896 case UserOperation.SaveComment: {
897 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
898 saveCommentRes(comment_view, this.state.comments);
899 this.setState(this.state);
904 case UserOperation.CreateCommentLike: {
905 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
906 createCommentLikeRes(comment_view, this.state.comments);
907 this.setState(this.state);
912 case UserOperation.BlockPerson: {
913 const data = wsJsonToRes<BlockPersonResponse>(msg);
914 updatePersonBlock(data);
919 case UserOperation.CreatePostReport: {
920 const data = wsJsonToRes<PostReportResponse>(msg);
922 toast(i18n.t("report_created"));
928 case UserOperation.CreateCommentReport: {
929 const data = wsJsonToRes<CommentReportResponse>(msg);
931 toast(i18n.t("report_created"));
937 case UserOperation.PurgePerson:
938 case UserOperation.PurgePost:
939 case UserOperation.PurgeComment:
940 case UserOperation.PurgeCommunity: {
941 const data = wsJsonToRes<PurgeItemResponse>(msg);
943 toast(i18n.t("purge_success"));
944 this.context.router.history.push(`/`);