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 { UserListing } from "./user-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.user) {
134 this.state.subscribedCommunities = this.isoData.routeData[2].communities;
136 this.state.loading = false;
138 this.fetchTrendingCommunities();
140 if (UserService.Instance.user) {
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.user
198 ? Object.values(ListingType)[
199 UserService.Instance.user.default_listing_type
202 let sort: SortType = pathSplit[7]
203 ? SortType[pathSplit[7]]
204 : UserService.Instance.user
205 ? Object.values(SortType)[UserService.Instance.user.default_sort_type]
208 let page = pathSplit[9] ? Number(pathSplit[9]) : 1;
210 let promises: Promise<any>[] = [];
212 if (dataType == DataType.Post) {
213 let getPostsForm: GetPosts = {
219 setOptionalAuth(getPostsForm, req.auth);
220 promises.push(req.client.getPosts(getPostsForm));
222 let getCommentsForm: GetComments = {
228 setOptionalAuth(getCommentsForm, req.auth);
229 promises.push(req.client.getComments(getCommentsForm));
232 let trendingCommunitiesForm: ListCommunities = {
233 type_: ListingType.Local,
237 promises.push(req.client.listCommunities(trendingCommunitiesForm));
240 promises.push(req.client.getFollowedCommunities({ auth: req.auth }));
246 componentDidUpdate(_: any, lastState: MainState) {
248 lastState.listingType !== this.state.listingType ||
249 lastState.dataType !== this.state.dataType ||
250 lastState.sort !== this.state.sort ||
251 lastState.page !== this.state.page
253 this.setState({ loading: true });
258 get documentTitle(): string {
260 this.state.siteRes.site_view
261 ? this.state.siteRes.site_view.site.name
268 <div class="container">
270 title={this.documentTitle}
271 path={this.context.router.route.match.url}
273 {this.state.siteRes.site_view?.site && (
275 <main role="main" class="col-12 col-md-8">
278 <aside class="col-12 col-md-4">{this.mySidebar()}</aside>
288 {!this.state.loading && (
290 <div class="card border-secondary mb-3">
291 <div class="card-body">
292 {this.trendingCommunities()}
293 {this.createCommunityButton()}
297 {UserService.Instance.user &&
298 this.state.subscribedCommunities.length > 0 && (
299 <div class="card border-secondary mb-3">
300 <div class="card-body">{this.subscribedCommunities()}</div>
304 <div class="card border-secondary mb-3">
305 <div class="card-body">{this.sidebar()}</div>
313 createCommunityButton() {
315 <Link className="btn btn-secondary btn-block" to="/create_community">
316 {i18n.t("create_a_community")}
321 trendingCommunities() {
325 <T i18nKey="trending_communities">
327 <Link className="text-body" to="/communities">
332 <ul class="list-inline">
333 {this.state.trendingCommunities.map(cv => (
334 <li class="list-inline-item d-inline-block">
335 <CommunityLink community={cv.community} />
343 subscribedCommunities() {
347 <T i18nKey="subscribed_to_communities">
349 <Link className="text-body" to="/communities">
354 <ul class="list-inline mb-0">
355 {this.state.subscribedCommunities.map(cfv => (
356 <li class="list-inline-item d-inline-block">
357 <CommunityLink community={cfv.community} />
366 let site = this.state.siteRes.site_view.site;
369 {!this.state.showEditSite ? (
373 {this.adminButtons()}
375 <BannerIconHeader banner={site.banner} />
379 <SiteForm site={site} onCancel={this.handleEditCancel} />
385 updateUrl(paramUpdates: UrlParams) {
386 const listingTypeStr = paramUpdates.listingType || this.state.listingType;
387 const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
388 const sortStr = paramUpdates.sort || this.state.sort;
389 const page = paramUpdates.page || this.state.page;
390 this.props.history.push(
391 `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
398 {this.state.siteRes.site_view.site.description &&
399 this.siteDescription()}
407 return <h5 class="mb-0">{`${this.documentTitle}`}</h5>;
412 <ul class="mt-1 list-inline small mb-0">
413 <li class="list-inline-item">{i18n.t("admins")}:</li>
414 {this.state.siteRes.admins.map(av => (
415 <li class="list-inline-item">
416 <UserListing user={av.user} />
424 let counts = this.state.siteRes.site_view.counts;
426 <ul class="my-2 list-inline">
427 <li className="list-inline-item badge badge-secondary">
428 {i18n.t("number_online", { count: this.state.siteRes.online })}
431 className="list-inline-item badge badge-secondary pointer"
432 data-tippy-content={`${i18n.t("number_of_users", {
433 count: counts.users_active_day,
434 })} ${i18n.t("active_in_the_last")} ${i18n.t("day")}`}
436 {i18n.t("number_of_users", {
437 count: counts.users_active_day,
442 className="list-inline-item badge badge-secondary pointer"
443 data-tippy-content={`${i18n.t("number_of_users", {
444 count: counts.users_active_week,
445 })} ${i18n.t("active_in_the_last")} ${i18n.t("week")}`}
447 {i18n.t("number_of_users", {
448 count: counts.users_active_week,
453 className="list-inline-item badge badge-secondary pointer"
454 data-tippy-content={`${i18n.t("number_of_users", {
455 count: counts.users_active_month,
456 })} ${i18n.t("active_in_the_last")} ${i18n.t("month")}`}
458 {i18n.t("number_of_users", {
459 count: counts.users_active_month,
464 className="list-inline-item badge badge-secondary pointer"
465 data-tippy-content={`${i18n.t("number_of_users", {
466 count: counts.users_active_half_year,
467 })} ${i18n.t("active_in_the_last")} ${i18n.t("number_of_months", {
471 {i18n.t("number_of_users", {
472 count: counts.users_active_half_year,
474 / {i18n.t("number_of_months", { count: 6 })}
476 <li className="list-inline-item badge badge-secondary">
477 {i18n.t("number_of_subscribers", {
481 <li className="list-inline-item badge badge-secondary">
482 {i18n.t("number_of_communities", {
483 count: counts.communities,
486 <li className="list-inline-item badge badge-secondary">
487 {i18n.t("number_of_posts", {
491 <li className="list-inline-item badge badge-secondary">
492 {i18n.t("number_of_comments", {
493 count: counts.comments,
496 <li className="list-inline-item">
497 <Link className="badge badge-secondary" to="/modlog">
508 <ul class="list-inline mb-1 text-muted font-weight-bold">
509 <li className="list-inline-item-action">
513 onClick={linkEvent(this, this.handleEditClick)}
514 aria-label={i18n.t("edit")}
515 data-tippy-content={i18n.t("edit")}
517 <Icon icon="edit" classes="icon-inline" />
529 dangerouslySetInnerHTML={mdToHtml(
530 this.state.siteRes.site_view.site.description
538 <div class="main-content-wrapper">
539 {this.state.loading ? (
555 let site = this.state.siteRes.site_view.site;
556 return this.state.dataType == DataType.Post ? (
558 posts={this.state.posts}
561 enableDownvotes={site.enable_downvotes}
562 enableNsfw={site.enable_nsfw}
566 nodes={commentsToFlatNodes(this.state.comments)}
570 enableDownvotes={site.enable_downvotes}
577 <div className="mb-3">
580 type_={this.state.dataType}
581 onChange={this.handleDataTypeChange}
586 type_={this.state.listingType}
587 showLocal={this.showLocal}
588 onChange={this.handleListingTypeChange}
592 <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
594 {this.state.listingType == ListingType.All && (
596 href={`/feeds/all.xml?sort=${this.state.sort}`}
600 <Icon icon="rss" classes="text-muted small" />
603 {this.state.listingType == ListingType.Local && (
605 href={`/feeds/local.xml?sort=${this.state.sort}`}
609 <Icon icon="rss" classes="text-muted small" />
612 {UserService.Instance.user &&
613 this.state.listingType == ListingType.Subscribed && (
615 href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${this.state.sort}`}
619 <Icon icon="rss" classes="text-muted small" />
629 {this.state.page > 1 && (
631 class="btn btn-secondary mr-1"
632 onClick={linkEvent(this, this.prevPage)}
637 {this.state.posts.length > 0 && (
639 class="btn btn-secondary"
640 onClick={linkEvent(this, this.nextPage)}
649 get showLocal(): boolean {
650 return this.isoData.site_res.federated_instances?.linked.length > 0;
653 get canAdmin(): boolean {
655 UserService.Instance.user &&
656 this.state.siteRes.admins
658 .includes(UserService.Instance.user.id)
662 handleEditClick(i: Main) {
663 i.state.showEditSite = true;
668 this.state.showEditSite = false;
669 this.setState(this.state);
673 i.updateUrl({ page: i.state.page + 1 });
674 window.scrollTo(0, 0);
678 i.updateUrl({ page: i.state.page - 1 });
679 window.scrollTo(0, 0);
682 handleSortChange(val: SortType) {
683 this.updateUrl({ sort: val, page: 1 });
684 window.scrollTo(0, 0);
687 handleListingTypeChange(val: ListingType) {
688 this.updateUrl({ listingType: val, page: 1 });
689 window.scrollTo(0, 0);
692 handleDataTypeChange(val: DataType) {
693 this.updateUrl({ dataType: DataType[val], page: 1 });
694 window.scrollTo(0, 0);
698 if (this.state.dataType == DataType.Post) {
699 let getPostsForm: GetPosts = {
700 page: this.state.page,
702 sort: this.state.sort,
703 type_: this.state.listingType,
704 auth: authField(false),
706 WebSocketService.Instance.send(wsClient.getPosts(getPostsForm));
708 let getCommentsForm: GetComments = {
709 page: this.state.page,
711 sort: this.state.sort,
712 type_: this.state.listingType,
713 auth: authField(false),
715 WebSocketService.Instance.send(wsClient.getComments(getCommentsForm));
719 parseMessage(msg: any) {
720 let op = wsUserOp(msg);
722 toast(i18n.t(msg.error), "danger");
724 } else if (msg.reconnect) {
725 WebSocketService.Instance.send(
726 wsClient.communityJoin({ community_id: 0 })
729 } else if (op == UserOperation.GetFollowedCommunities) {
730 let data = wsJsonToRes<GetFollowedCommunitiesResponse>(msg).data;
731 this.state.subscribedCommunities = data.communities;
732 this.setState(this.state);
733 } else if (op == UserOperation.ListCommunities) {
734 let data = wsJsonToRes<ListCommunitiesResponse>(msg).data;
735 this.state.trendingCommunities = data.communities;
736 this.setState(this.state);
737 } else if (op == UserOperation.EditSite) {
738 let data = wsJsonToRes<SiteResponse>(msg).data;
739 this.state.siteRes.site_view = data.site_view;
740 this.state.showEditSite = false;
741 this.setState(this.state);
742 toast(i18n.t("site_saved"));
743 } else if (op == UserOperation.GetPosts) {
744 let data = wsJsonToRes<GetPostsResponse>(msg).data;
745 this.state.posts = data.posts;
746 this.state.loading = false;
747 this.setState(this.state);
748 restoreScrollPosition(this.context);
750 } else if (op == UserOperation.CreatePost) {
751 let data = wsJsonToRes<PostResponse>(msg).data;
754 let nsfw = data.post_view.post.nsfw || data.post_view.community.nsfw;
758 UserService.Instance.user &&
759 UserService.Instance.user.show_nsfw);
761 // Only push these if you're on the first page, and you pass the nsfw check
762 if (this.state.page == 1 && nsfwCheck) {
763 // If you're on subscribed, only push it if you're subscribed.
764 if (this.state.listingType == ListingType.Subscribed) {
766 this.state.subscribedCommunities
767 .map(c => c.community.id)
768 .includes(data.post_view.community.id)
770 this.state.posts.unshift(data.post_view);
771 notifyPost(data.post_view, this.context.router);
773 } else if (this.state.listingType == ListingType.Local) {
774 // If you're on the local view, only push it if its local
775 if (data.post_view.post.local) {
776 this.state.posts.unshift(data.post_view);
777 notifyPost(data.post_view, this.context.router);
780 this.state.posts.unshift(data.post_view);
781 notifyPost(data.post_view, this.context.router);
783 this.setState(this.state);
786 op == UserOperation.EditPost ||
787 op == UserOperation.DeletePost ||
788 op == UserOperation.RemovePost ||
789 op == UserOperation.LockPost ||
790 op == UserOperation.StickyPost ||
791 op == UserOperation.SavePost
793 let data = wsJsonToRes<PostResponse>(msg).data;
794 editPostFindRes(data.post_view, this.state.posts);
795 this.setState(this.state);
796 } else if (op == UserOperation.CreatePostLike) {
797 let data = wsJsonToRes<PostResponse>(msg).data;
798 createPostLikeFindRes(data.post_view, this.state.posts);
799 this.setState(this.state);
800 } else if (op == UserOperation.AddAdmin) {
801 let data = wsJsonToRes<AddAdminResponse>(msg).data;
802 this.state.siteRes.admins = data.admins;
803 this.setState(this.state);
804 } else if (op == UserOperation.BanUser) {
805 let data = wsJsonToRes<BanUserResponse>(msg).data;
806 let found = this.state.siteRes.banned.find(
807 u => (u.user.id = data.user_view.user.id)
810 // Remove the banned if its found in the list, and the action is an unban
811 if (found && !data.banned) {
812 this.state.siteRes.banned = this.state.siteRes.banned.filter(
813 i => i.user.id !== data.user_view.user.id
816 this.state.siteRes.banned.push(data.user_view);
820 .filter(p => p.creator.id == data.user_view.user.id)
821 .forEach(p => (p.creator.banned = data.banned));
823 this.setState(this.state);
824 } else if (op == UserOperation.GetComments) {
825 let data = wsJsonToRes<GetCommentsResponse>(msg).data;
826 this.state.comments = data.comments;
827 this.state.loading = false;
828 this.setState(this.state);
830 op == UserOperation.EditComment ||
831 op == UserOperation.DeleteComment ||
832 op == UserOperation.RemoveComment
834 let data = wsJsonToRes<CommentResponse>(msg).data;
835 editCommentRes(data.comment_view, this.state.comments);
836 this.setState(this.state);
837 } else if (op == UserOperation.CreateComment) {
838 let data = wsJsonToRes<CommentResponse>(msg).data;
840 // Necessary since it might be a user reply
842 // If you're on subscribed, only push it if you're subscribed.
843 if (this.state.listingType == ListingType.Subscribed) {
845 this.state.subscribedCommunities
846 .map(c => c.community.id)
847 .includes(data.comment_view.community.id)
849 this.state.comments.unshift(data.comment_view);
852 this.state.comments.unshift(data.comment_view);
854 this.setState(this.state);
856 } else if (op == UserOperation.SaveComment) {
857 let data = wsJsonToRes<CommentResponse>(msg).data;
858 saveCommentRes(data.comment_view, this.state.comments);
859 this.setState(this.state);
860 } else if (op == UserOperation.CreateCommentLike) {
861 let data = wsJsonToRes<CommentResponse>(msg).data;
862 createCommentLikeRes(data.comment_view, this.state.comments);
863 this.setState(this.state);