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";
39 createPostLikeFindRes,
53 postToCommentSortType,
55 restoreScrollPosition,
67 import { CommentNodes } from "../comment/comment-nodes";
68 import { BannerIconHeader } from "../common/banner-icon-header";
69 import { DataTypeSelect } from "../common/data-type-select";
70 import { HtmlTags } from "../common/html-tags";
71 import { Icon, Spinner } from "../common/icon";
72 import { Paginator } from "../common/paginator";
73 import { SortSelect } from "../common/sort-select";
74 import { Sidebar } from "../community/sidebar";
75 import { SiteSidebar } from "../home/site-sidebar";
76 import { PostListings } from "../post/post-listings";
77 import { CommunityLink } from "./community-link";
80 communityRes?: GetCommunityResponse;
81 communityLoading: boolean;
82 listingsLoading: boolean;
84 comments: CommentView[];
85 showSidebarMobile: boolean;
88 interface CommunityProps {
94 function getCommunityQueryParams() {
95 return getQueryParams<CommunityProps>({
96 dataType: getDataTypeFromQuery,
97 page: getPageFromString,
98 sort: getSortTypeFromQuery,
102 function getDataTypeFromQuery(type?: string): DataType {
103 return type ? DataType[type] : DataType.Post;
106 function getSortTypeFromQuery(type?: string): SortType {
108 UserService.Instance.myUserInfo?.local_user_view.local_user
111 return type ? (type as SortType) : mySortType ?? "Active";
114 export class Community extends Component<
115 RouteComponentProps<{ name: string }>,
118 private isoData = setIsoData(this.context);
119 private subscription?: Subscription;
121 communityLoading: true,
122 listingsLoading: true,
125 showSidebarMobile: false,
128 constructor(props: RouteComponentProps<{ name: string }>, context: any) {
129 super(props, context);
131 this.handleSortChange = this.handleSortChange.bind(this);
132 this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
133 this.handlePageChange = this.handlePageChange.bind(this);
135 this.parseMessage = this.parseMessage.bind(this);
136 this.subscription = wsSubscribe(this.parseMessage);
138 // Only fetch the data if coming from another route
139 if (this.isoData.path == this.context.router.route.match.url) {
142 communityRes: this.isoData.routeData[0] as GetCommunityResponse,
144 const postsRes = this.isoData.routeData[1] as
147 const commentsRes = this.isoData.routeData[2] as
148 | GetCommentsResponse
152 this.state = { ...this.state, posts: postsRes.posts };
156 this.state = { ...this.state, comments: commentsRes.comments };
161 communityLoading: false,
162 listingsLoading: false,
165 this.fetchCommunity();
171 const form: GetCommunity = {
172 name: this.props.match.params.name,
175 WebSocketService.Instance.send(wsClient.getCommunity(form));
178 componentDidMount() {
182 componentWillUnmount() {
183 saveScrollPosition(this.context);
184 this.subscription?.unsubscribe();
187 static fetchInitialData({
190 query: { dataType: urlDataType, page: urlPage, sort: urlSort },
192 }: InitialFetchRequest<QueryParams<CommunityProps>>): Promise<any>[] {
193 const pathSplit = path.split("/");
194 const promises: Promise<any>[] = [];
196 const communityName = pathSplit[2];
197 const communityForm: GetCommunity = {
201 promises.push(client.getCommunity(communityForm));
203 const dataType = getDataTypeFromQuery(urlDataType);
205 const sort = getSortTypeFromQuery(urlSort);
207 const page = getPageFromString(urlPage);
209 if (dataType === DataType.Post) {
210 const getPostsForm: GetPosts = {
211 community_name: communityName,
219 promises.push(client.getPosts(getPostsForm));
220 promises.push(Promise.resolve());
222 const getCommentsForm: GetComments = {
223 community_name: communityName,
226 sort: postToCommentSortType(sort),
231 promises.push(Promise.resolve());
232 promises.push(client.getComments(getCommentsForm));
238 get documentTitle(): string {
239 const cRes = this.state.communityRes;
241 ? `${cRes.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}`
246 const res = this.state.communityRes;
247 const { page } = getCommunityQueryParams();
250 <div className="container-lg">
251 {this.state.communityLoading ? (
259 title={this.documentTitle}
260 path={this.context.router.route.match.url}
261 description={res.community_view.community.description}
262 image={res.community_view.community.icon}
265 <div className="row">
266 <div className="col-12 col-md-8">
268 <div className="d-block d-md-none">
270 className="btn btn-secondary d-inline-block mb-2 mr-3"
271 onClick={linkEvent(this, this.handleShowSidebarMobile)}
273 {i18n.t("sidebar")}{" "}
276 this.state.showSidebarMobile
280 classes="icon-inline"
283 {this.state.showSidebarMobile && this.sidebar(res)}
287 <Paginator page={page} onChange={this.handlePageChange} />
289 <div className="d-none d-md-block col-md-4">
304 discussion_languages,
306 }: GetCommunityResponse) {
307 const { site_res } = this.isoData;
308 // For some reason, this returns an empty vec if it matches the site langs
309 const communityLangs =
310 discussion_languages.length === 0
311 ? site_res.all_languages.map(({ id }) => id)
312 : discussion_languages;
317 community_view={community_view}
318 moderators={moderators}
319 admins={site_res.admins}
321 enableNsfw={enableNsfw(site_res)}
323 allLanguages={site_res.all_languages}
324 siteLanguages={site_res.discussion_languages}
325 communityLanguages={communityLangs}
327 {!community_view.community.local && site && (
328 <SiteSidebar site={site} showLocal={showLocal(this.isoData)} />
335 const { dataType } = getCommunityQueryParams();
336 const { site_res } = this.isoData;
337 const { listingsLoading, posts, comments, communityRes } = this.state;
339 if (listingsLoading) {
345 } else if (dataType === DataType.Post) {
350 enableDownvotes={enableDownvotes(site_res)}
351 enableNsfw={enableNsfw(site_res)}
352 allLanguages={site_res.all_languages}
353 siteLanguages={site_res.discussion_languages}
359 nodes={commentsToFlatNodes(comments)}
360 viewType={CommentViewType.Flat}
363 enableDownvotes={enableDownvotes(site_res)}
364 moderators={communityRes?.moderators}
365 admins={site_res.admins}
366 allLanguages={site_res.all_languages}
367 siteLanguages={site_res.discussion_languages}
373 get communityInfo() {
374 const community = this.state.communityRes?.community_view.community;
378 <div className="mb-2">
379 <BannerIconHeader banner={community.banner} icon={community.icon} />
380 <h5 className="mb-0 overflow-wrap-anywhere">{community.title}</h5>
382 community={community}
394 // let communityRss = this.state.communityRes.map(r =>
395 // communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
397 const { dataType, sort } = getCommunityQueryParams();
398 const res = this.state.communityRes;
399 const communityRss = res
400 ? communityRSSUrl(res.community_view.community.actor_id, sort)
404 <div className="mb-3">
405 <span className="mr-3">
408 onChange={this.handleDataTypeChange}
411 <span className="mr-2">
412 <SortSelect sort={sort} onChange={this.handleSortChange} />
416 <a href={communityRss} title="RSS" rel={relTags}>
417 <Icon icon="rss" classes="text-muted small" />
421 type="application/atom+xml"
430 handlePageChange(page: number) {
431 this.updateUrl({ page });
432 window.scrollTo(0, 0);
435 handleSortChange(sort: SortType) {
436 this.updateUrl({ sort, page: 1 });
437 window.scrollTo(0, 0);
440 handleDataTypeChange(dataType: DataType) {
441 this.updateUrl({ dataType, page: 1 });
442 window.scrollTo(0, 0);
445 handleShowSidebarMobile(i: Community) {
446 i.setState(({ showSidebarMobile }) => ({
447 showSidebarMobile: !showSidebarMobile,
451 updateUrl({ dataType, page, sort }: Partial<CommunityProps>) {
453 dataType: urlDataType,
456 } = getCommunityQueryParams();
458 const queryParams: QueryParams<CommunityProps> = {
459 dataType: getDataTypeString(dataType ?? urlDataType),
460 page: (page ?? urlPage).toString(),
461 sort: sort ?? urlSort,
464 this.props.history.push(
465 `/c/${this.props.match.params.name}${getQueryString(queryParams)}`
471 listingsLoading: true,
478 const { dataType, page, sort } = getCommunityQueryParams();
479 const { name } = this.props.match.params;
482 if (dataType === DataType.Post) {
483 const form: GetPosts = {
488 community_name: name,
492 req = wsClient.getPosts(form);
494 const form: GetComments = {
497 sort: postToCommentSortType(sort),
499 community_name: name,
504 req = wsClient.getComments(form);
507 WebSocketService.Instance.send(req);
510 parseMessage(msg: any) {
511 const { page } = getCommunityQueryParams();
512 const op = wsUserOp(msg);
514 const res = this.state.communityRes;
517 toast(i18n.t(msg.error), "danger");
518 this.context.router.history.push("/");
519 } else if (msg.reconnect) {
521 WebSocketService.Instance.send(
522 wsClient.communityJoin({
523 community_id: res.community_view.community.id,
531 case UserOperation.GetCommunity: {
532 const data = wsJsonToRes<GetCommunityResponse>(msg);
534 this.setState({ communityRes: data, communityLoading: false });
535 // TODO why is there no auth in this form?
536 WebSocketService.Instance.send(
537 wsClient.communityJoin({
538 community_id: data.community_view.community.id,
545 case UserOperation.EditCommunity:
546 case UserOperation.DeleteCommunity:
547 case UserOperation.RemoveCommunity: {
548 const { community_view, discussion_languages } =
549 wsJsonToRes<CommunityResponse>(msg);
552 res.community_view = community_view;
553 res.discussion_languages = discussion_languages;
554 this.setState(this.state);
560 case UserOperation.FollowCommunity: {
564 counts: { subscribers },
566 } = wsJsonToRes<CommunityResponse>(msg);
569 res.community_view.subscribed = subscribed;
570 res.community_view.counts.subscribers = subscribers;
571 this.setState(this.state);
577 case UserOperation.GetPosts: {
578 const { posts } = wsJsonToRes<GetPostsResponse>(msg);
580 this.setState({ posts, listingsLoading: false });
581 restoreScrollPosition(this.context);
587 case UserOperation.EditPost:
588 case UserOperation.DeletePost:
589 case UserOperation.RemovePost:
590 case UserOperation.LockPost:
591 case UserOperation.FeaturePost:
592 case UserOperation.SavePost: {
593 const { post_view } = wsJsonToRes<PostResponse>(msg);
595 editPostFindRes(post_view, this.state.posts);
596 this.setState(this.state);
601 case UserOperation.CreatePost: {
602 const { post_view } = wsJsonToRes<PostResponse>(msg);
604 const showPostNotifs =
605 UserService.Instance.myUserInfo?.local_user_view.local_user
606 .show_new_post_notifs;
608 // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
609 if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
610 this.state.posts.unshift(post_view);
611 if (showPostNotifs) {
612 notifyPost(post_view, this.context.router);
614 this.setState(this.state);
620 case UserOperation.CreatePostLike: {
621 const { post_view } = wsJsonToRes<PostResponse>(msg);
623 createPostLikeFindRes(post_view, this.state.posts);
624 this.setState(this.state);
629 case UserOperation.AddModToCommunity: {
630 const { moderators } = wsJsonToRes<AddModToCommunityResponse>(msg);
633 res.moderators = moderators;
634 this.setState(this.state);
640 case UserOperation.BanFromCommunity: {
643 person: { id: personId },
646 } = wsJsonToRes<BanFromCommunityResponse>(msg);
648 // TODO this might be incorrect
650 .filter(p => p.creator.id === personId)
651 .forEach(p => (p.creator_banned_from_community = banned));
653 this.setState(this.state);
658 case UserOperation.GetComments: {
659 const { comments } = wsJsonToRes<GetCommentsResponse>(msg);
660 this.setState({ comments, listingsLoading: false });
665 case UserOperation.EditComment:
666 case UserOperation.DeleteComment:
667 case UserOperation.RemoveComment: {
668 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
669 editCommentRes(comment_view, this.state.comments);
670 this.setState(this.state);
675 case UserOperation.CreateComment: {
676 const { form_id, comment_view } = wsJsonToRes<CommentResponse>(msg);
678 // Necessary since it might be a user reply
680 this.setState(({ comments }) => ({
681 comments: [comment_view].concat(comments),
688 case UserOperation.SaveComment: {
689 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
691 saveCommentRes(comment_view, this.state.comments);
692 this.setState(this.state);
697 case UserOperation.CreateCommentLike: {
698 const { comment_view } = wsJsonToRes<CommentResponse>(msg);
700 createCommentLikeRes(comment_view, this.state.comments);
701 this.setState(this.state);
706 case UserOperation.BlockPerson: {
707 const data = wsJsonToRes<BlockPersonResponse>(msg);
708 updatePersonBlock(data);
713 case UserOperation.CreatePostReport:
714 case UserOperation.CreateCommentReport: {
715 const data = wsJsonToRes<PostReportResponse>(msg);
718 toast(i18n.t("report_created"));
724 case UserOperation.PurgeCommunity: {
725 const { success } = wsJsonToRes<PurgeItemResponse>(msg);
728 toast(i18n.t("purge_success"));
729 this.context.router.history.push(`/`);
735 case UserOperation.BlockCommunity: {
736 const data = wsJsonToRes<BlockCommunityResponse>(msg);
738 res.community_view.blocked = data.blocked;
739 this.setState(this.state);
741 updateCommunityBlock(data);