1 import { Component, linkEvent } from "inferno";
2 import { RouteComponentProps } from "inferno-router/dist/Route";
4 AddModToCommunityResponse,
5 BanFromCommunityResponse,
6 BlockCommunityResponse,
25 } from "lemmy-js-client";
26 import { Subscription } from "rxjs";
27 import { i18n } from "../../i18next";
32 } from "../../interfaces";
33 import { UserService, WebSocketService } from "../../services";
40 createPostLikeFindRes,
54 postToCommentSortType,
56 restoreScrollPosition,
68 import { CommentNodes } from "../comment/comment-nodes";
69 import { BannerIconHeader } from "../common/banner-icon-header";
70 import { DataTypeSelect } from "../common/data-type-select";
71 import { HtmlTags } from "../common/html-tags";
72 import { Icon, Spinner } from "../common/icon";
73 import { Paginator } from "../common/paginator";
74 import { SortSelect } from "../common/sort-select";
75 import { Sidebar } from "../community/sidebar";
76 import { SiteSidebar } from "../home/site-sidebar";
77 import { PostListings } from "../post/post-listings";
78 import { CommunityLink } from "./community-link";
80 interface CommunityData {
81 communityResponse: GetCommunityResponse;
82 postsResponse?: GetPostsResponse;
83 commentsResponse?: GetCommentsResponse;
87 communityRes?: GetCommunityResponse;
88 communityLoading: boolean;
89 listingsLoading: boolean;
91 comments: CommentView[];
92 showSidebarMobile: boolean;
95 interface CommunityProps {
101 function getCommunityQueryParams() {
102 return getQueryParams<CommunityProps>({
103 dataType: getDataTypeFromQuery,
104 page: getPageFromString,
105 sort: getSortTypeFromQuery,
109 function getDataTypeFromQuery(type?: string): DataType {
110 return type ? DataType[type] : DataType.Post;
113 function getSortTypeFromQuery(type?: string): SortType {
115 UserService.Instance.myUserInfo?.local_user_view.local_user
118 return type ? (type as SortType) : mySortType ?? "Active";
121 export class Community extends Component<
122 RouteComponentProps<{ name: string }>,
125 private isoData = setIsoData<CommunityData>(this.context);
126 private subscription?: Subscription;
128 communityLoading: true,
129 listingsLoading: true,
132 showSidebarMobile: false,
135 constructor(props: RouteComponentProps<{ name: string }>, context: any) {
136 super(props, context);
138 this.handleSortChange = this.handleSortChange.bind(this);
139 this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
140 this.handlePageChange = this.handlePageChange.bind(this);
142 this.parseMessage = this.parseMessage.bind(this);
143 this.subscription = wsSubscribe(this.parseMessage);
145 // Only fetch the data if coming from another route
146 if (this.isoData.path == this.context.router.route.match.url) {
147 const { communityResponse, commentsResponse, postsResponse } =
148 this.isoData.routeData;
152 communityRes: communityResponse,
156 this.state = { ...this.state, posts: postsResponse.posts };
159 if (commentsResponse) {
160 this.state = { ...this.state, comments: commentsResponse.comments };
165 communityLoading: false,
166 listingsLoading: false,
169 this.fetchCommunity();
175 const form: GetCommunity = {
176 name: this.props.match.params.name,
179 WebSocketService.Instance.send(wsClient.getCommunity(form));
182 componentDidMount() {
186 componentWillUnmount() {
187 saveScrollPosition(this.context);
188 this.subscription?.unsubscribe();
191 static fetchInitialData({
194 query: { dataType: urlDataType, page: urlPage, sort: urlSort },
196 }: InitialFetchRequest<
197 QueryParams<CommunityProps>
198 >): WithPromiseKeys<CommunityData> {
199 const pathSplit = path.split("/");
201 const communityName = pathSplit[2];
202 const communityForm: GetCommunity = {
207 const dataType = getDataTypeFromQuery(urlDataType);
209 const sort = getSortTypeFromQuery(urlSort);
211 const page = getPageFromString(urlPage);
213 let postsResponse: Promise<GetPostsResponse> | undefined = undefined;
214 let commentsResponse: Promise<GetCommentsResponse> | undefined = undefined;
216 if (dataType === DataType.Post) {
217 const getPostsForm: GetPosts = {
218 community_name: communityName,
227 postsResponse = client.getPosts(getPostsForm);
229 const getCommentsForm: GetComments = {
230 community_name: communityName,
233 sort: postToCommentSortType(sort),
239 commentsResponse = client.getComments(getCommentsForm);
243 communityResponse: client.getCommunity(communityForm),
249 get documentTitle(): string {
250 const cRes = this.state.communityRes;
252 ? `${cRes.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}`
257 const res = this.state.communityRes;
258 const { page } = getCommunityQueryParams();
261 <div className="container-lg">
262 {this.state.communityLoading ? (
270 title={this.documentTitle}
271 path={this.context.router.route.match.url}
272 description={res.community_view.community.description}
273 image={res.community_view.community.icon}
276 <div className="row">
277 <div className="col-12 col-md-8">
279 <div className="d-block d-md-none">
281 className="btn btn-secondary d-inline-block mb-2 mr-3"
282 onClick={linkEvent(this, this.handleShowSidebarMobile)}
284 {i18n.t("sidebar")}{" "}
287 this.state.showSidebarMobile
291 classes="icon-inline"
294 {this.state.showSidebarMobile && this.sidebar(res)}
298 <Paginator page={page} onChange={this.handlePageChange} />
300 <div className="d-none d-md-block col-md-4">
315 discussion_languages,
317 }: GetCommunityResponse) {
318 const { site_res } = this.isoData;
319 // For some reason, this returns an empty vec if it matches the site langs
320 const communityLangs =
321 discussion_languages.length === 0
322 ? site_res.all_languages.map(({ id }) => id)
323 : discussion_languages;
328 community_view={community_view}
329 moderators={moderators}
330 admins={site_res.admins}
332 enableNsfw={enableNsfw(site_res)}
334 allLanguages={site_res.all_languages}
335 siteLanguages={site_res.discussion_languages}
336 communityLanguages={communityLangs}
338 {!community_view.community.local && site && (
339 <SiteSidebar site={site} showLocal={showLocal(this.isoData)} />
346 const { dataType } = getCommunityQueryParams();
347 const { site_res } = this.isoData;
348 const { listingsLoading, posts, comments, communityRes } = this.state;
350 if (listingsLoading) {
356 } else if (dataType === DataType.Post) {
361 enableDownvotes={enableDownvotes(site_res)}
362 enableNsfw={enableNsfw(site_res)}
363 allLanguages={site_res.all_languages}
364 siteLanguages={site_res.discussion_languages}
370 nodes={commentsToFlatNodes(comments)}
371 viewType={CommentViewType.Flat}
374 enableDownvotes={enableDownvotes(site_res)}
375 moderators={communityRes?.moderators}
376 admins={site_res.admins}
377 allLanguages={site_res.all_languages}
378 siteLanguages={site_res.discussion_languages}
384 get communityInfo() {
385 const community = this.state.communityRes?.community_view.community;
389 <div className="mb-2">
390 <BannerIconHeader banner={community.banner} icon={community.icon} />
391 <h5 className="mb-0 overflow-wrap-anywhere">{community.title}</h5>
393 community={community}
405 // let communityRss = this.state.communityRes.map(r =>
406 // communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
408 const { dataType, sort } = getCommunityQueryParams();
409 const res = this.state.communityRes;
410 const communityRss = res
411 ? communityRSSUrl(res.community_view.community.actor_id, sort)
415 <div className="mb-3">
416 <span className="mr-3">
419 onChange={this.handleDataTypeChange}
422 <span className="mr-2">
423 <SortSelect sort={sort} onChange={this.handleSortChange} />
427 <a href={communityRss} title="RSS" rel={relTags}>
428 <Icon icon="rss" classes="text-muted small" />
432 type="application/atom+xml"
441 handlePageChange(page: number) {
442 this.updateUrl({ page });
443 window.scrollTo(0, 0);
446 handleSortChange(sort: SortType) {
447 this.updateUrl({ sort, page: 1 });
448 window.scrollTo(0, 0);
451 handleDataTypeChange(dataType: DataType) {
452 this.updateUrl({ dataType, page: 1 });
453 window.scrollTo(0, 0);
456 handleShowSidebarMobile(i: Community) {
457 i.setState(({ showSidebarMobile }) => ({
458 showSidebarMobile: !showSidebarMobile,
462 updateUrl({ dataType, page, sort }: Partial<CommunityProps>) {
464 dataType: urlDataType,
467 } = getCommunityQueryParams();
469 const queryParams: QueryParams<CommunityProps> = {
470 dataType: getDataTypeString(dataType ?? urlDataType),
471 page: (page ?? urlPage).toString(),
472 sort: sort ?? urlSort,
475 this.props.history.push(
476 `/c/${this.props.match.params.name}${getQueryString(queryParams)}`
482 listingsLoading: true,
489 const { dataType, page, sort } = getCommunityQueryParams();
490 const { name } = this.props.match.params;
493 if (dataType === DataType.Post) {
494 const form: GetPosts = {
499 community_name: name,
503 req = wsClient.getPosts(form);
505 const form: GetComments = {
508 sort: postToCommentSortType(sort),
510 community_name: name,
515 req = wsClient.getComments(form);
518 WebSocketService.Instance.send(req);
521 parseMessage(msg: any) {
522 const { page } = getCommunityQueryParams();
523 const op = wsUserOp(msg);
525 const res = this.state.communityRes;
528 toast(i18n.t(msg.error), "danger");
529 this.context.router.history.push("/");
530 } else if (msg.reconnect) {
532 WebSocketService.Instance.send(
533 wsClient.communityJoin({
534 community_id: res.community_view.community.id,
542 case UserOperation.GetCommunity: {
543 const data = wsJsonToRes<GetCommunityResponse>(msg);
545 this.setState({ communityRes: data, communityLoading: false });
546 // TODO why is there no auth in this form?
547 WebSocketService.Instance.send(
548 wsClient.communityJoin({
549 community_id: data.community_view.community.id,
556 case UserOperation.EditCommunity:
557 case UserOperation.DeleteCommunity:
558 case UserOperation.RemoveCommunity: {
559 const { community_view, discussion_languages } =
560 wsJsonToRes<CommunityResponse>(msg);
563 res.community_view = community_view;
564 res.discussion_languages = discussion_languages;
565 this.setState(this.state);
571 case UserOperation.FollowCommunity: {
575 counts: { subscribers },
577 } = wsJsonToRes<CommunityResponse>(msg);
580 res.community_view.subscribed = subscribed;
581 res.community_view.counts.subscribers = subscribers;
582 this.setState(this.state);
588 case UserOperation.GetPosts: {
589 const { posts } = wsJsonToRes<GetPostsResponse>(msg);
591 this.setState({ posts, listingsLoading: false });
592 restoreScrollPosition(this.context);
598 case UserOperation.EditPost:
599 case UserOperation.DeletePost:
600 case UserOperation.RemovePost:
601 case UserOperation.LockPost:
602 case UserOperation.FeaturePost:
603 case UserOperation.SavePost: {
604 const { post_view } = wsJsonToRes<PostResponse>(msg);
606 editPostFindRes(post_view, this.state.posts);
607 this.setState(this.state);
612 case UserOperation.CreatePost: {
613 const { post_view } = wsJsonToRes<PostResponse>(msg);
615 const showPostNotifs =
616 UserService.Instance.myUserInfo?.local_user_view.local_user
617 .show_new_post_notifs;
619 // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
620 if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
621 this.state.posts.unshift(post_view);
622 if (showPostNotifs) {
623 notifyPost(post_view, this.context.router);
625 this.setState(this.state);
631 case UserOperation.CreatePostLike: {
632 const { post_view } = wsJsonToRes<PostResponse>(msg);
634 createPostLikeFindRes(post_view, this.state.posts);
635 this.setState(this.state);
640 case UserOperation.AddModToCommunity: {
641 const { moderators } = wsJsonToRes<AddModToCommunityResponse>(msg);
644 res.moderators = moderators;
645 this.setState(this.state);
651 case UserOperation.BanFromCommunity: {
654 person: { id: personId },
657 } = wsJsonToRes<BanFromCommunityResponse>(msg);
659 // TODO this might be incorrect
661 .filter(p => p.creator.id === personId)
662 .forEach(p => (p.creator_banned_from_community = banned));
664 this.setState(this.state);
669 case UserOperation.GetComments: {
670 const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
671 this.setState({ comments, listingsLoading: false });
676 case UserOperation.EditComment:
677 case UserOperation.DeleteComment:
678 case UserOperation.RemoveComment: {
679 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
680 editCommentRes(comment_view, this.state.comments);
681 this.setState(this.state);
686 case UserOperation.CreateComment: {
687 const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
689 // Necessary since it might be a user reply
691 this.setState(({ comments }) => ({
692 comments: [comment_view].concat(comments),
699 case UserOperation.SaveComment: {
700 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
702 saveCommentRes(comment_view, this.state.comments);
703 this.setState(this.state);
708 case UserOperation.CreateCommentLike: {
709 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
711 createCommentLikeRes(comment_view, this.state.comments);
712 this.setState(this.state);
717 case UserOperation.BlockPerson: {
718 const data = wsJsonToRes<BlockPersonResponse>(msg);
719 updatePersonBlock(data);
724 case UserOperation.CreatePostReport:
725 case UserOperation.CreateCommentReport: {
726 const data = wsJsonToRes<PostReportResponse>(msg);
729 toast(i18n.t("report_created"));
735 case UserOperation.PurgeCommunity: {
736 const { success } = wsJsonToRes<PurgeItemResponse>(msg);
739 toast(i18n.t("purge_success"));
740 this.context.router.history.push(`/`);
746 case UserOperation.BlockCommunity: {
747 const data = wsJsonToRes<BlockCommunityResponse>(msg);
749 res.community_view.blocked = data.blocked;
750 this.setState(this.state);
752 updateCommunityBlock(data);