1 import { Component, linkEvent } from "inferno";
2 import { Link } from "inferno-router";
3 import { Subscription } from "rxjs";
7 GetFollowedCommunitiesResponse,
9 ListCommunitiesResponse,
25 } from "lemmy-js-client";
26 import { DataType, InitialFetchRequest } from "../interfaces";
27 import { WebSocketService, UserService } from "../services";
28 import { PostListings } from "./post-listings";
29 import { CommentNodes } from "./comment-nodes";
30 import { SortSelect } from "./sort-select";
31 import { ListingTypeSelect } from "./listing-type-select";
32 import { DataTypeSelect } from "./data-type-select";
33 import { SiteForm } from "./site-form";
34 import { PersonListing } from "./person-listing";
35 import { CommunityLink } from "./community-link";
36 import { BannerIconHeader } from "./banner-icon-header";
37 import { Icon, Spinner } from "./icon";
43 getListingTypeFromProps,
50 createPostLikeFindRes,
62 restoreScrollPosition,
64 import { i18n } from "../i18next";
65 import { T } from "inferno-i18next";
66 import { HtmlTags } from "./html-tags";
69 subscribedCommunities: CommunityFollowerView[];
70 trendingCommunities: CommunityView[];
71 siteRes: GetSiteResponse;
72 showEditSite: boolean;
75 comments: CommentView[];
76 listingType: ListingType;
83 listingType: ListingType;
90 listingType?: ListingType;
96 export class Main extends Component<any, MainState> {
97 private isoData = setIsoData(this.context);
98 private subscription: Subscription;
99 private emptyState: MainState = {
100 subscribedCommunities: [],
101 trendingCommunities: [],
102 siteRes: this.isoData.site_res,
107 listingType: getListingTypeFromProps(this.props),
108 dataType: getDataTypeFromProps(this.props),
109 sort: getSortTypeFromProps(this.props),
110 page: getPageFromProps(this.props),
113 constructor(props: any, context: any) {
114 super(props, context);
116 this.state = this.emptyState;
117 this.handleEditCancel = this.handleEditCancel.bind(this);
118 this.handleSortChange = this.handleSortChange.bind(this);
119 this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
120 this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
122 this.parseMessage = this.parseMessage.bind(this);
123 this.subscription = wsSubscribe(this.parseMessage);
125 // Only fetch the data if coming from another route
126 if (this.isoData.path == this.context.router.route.match.url) {
127 if (this.state.dataType == DataType.Post) {
128 this.state.posts = this.isoData.routeData[0].posts;
130 this.state.comments = this.isoData.routeData[0].comments;
132 this.state.trendingCommunities = this.isoData.routeData[1].communities;
133 if (UserService.Instance.localUserView) {
134 this.state.subscribedCommunities = this.isoData.routeData[2].communities;
136 this.state.loading = false;
138 this.fetchTrendingCommunities();
140 if (UserService.Instance.localUserView) {
141 WebSocketService.Instance.send(
142 wsClient.getFollowedCommunities({
152 fetchTrendingCommunities() {
153 let listCommunitiesForm: ListCommunities = {
154 type_: ListingType.Local,
157 auth: authField(false),
159 WebSocketService.Instance.send(
160 wsClient.listCommunities(listCommunitiesForm)
164 componentDidMount() {
165 // This means it hasn't been set up yet
166 if (!this.state.siteRes.site_view) {
167 this.context.router.history.push("/setup");
170 WebSocketService.Instance.send(wsClient.communityJoin({ community_id: 0 }));
173 componentWillUnmount() {
174 saveScrollPosition(this.context);
175 this.subscription.unsubscribe();
176 window.isoData.path = undefined;
179 static getDerivedStateFromProps(props: any): MainProps {
181 listingType: getListingTypeFromProps(props),
182 dataType: getDataTypeFromProps(props),
183 sort: getSortTypeFromProps(props),
184 page: getPageFromProps(props),
188 static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
189 let pathSplit = req.path.split("/");
190 let dataType: DataType = pathSplit[3]
191 ? DataType[pathSplit[3]]
194 // TODO figure out auth default_listingType, default_sort_type
195 let type_: ListingType = pathSplit[5]
196 ? ListingType[pathSplit[5]]
197 : UserService.Instance.localUserView
198 ? Object.values(ListingType)[
199 UserService.Instance.localUserView.local_user.default_listing_type
202 let sort: SortType = pathSplit[7]
203 ? SortType[pathSplit[7]]
204 : UserService.Instance.localUserView
205 ? Object.values(SortType)[
206 UserService.Instance.localUserView.local_user.default_sort_type
210 let page = pathSplit[9] ? Number(pathSplit[9]) : 1;
212 let promises: Promise<any>[] = [];
214 if (dataType == DataType.Post) {
215 let getPostsForm: GetPosts = {
222 setOptionalAuth(getPostsForm, req.auth);
223 promises.push(req.client.getPosts(getPostsForm));
225 let getCommentsForm: GetComments = {
232 setOptionalAuth(getCommentsForm, req.auth);
233 promises.push(req.client.getComments(getCommentsForm));
236 let trendingCommunitiesForm: ListCommunities = {
237 type_: ListingType.Local,
241 promises.push(req.client.listCommunities(trendingCommunitiesForm));
244 promises.push(req.client.getFollowedCommunities({ auth: req.auth }));
250 componentDidUpdate(_: any, lastState: MainState) {
252 lastState.listingType !== this.state.listingType ||
253 lastState.dataType !== this.state.dataType ||
254 lastState.sort !== this.state.sort ||
255 lastState.page !== this.state.page
257 this.setState({ loading: true });
262 get documentTitle(): string {
264 this.state.siteRes.site_view
265 ? this.state.siteRes.site_view.site.name
272 <div class="container">
274 title={this.documentTitle}
275 path={this.context.router.route.match.url}
277 {this.state.siteRes.site_view?.site && (
279 <main role="main" class="col-12 col-md-8">
282 <aside class="col-12 col-md-4">{this.mySidebar()}</aside>
292 {!this.state.loading && (
294 <div class="card border-secondary mb-3">
295 <div class="card-body">
296 {this.trendingCommunities()}
297 {this.createCommunityButton()}
301 {UserService.Instance.localUserView &&
302 this.state.subscribedCommunities.length > 0 && (
303 <div class="card border-secondary mb-3">
304 <div class="card-body">{this.subscribedCommunities()}</div>
308 <div class="card border-secondary mb-3">
309 <div class="card-body">{this.sidebar()}</div>
317 createCommunityButton() {
319 <Link className="btn btn-secondary btn-block" to="/create_community">
320 {i18n.t("create_a_community")}
325 trendingCommunities() {
329 <T i18nKey="trending_communities">
331 <Link className="text-body" to="/communities">
336 <ul class="list-inline">
337 {this.state.trendingCommunities.map(cv => (
338 <li class="list-inline-item d-inline-block">
339 <CommunityLink community={cv.community} />
347 subscribedCommunities() {
351 <T i18nKey="subscribed_to_communities">
353 <Link className="text-body" to="/communities">
358 <ul class="list-inline mb-0">
359 {this.state.subscribedCommunities.map(cfv => (
360 <li class="list-inline-item d-inline-block">
361 <CommunityLink community={cfv.community} />
370 let site = this.state.siteRes.site_view.site;
373 {!this.state.showEditSite ? (
377 {this.adminButtons()}
379 <BannerIconHeader banner={site.banner} />
383 <SiteForm site={site} onCancel={this.handleEditCancel} />
389 updateUrl(paramUpdates: UrlParams) {
390 const listingTypeStr = paramUpdates.listingType || this.state.listingType;
391 const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
392 const sortStr = paramUpdates.sort || this.state.sort;
393 const page = paramUpdates.page || this.state.page;
394 this.props.history.push(
395 `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
402 {this.state.siteRes.site_view.site.description &&
403 this.siteDescription()}
411 return <h5 class="mb-0">{`${this.documentTitle}`}</h5>;
416 <ul class="mt-1 list-inline small mb-0">
417 <li class="list-inline-item">{i18n.t("admins")}:</li>
418 {this.state.siteRes.admins.map(av => (
419 <li class="list-inline-item">
420 <PersonListing person={av.person} />
428 let counts = this.state.siteRes.site_view.counts;
430 <ul class="my-2 list-inline">
431 <li className="list-inline-item badge badge-secondary">
432 {i18n.t("number_online", { count: this.state.siteRes.online })}
435 className="list-inline-item badge badge-secondary pointer"
436 data-tippy-content={`${i18n.t("number_of_users", {
437 count: counts.users_active_day,
438 })} ${i18n.t("active_in_the_last")} ${i18n.t("day")}`}
440 {i18n.t("number_of_users", {
441 count: counts.users_active_day,
446 className="list-inline-item badge badge-secondary pointer"
447 data-tippy-content={`${i18n.t("number_of_users", {
448 count: counts.users_active_week,
449 })} ${i18n.t("active_in_the_last")} ${i18n.t("week")}`}
451 {i18n.t("number_of_users", {
452 count: counts.users_active_week,
457 className="list-inline-item badge badge-secondary pointer"
458 data-tippy-content={`${i18n.t("number_of_users", {
459 count: counts.users_active_month,
460 })} ${i18n.t("active_in_the_last")} ${i18n.t("month")}`}
462 {i18n.t("number_of_users", {
463 count: counts.users_active_month,
468 className="list-inline-item badge badge-secondary pointer"
469 data-tippy-content={`${i18n.t("number_of_users", {
470 count: counts.users_active_half_year,
471 })} ${i18n.t("active_in_the_last")} ${i18n.t("number_of_months", {
475 {i18n.t("number_of_users", {
476 count: counts.users_active_half_year,
478 / {i18n.t("number_of_months", { count: 6 })}
480 <li className="list-inline-item badge badge-secondary">
481 {i18n.t("number_of_users", {
485 <li className="list-inline-item badge badge-secondary">
486 {i18n.t("number_of_communities", {
487 count: counts.communities,
490 <li className="list-inline-item badge badge-secondary">
491 {i18n.t("number_of_posts", {
495 <li className="list-inline-item badge badge-secondary">
496 {i18n.t("number_of_comments", {
497 count: counts.comments,
500 <li className="list-inline-item">
501 <Link className="badge badge-secondary" to="/modlog">
512 <ul class="list-inline mb-1 text-muted font-weight-bold">
513 <li className="list-inline-item-action">
517 onClick={linkEvent(this, this.handleEditClick)}
518 aria-label={i18n.t("edit")}
519 data-tippy-content={i18n.t("edit")}
521 <Icon icon="edit" classes="icon-inline" />
533 dangerouslySetInnerHTML={mdToHtml(
534 this.state.siteRes.site_view.site.description
542 <div class="main-content-wrapper">
543 {this.state.loading ? (
559 let site = this.state.siteRes.site_view.site;
560 return this.state.dataType == DataType.Post ? (
562 posts={this.state.posts}
565 enableDownvotes={site.enable_downvotes}
566 enableNsfw={site.enable_nsfw}
570 nodes={commentsToFlatNodes(this.state.comments)}
574 enableDownvotes={site.enable_downvotes}
581 <div className="mb-3">
584 type_={this.state.dataType}
585 onChange={this.handleDataTypeChange}
590 type_={this.state.listingType}
591 showLocal={this.showLocal}
592 onChange={this.handleListingTypeChange}
596 <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
598 {this.state.listingType == ListingType.All && (
600 href={`/feeds/all.xml?sort=${this.state.sort}`}
604 <Icon icon="rss" classes="text-muted small" />
607 {this.state.listingType == ListingType.Local && (
609 href={`/feeds/local.xml?sort=${this.state.sort}`}
613 <Icon icon="rss" classes="text-muted small" />
616 {UserService.Instance.localUserView &&
617 this.state.listingType == ListingType.Subscribed && (
619 href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${this.state.sort}`}
623 <Icon icon="rss" classes="text-muted small" />
633 {this.state.page > 1 && (
635 class="btn btn-secondary mr-1"
636 onClick={linkEvent(this, this.prevPage)}
641 {this.state.posts.length > 0 && (
643 class="btn btn-secondary"
644 onClick={linkEvent(this, this.nextPage)}
653 get showLocal(): boolean {
654 return this.isoData.site_res.federated_instances?.linked.length > 0;
657 get canAdmin(): boolean {
659 UserService.Instance.localUserView &&
660 this.state.siteRes.admins
661 .map(a => a.person.id)
662 .includes(UserService.Instance.localUserView.person.id)
666 handleEditClick(i: Main) {
667 i.state.showEditSite = true;
672 this.state.showEditSite = false;
673 this.setState(this.state);
677 i.updateUrl({ page: i.state.page + 1 });
678 window.scrollTo(0, 0);
682 i.updateUrl({ page: i.state.page - 1 });
683 window.scrollTo(0, 0);
686 handleSortChange(val: SortType) {
687 this.updateUrl({ sort: val, page: 1 });
688 window.scrollTo(0, 0);
691 handleListingTypeChange(val: ListingType) {
692 this.updateUrl({ listingType: val, page: 1 });
693 window.scrollTo(0, 0);
696 handleDataTypeChange(val: DataType) {
697 this.updateUrl({ dataType: DataType[val], page: 1 });
698 window.scrollTo(0, 0);
702 if (this.state.dataType == DataType.Post) {
703 let getPostsForm: GetPosts = {
704 page: this.state.page,
706 sort: this.state.sort,
707 type_: this.state.listingType,
709 auth: authField(false),
711 WebSocketService.Instance.send(wsClient.getPosts(getPostsForm));
713 let getCommentsForm: GetComments = {
714 page: this.state.page,
716 sort: this.state.sort,
717 type_: this.state.listingType,
719 auth: authField(false),
721 WebSocketService.Instance.send(wsClient.getComments(getCommentsForm));
725 parseMessage(msg: any) {
726 let op = wsUserOp(msg);
729 toast(i18n.t(msg.error), "danger");
731 } else if (msg.reconnect) {
732 WebSocketService.Instance.send(
733 wsClient.communityJoin({ community_id: 0 })
736 } else if (op == UserOperation.GetFollowedCommunities) {
737 let data = wsJsonToRes<GetFollowedCommunitiesResponse>(msg).data;
738 this.state.subscribedCommunities = data.communities;
739 this.setState(this.state);
740 } else if (op == UserOperation.ListCommunities) {
741 let data = wsJsonToRes<ListCommunitiesResponse>(msg).data;
742 this.state.trendingCommunities = data.communities;
743 this.setState(this.state);
744 } else if (op == UserOperation.EditSite) {
745 let data = wsJsonToRes<SiteResponse>(msg).data;
746 this.state.siteRes.site_view = data.site_view;
747 this.state.showEditSite = false;
748 this.setState(this.state);
749 toast(i18n.t("site_saved"));
750 } else if (op == UserOperation.GetPosts) {
751 let data = wsJsonToRes<GetPostsResponse>(msg).data;
752 this.state.posts = data.posts;
753 this.state.loading = false;
754 this.setState(this.state);
755 restoreScrollPosition(this.context);
757 } else if (op == UserOperation.CreatePost) {
758 let data = wsJsonToRes<PostResponse>(msg).data;
761 let nsfw = data.post_view.post.nsfw || data.post_view.community.nsfw;
765 UserService.Instance.localUserView &&
766 UserService.Instance.localUserView.local_user.show_nsfw);
768 // Only push these if you're on the first page, and you pass the nsfw check
769 if (this.state.page == 1 && nsfwCheck) {
770 // If you're on subscribed, only push it if you're subscribed.
771 if (this.state.listingType == ListingType.Subscribed) {
773 this.state.subscribedCommunities
774 .map(c => c.community.id)
775 .includes(data.post_view.community.id)
777 this.state.posts.unshift(data.post_view);
778 notifyPost(data.post_view, this.context.router);
780 } else if (this.state.listingType == ListingType.Local) {
781 // If you're on the local view, only push it if its local
782 if (data.post_view.post.local) {
783 this.state.posts.unshift(data.post_view);
784 notifyPost(data.post_view, this.context.router);
787 this.state.posts.unshift(data.post_view);
788 notifyPost(data.post_view, this.context.router);
790 this.setState(this.state);
793 op == UserOperation.EditPost ||
794 op == UserOperation.DeletePost ||
795 op == UserOperation.RemovePost ||
796 op == UserOperation.LockPost ||
797 op == UserOperation.StickyPost ||
798 op == UserOperation.SavePost
800 let data = wsJsonToRes<PostResponse>(msg).data;
801 editPostFindRes(data.post_view, this.state.posts);
802 this.setState(this.state);
803 } else if (op == UserOperation.CreatePostLike) {
804 let data = wsJsonToRes<PostResponse>(msg).data;
805 createPostLikeFindRes(data.post_view, this.state.posts);
806 this.setState(this.state);
807 } else if (op == UserOperation.AddAdmin) {
808 let data = wsJsonToRes<AddAdminResponse>(msg).data;
809 this.state.siteRes.admins = data.admins;
810 this.setState(this.state);
811 } else if (op == UserOperation.BanPerson) {
812 let data = wsJsonToRes<BanPersonResponse>(msg).data;
813 let found = this.state.siteRes.banned.find(
814 p => (p.person.id = data.person_view.person.id)
817 // Remove the banned if its found in the list, and the action is an unban
818 if (found && !data.banned) {
819 this.state.siteRes.banned = this.state.siteRes.banned.filter(
820 i => i.person.id !== data.person_view.person.id
823 this.state.siteRes.banned.push(data.person_view);
827 .filter(p => p.creator.id == data.person_view.person.id)
828 .forEach(p => (p.creator.banned = data.banned));
830 this.setState(this.state);
831 } else if (op == UserOperation.GetComments) {
832 let data = wsJsonToRes<GetCommentsResponse>(msg).data;
833 this.state.comments = data.comments;
834 this.state.loading = false;
835 this.setState(this.state);
837 op == UserOperation.EditComment ||
838 op == UserOperation.DeleteComment ||
839 op == UserOperation.RemoveComment
841 let data = wsJsonToRes<CommentResponse>(msg).data;
842 editCommentRes(data.comment_view, this.state.comments);
843 this.setState(this.state);
844 } else if (op == UserOperation.CreateComment) {
845 let data = wsJsonToRes<CommentResponse>(msg).data;
847 // Necessary since it might be a user reply
849 // If you're on subscribed, only push it if you're subscribed.
850 if (this.state.listingType == ListingType.Subscribed) {
852 this.state.subscribedCommunities
853 .map(c => c.community.id)
854 .includes(data.comment_view.community.id)
856 this.state.comments.unshift(data.comment_view);
859 this.state.comments.unshift(data.comment_view);
861 this.setState(this.state);
863 } else if (op == UserOperation.SaveComment) {
864 let data = wsJsonToRes<CommentResponse>(msg).data;
865 saveCommentRes(data.comment_view, this.state.comments);
866 this.setState(this.state);
867 } else if (op == UserOperation.CreateCommentLike) {
868 let data = wsJsonToRes<CommentResponse>(msg).data;
869 createCommentLikeRes(data.comment_view, this.state.comments);
870 this.setState(this.state);