1 import { Component, linkEvent } from "inferno";
2 import { RouteComponentProps } from "inferno-router/dist/Route";
4 AddModToCommunityResponse,
5 BanFromCommunityResponse,
6 BlockCommunityResponse,
26 } from "lemmy-js-client";
27 import { Subscription } from "rxjs";
28 import { i18n } from "../../i18next";
33 } from "../../interfaces";
34 import { UserService, WebSocketService } from "../../services";
40 createPostLikeFindRes,
54 postToCommentSortType,
56 restoreScrollPosition,
70 import { CommentNodes } from "../comment/comment-nodes";
71 import { BannerIconHeader } from "../common/banner-icon-header";
72 import { DataTypeSelect } from "../common/data-type-select";
73 import { HtmlTags } from "../common/html-tags";
74 import { Icon, Spinner } from "../common/icon";
75 import { Paginator } from "../common/paginator";
76 import { SortSelect } from "../common/sort-select";
77 import { Sidebar } from "../community/sidebar";
78 import { SiteSidebar } from "../home/site-sidebar";
79 import { PostListings } from "../post/post-listings";
80 import { CommunityLink } from "./community-link";
83 communityRes?: GetCommunityResponse;
84 communityLoading: boolean;
85 listingsLoading: boolean;
87 comments: CommentView[];
88 showSidebarMobile: boolean;
91 interface CommunityProps {
97 function getCommunityQueryParams() {
98 return getQueryParams<CommunityProps>({
99 dataType: getDataTypeFromQuery,
100 page: getPageFromString,
101 sort: getSortTypeFromQuery,
105 const getDataTypeFromQuery = (type?: string): DataType =>
106 routeDataTypeToEnum(type ?? "", DataType.Post);
108 function getSortTypeFromQuery(type?: string): SortType {
110 UserService.Instance.myUserInfo?.local_user_view.local_user
113 return routeSortTypeToEnum(
115 mySortType ? Object.values(SortType)[mySortType] : SortType.Active
119 export class Community extends Component<
120 RouteComponentProps<{ name: string }>,
123 private isoData = setIsoData(this.context);
124 private subscription?: Subscription;
126 communityLoading: true,
127 listingsLoading: true,
130 showSidebarMobile: false,
133 constructor(props: RouteComponentProps<{ name: string }>, context: any) {
134 super(props, context);
136 this.handleSortChange = this.handleSortChange.bind(this);
137 this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
138 this.handlePageChange = this.handlePageChange.bind(this);
140 this.parseMessage = this.parseMessage.bind(this);
141 this.subscription = wsSubscribe(this.parseMessage);
143 // Only fetch the data if coming from another route
144 if (this.isoData.path == this.context.router.route.match.url) {
147 communityRes: this.isoData.routeData[0] as GetCommunityResponse,
149 const postsRes = this.isoData.routeData[1] as
152 const commentsRes = this.isoData.routeData[2] as
153 | GetCommentsResponse
157 this.state = { ...this.state, posts: postsRes.posts };
161 this.state = { ...this.state, comments: commentsRes.comments };
166 communityLoading: false,
167 listingsLoading: false,
170 this.fetchCommunity();
176 const form: GetCommunity = {
177 name: this.props.match.params.name,
180 WebSocketService.Instance.send(wsClient.getCommunity(form));
183 componentDidMount() {
187 componentWillUnmount() {
188 saveScrollPosition(this.context);
189 this.subscription?.unsubscribe();
192 static fetchInitialData({
195 query: { dataType: urlDataType, page: urlPage, sort: urlSort },
197 }: InitialFetchRequest<QueryParams<CommunityProps>>): Promise<any>[] {
198 const pathSplit = path.split("/");
199 const promises: Promise<any>[] = [];
201 const communityName = pathSplit[2];
202 const communityForm: GetCommunity = {
206 promises.push(client.getCommunity(communityForm));
208 const dataType = getDataTypeFromQuery(urlDataType);
210 const sort = getSortTypeFromQuery(urlSort);
212 const page = getPageFromString(urlPage);
214 if (dataType === DataType.Post) {
215 const getPostsForm: GetPosts = {
216 community_name: communityName,
220 type_: ListingType.All,
224 promises.push(client.getPosts(getPostsForm));
225 promises.push(Promise.resolve());
227 const getCommentsForm: GetComments = {
228 community_name: communityName,
231 sort: postToCommentSortType(sort),
232 type_: ListingType.All,
236 promises.push(Promise.resolve());
237 promises.push(client.getComments(getCommentsForm));
243 get documentTitle(): string {
244 const cRes = this.state.communityRes;
246 ? `${cRes.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}`
251 const res = this.state.communityRes;
252 const { page } = getCommunityQueryParams();
255 <div className="container-lg">
256 {this.state.communityLoading ? (
264 title={this.documentTitle}
265 path={this.context.router.route.match.url}
266 description={res.community_view.community.description}
267 image={res.community_view.community.icon}
270 <div className="row">
271 <div className="col-12 col-md-8">
273 <div className="d-block d-md-none">
275 className="btn btn-secondary d-inline-block mb-2 mr-3"
276 onClick={linkEvent(this, this.handleShowSidebarMobile)}
278 {i18n.t("sidebar")}{" "}
281 this.state.showSidebarMobile
285 classes="icon-inline"
288 {this.state.showSidebarMobile && this.sidebar(res)}
292 <Paginator page={page} onChange={this.handlePageChange} />
294 <div className="d-none d-md-block col-md-4">
309 discussion_languages,
311 }: GetCommunityResponse) {
312 const { site_res } = this.isoData;
313 // For some reason, this returns an empty vec if it matches the site langs
314 const communityLangs =
315 discussion_languages.length === 0
316 ? site_res.all_languages.map(({ id }) => id)
317 : discussion_languages;
322 community_view={community_view}
323 moderators={moderators}
324 admins={site_res.admins}
326 enableNsfw={enableNsfw(site_res)}
328 allLanguages={site_res.all_languages}
329 siteLanguages={site_res.discussion_languages}
330 communityLanguages={communityLangs}
332 {!community_view.community.local && site && (
333 <SiteSidebar site={site} showLocal={showLocal(this.isoData)} />
340 const { dataType } = getCommunityQueryParams();
341 const { site_res } = this.isoData;
342 const { listingsLoading, posts, comments, communityRes } = this.state;
344 if (listingsLoading) {
350 } else if (dataType === DataType.Post) {
355 enableDownvotes={enableDownvotes(site_res)}
356 enableNsfw={enableNsfw(site_res)}
357 allLanguages={site_res.all_languages}
358 siteLanguages={site_res.discussion_languages}
364 nodes={commentsToFlatNodes(comments)}
365 viewType={CommentViewType.Flat}
368 enableDownvotes={enableDownvotes(site_res)}
369 moderators={communityRes?.moderators}
370 admins={site_res.admins}
371 allLanguages={site_res.all_languages}
372 siteLanguages={site_res.discussion_languages}
378 get communityInfo() {
379 const community = this.state.communityRes?.community_view.community;
383 <div className="mb-2">
384 <BannerIconHeader banner={community.banner} icon={community.icon} />
385 <h5 className="mb-0 overflow-wrap-anywhere">{community.title}</h5>
387 community={community}
399 // let communityRss = this.state.communityRes.map(r =>
400 // communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
402 const { dataType, sort } = getCommunityQueryParams();
403 const res = this.state.communityRes;
404 const communityRss = res
405 ? communityRSSUrl(res.community_view.community.actor_id, sort)
409 <div className="mb-3">
410 <span className="mr-3">
413 onChange={this.handleDataTypeChange}
416 <span className="mr-2">
417 <SortSelect sort={sort} onChange={this.handleSortChange} />
421 <a href={communityRss} title="RSS" rel={relTags}>
422 <Icon icon="rss" classes="text-muted small" />
426 type="application/atom+xml"
435 handlePageChange(page: number) {
436 this.updateUrl({ page });
437 window.scrollTo(0, 0);
440 handleSortChange(sort: SortType) {
441 this.updateUrl({ sort, page: 1 });
442 window.scrollTo(0, 0);
445 handleDataTypeChange(dataType: DataType) {
446 this.updateUrl({ dataType, page: 1 });
447 window.scrollTo(0, 0);
450 handleShowSidebarMobile(i: Community) {
451 i.setState(({ showSidebarMobile }) => ({
452 showSidebarMobile: !showSidebarMobile,
456 updateUrl({ dataType, page, sort }: Partial<CommunityProps>) {
458 dataType: urlDataType,
461 } = getCommunityQueryParams();
463 const queryParams: QueryParams<CommunityProps> = {
464 dataType: getDataTypeString(dataType ?? urlDataType),
465 page: (page ?? urlPage).toString(),
466 sort: sort ?? urlSort,
469 this.props.history.push(
470 `/c/${this.props.match.params.name}${getQueryString(queryParams)}`
476 listingsLoading: true,
483 const { dataType, page, sort } = getCommunityQueryParams();
484 const { name } = this.props.match.params;
487 if (dataType === DataType.Post) {
488 const form: GetPosts = {
492 type_: ListingType.All,
493 community_name: name,
497 req = wsClient.getPosts(form);
499 const form: GetComments = {
502 sort: postToCommentSortType(sort),
503 type_: ListingType.All,
504 community_name: name,
509 req = wsClient.getComments(form);
512 WebSocketService.Instance.send(req);
515 parseMessage(msg: any) {
516 const { page } = getCommunityQueryParams();
517 const op = wsUserOp(msg);
519 const res = this.state.communityRes;
522 toast(i18n.t(msg.error), "danger");
523 this.context.router.history.push("/");
524 } else if (msg.reconnect) {
526 WebSocketService.Instance.send(
527 wsClient.communityJoin({
528 community_id: res.community_view.community.id,
536 case UserOperation.GetCommunity: {
537 const data = wsJsonToRes<GetCommunityResponse>(msg);
539 this.setState({ communityRes: data, communityLoading: false });
540 // TODO why is there no auth in this form?
541 WebSocketService.Instance.send(
542 wsClient.communityJoin({
543 community_id: data.community_view.community.id,
550 case UserOperation.EditCommunity:
551 case UserOperation.DeleteCommunity:
552 case UserOperation.RemoveCommunity: {
553 const { community_view, discussion_languages } =
554 wsJsonToRes<CommunityResponse>(msg);
557 res.community_view = community_view;
558 res.discussion_languages = discussion_languages;
559 this.setState(this.state);
565 case UserOperation.FollowCommunity: {
569 counts: { subscribers },
571 } = wsJsonToRes<CommunityResponse>(msg);
574 res.community_view.subscribed = subscribed;
575 res.community_view.counts.subscribers = subscribers;
576 this.setState(this.state);
582 case UserOperation.GetPosts: {
583 const { posts } = wsJsonToRes<GetPostsResponse>(msg);
585 this.setState({ posts, listingsLoading: false });
586 restoreScrollPosition(this.context);
592 case UserOperation.EditPost:
593 case UserOperation.DeletePost:
594 case UserOperation.RemovePost:
595 case UserOperation.LockPost:
596 case UserOperation.FeaturePost:
597 case UserOperation.SavePost: {
598 const { post_view } = wsJsonToRes<PostResponse>(msg);
600 editPostFindRes(post_view, this.state.posts);
601 this.setState(this.state);
606 case UserOperation.CreatePost: {
607 const { post_view } = wsJsonToRes<PostResponse>(msg);
609 const showPostNotifs =
610 UserService.Instance.myUserInfo?.local_user_view.local_user
611 .show_new_post_notifs;
613 // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
614 if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
615 this.state.posts.unshift(post_view);
616 if (showPostNotifs) {
617 notifyPost(post_view, this.context.router);
619 this.setState(this.state);
625 case UserOperation.CreatePostLike: {
626 const { post_view } = wsJsonToRes<PostResponse>(msg);
628 createPostLikeFindRes(post_view, this.state.posts);
629 this.setState(this.state);
634 case UserOperation.AddModToCommunity: {
635 const { moderators } = wsJsonToRes<AddModToCommunityResponse>(msg);
638 res.moderators = moderators;
639 this.setState(this.state);
645 case UserOperation.BanFromCommunity: {
648 person: { id: personId },
651 } = wsJsonToRes<BanFromCommunityResponse>(msg);
653 // TODO this might be incorrect
655 .filter(p => p.creator.id === personId)
656 .forEach(p => (p.creator_banned_from_community = banned));
658 this.setState(this.state);
663 case UserOperation.GetComments: {
664 const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
665 this.setState({ comments, listingsLoading: false });
670 case UserOperation.EditComment:
671 case UserOperation.DeleteComment:
672 case UserOperation.RemoveComment: {
673 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
674 editCommentRes(comment_view, this.state.comments);
675 this.setState(this.state);
680 case UserOperation.CreateComment: {
681 const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
683 // Necessary since it might be a user reply
685 this.setState(({ comments }) => ({
686 comments: [comment_view].concat(comments),
693 case UserOperation.SaveComment: {
694 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
696 saveCommentRes(comment_view, this.state.comments);
697 this.setState(this.state);
702 case UserOperation.CreateCommentLike: {
703 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
705 createCommentLikeRes(comment_view, this.state.comments);
706 this.setState(this.state);
711 case UserOperation.BlockPerson: {
712 const data = wsJsonToRes<BlockPersonResponse>(msg);
713 updatePersonBlock(data);
718 case UserOperation.CreatePostReport:
719 case UserOperation.CreateCommentReport: {
720 const data = wsJsonToRes<PostReportResponse>(msg);
723 toast(i18n.t("report_created"));
729 case UserOperation.PurgeCommunity: {
730 const { success } = wsJsonToRes<PurgeItemResponse>(msg);
733 toast(i18n.t("purge_success"));
734 this.context.router.history.push(`/`);
740 case UserOperation.BlockCommunity: {
741 const data = wsJsonToRes<BlockCommunityResponse>(msg);
743 res.community_view.blocked = data.blocked;
744 this.setState(this.state);
746 updateCommunityBlock(data);