import { Component, linkEvent } from "inferno"; import { RouteComponentProps } from "inferno-router/dist/Route"; import { AddAdmin, AddModToCommunity, AddModToCommunityResponse, BanFromCommunity, BanFromCommunityResponse, BanPerson, BanPersonResponse, BlockCommunity, BlockPerson, CommentId, CommentReplyResponse, CommentResponse, CommunityResponse, CreateComment, CreateCommentLike, CreateCommentReport, CreatePostLike, CreatePostReport, DeleteComment, DeleteCommunity, DeletePost, DistinguishComment, EditComment, EditCommunity, EditPost, FeaturePost, FollowCommunity, GetComments, GetCommentsResponse, GetCommunity, GetCommunityResponse, GetPosts, GetPostsResponse, GetSiteResponse, LockPost, MarkCommentReplyAsRead, MarkPersonMentionAsRead, PostResponse, PurgeComment, PurgeCommunity, PurgeItemResponse, PurgePerson, PurgePost, RemoveComment, RemoveCommunity, RemovePost, SaveComment, SavePost, SortType, TransferCommunity, } from "lemmy-js-client"; import { i18n } from "../../i18next"; import { CommentViewType, DataType, InitialFetchRequest, } from "../../interfaces"; import { UserService } from "../../services"; import { FirstLoadService } from "../../services/FirstLoadService"; import { HttpService, RequestState } from "../../services/HttpService"; import { QueryParams, commentsToFlatNodes, communityRSSUrl, editComment, editPost, editWith, enableDownvotes, enableNsfw, fetchLimit, getCommentParentId, getDataTypeString, getPageFromString, getQueryParams, getQueryString, myAuth, postToCommentSortType, relTags, restoreScrollPosition, saveScrollPosition, setIsoData, setupTippy, showLocal, toast, updateCommunityBlock, updatePersonBlock, } from "../../utils"; import { CommentNodes } from "../comment/comment-nodes"; import { BannerIconHeader } from "../common/banner-icon-header"; import { DataTypeSelect } from "../common/data-type-select"; import { HtmlTags } from "../common/html-tags"; import { Icon, Spinner } from "../common/icon"; import { Paginator } from "../common/paginator"; import { SortSelect } from "../common/sort-select"; import { Sidebar } from "../community/sidebar"; import { SiteSidebar } from "../home/site-sidebar"; import { PostListings } from "../post/post-listings"; import { CommunityLink } from "./community-link"; interface State { communityRes: RequestState; postsRes: RequestState; commentsRes: RequestState; siteRes: GetSiteResponse; showSidebarMobile: boolean; finished: Map; isIsomorphic: boolean; } interface CommunityProps { dataType: DataType; sort: SortType; page: number; } function getCommunityQueryParams() { return getQueryParams({ dataType: getDataTypeFromQuery, page: getPageFromString, sort: getSortTypeFromQuery, }); } function getDataTypeFromQuery(type?: string): DataType { return type ? DataType[type] : DataType.Post; } function getSortTypeFromQuery(type?: string): SortType { const mySortType = UserService.Instance.myUserInfo?.local_user_view.local_user .default_sort_type; return type ? (type as SortType) : mySortType ?? "Active"; } export class Community extends Component< RouteComponentProps<{ name: string }>, State > { private isoData = setIsoData(this.context); state: State = { communityRes: { state: "empty" }, postsRes: { state: "empty" }, commentsRes: { state: "empty" }, siteRes: this.isoData.site_res, showSidebarMobile: false, finished: new Map(), isIsomorphic: false, }; constructor(props: RouteComponentProps<{ name: string }>, context: any) { super(props, context); this.handleSortChange = this.handleSortChange.bind(this); this.handleDataTypeChange = this.handleDataTypeChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this); // All of the action binds this.handleDeleteCommunity = this.handleDeleteCommunity.bind(this); this.handleEditCommunity = this.handleEditCommunity.bind(this); this.handleFollow = this.handleFollow.bind(this); this.handleRemoveCommunity = this.handleRemoveCommunity.bind(this); this.handleCreateComment = this.handleCreateComment.bind(this); this.handleEditComment = this.handleEditComment.bind(this); this.handleSaveComment = this.handleSaveComment.bind(this); this.handleBlockCommunity = this.handleBlockCommunity.bind(this); this.handleBlockPerson = this.handleBlockPerson.bind(this); this.handleDeleteComment = this.handleDeleteComment.bind(this); this.handleRemoveComment = this.handleRemoveComment.bind(this); this.handleCommentVote = this.handleCommentVote.bind(this); this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this); this.handleAddAdmin = this.handleAddAdmin.bind(this); this.handlePurgePerson = this.handlePurgePerson.bind(this); this.handlePurgeComment = this.handlePurgeComment.bind(this); this.handleCommentReport = this.handleCommentReport.bind(this); this.handleDistinguishComment = this.handleDistinguishComment.bind(this); this.handleTransferCommunity = this.handleTransferCommunity.bind(this); this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this); this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this); this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this); this.handleBanPerson = this.handleBanPerson.bind(this); this.handlePostVote = this.handlePostVote.bind(this); this.handlePostEdit = this.handlePostEdit.bind(this); this.handlePostReport = this.handlePostReport.bind(this); this.handleLockPost = this.handleLockPost.bind(this); this.handleDeletePost = this.handleDeletePost.bind(this); this.handleRemovePost = this.handleRemovePost.bind(this); this.handleSavePost = this.handleSavePost.bind(this); this.handlePurgePost = this.handlePurgePost.bind(this); this.handleFeaturePost = this.handleFeaturePost.bind(this); // Only fetch the data if coming from another route if (FirstLoadService.isFirstLoad) { const [communityRes, postsRes, commentsRes] = this.isoData.routeData; this.state = { ...this.state, communityRes, postsRes, commentsRes, isIsomorphic: true, }; } } async fetchCommunity() { this.setState({ communityRes: { state: "loading" } }); this.setState({ communityRes: await HttpService.client.getCommunity({ name: this.props.match.params.name, auth: myAuth(), }), }); } async componentDidMount() { if (!this.state.isIsomorphic) { await Promise.all([this.fetchCommunity(), this.fetchData()]); } setupTippy(); } componentWillUnmount() { saveScrollPosition(this.context); } static fetchInitialData({ client, path, query: { dataType: urlDataType, page: urlPage, sort: urlSort }, auth, }: InitialFetchRequest>): Promise< RequestState >[] { const pathSplit = path.split("/"); const promises: Promise>[] = []; const communityName = pathSplit[2]; const communityForm: GetCommunity = { name: communityName, auth, }; promises.push(client.getCommunity(communityForm)); const dataType = getDataTypeFromQuery(urlDataType); const sort = getSortTypeFromQuery(urlSort); const page = getPageFromString(urlPage); if (dataType === DataType.Post) { const getPostsForm: GetPosts = { community_name: communityName, page, limit: fetchLimit, sort, type_: "All", saved_only: false, auth, }; promises.push(client.getPosts(getPostsForm)); promises.push(Promise.resolve({ state: "empty" })); } else { const getCommentsForm: GetComments = { community_name: communityName, page, limit: fetchLimit, sort: postToCommentSortType(sort), type_: "All", saved_only: false, auth, }; promises.push(Promise.resolve({ state: "empty" })); promises.push(client.getComments(getCommentsForm)); } return promises; } get documentTitle(): string { const cRes = this.state.communityRes; return cRes.state == "success" ? `${cRes.data.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}` : ""; } renderCommunity() { switch (this.state.communityRes.state) { case "loading": return (
); case "success": { const res = this.state.communityRes.data; const { page } = getCommunityQueryParams(); return ( <>
{this.communityInfo(res)}
{this.state.showSidebarMobile && this.sidebar(res)}
{this.selects(res)} {this.listings(res)}
{this.sidebar(res)}
); } } } render() { return
{this.renderCommunity()}
; } sidebar(res: GetCommunityResponse) { const { site_res } = this.isoData; // For some reason, this returns an empty vec if it matches the site langs const communityLangs = res.discussion_languages.length === 0 ? site_res.all_languages.map(({ id }) => id) : res.discussion_languages; return ( <> {!res.community_view.community.local && res.site && ( )} ); } listings(communityRes: GetCommunityResponse) { const { dataType } = getCommunityQueryParams(); const { site_res } = this.isoData; if (dataType === DataType.Post) { switch (this.state.postsRes.state) { case "loading": return (
); case "success": return ( ); } } else { switch (this.state.commentsRes.state) { case "loading": return (
); case "success": return ( ); } } } communityInfo(res: GetCommunityResponse) { const community = res.community_view.community; return ( community && (
{community.title}
) ); } selects(res: GetCommunityResponse) { // let communityRss = this.state.communityRes.map(r => // communityRSSUrl(r.community_view.community.actor_id, this.state.sort) // ); const { dataType, sort } = getCommunityQueryParams(); const communityRss = res ? communityRSSUrl(res.community_view.community.actor_id, sort) : undefined; return (
{communityRss && ( <> )}
); } handlePageChange(page: number) { this.updateUrl({ page }); window.scrollTo(0, 0); } handleSortChange(sort: SortType) { this.updateUrl({ sort, page: 1 }); window.scrollTo(0, 0); } handleDataTypeChange(dataType: DataType) { this.updateUrl({ dataType, page: 1 }); window.scrollTo(0, 0); } handleShowSidebarMobile(i: Community) { i.setState(({ showSidebarMobile }) => ({ showSidebarMobile: !showSidebarMobile, })); } async updateUrl({ dataType, page, sort }: Partial) { const { dataType: urlDataType, page: urlPage, sort: urlSort, } = getCommunityQueryParams(); const queryParams: QueryParams = { dataType: getDataTypeString(dataType ?? urlDataType), page: (page ?? urlPage).toString(), sort: sort ?? urlSort, }; this.props.history.push( `/c/${this.props.match.params.name}${getQueryString(queryParams)}` ); await this.fetchData(); } async fetchData() { const { dataType, page, sort } = getCommunityQueryParams(); const { name } = this.props.match.params; if (dataType === DataType.Post) { this.setState({ postsRes: { state: "loading" } }); this.setState({ postsRes: await HttpService.client.getPosts({ page, limit: fetchLimit, sort, type_: "All", community_name: name, saved_only: false, auth: myAuth(), }), }); } else { this.setState({ commentsRes: { state: "loading" } }); this.setState({ commentsRes: await HttpService.client.getComments({ page, limit: fetchLimit, sort: postToCommentSortType(sort), type_: "All", community_name: name, saved_only: false, auth: myAuth(), }), }); } restoreScrollPosition(this.context); setupTippy(); } async handleDeleteCommunity(form: DeleteCommunity) { const deleteCommunityRes = await HttpService.client.deleteCommunity(form); this.updateCommunity(deleteCommunityRes); } async handleAddModToCommunity(form: AddModToCommunity) { const addModRes = await HttpService.client.addModToCommunity(form); this.updateModerators(addModRes); } async handleFollow(form: FollowCommunity) { const followCommunityRes = await HttpService.client.followCommunity(form); this.updateCommunity(followCommunityRes); // Update myUserInfo if (followCommunityRes.state == "success") { const communityId = followCommunityRes.data.community_view.community.id; const mui = UserService.Instance.myUserInfo; if (mui) { mui.follows = mui.follows.filter(i => i.community.id != communityId); } } } async handlePurgeCommunity(form: PurgeCommunity) { const purgeCommunityRes = await HttpService.client.purgeCommunity(form); this.purgeItem(purgeCommunityRes); } async handlePurgePerson(form: PurgePerson) { const purgePersonRes = await HttpService.client.purgePerson(form); this.purgeItem(purgePersonRes); } async handlePurgeComment(form: PurgeComment) { const purgeCommentRes = await HttpService.client.purgeComment(form); this.purgeItem(purgeCommentRes); } async handlePurgePost(form: PurgePost) { const purgeRes = await HttpService.client.purgePost(form); this.purgeItem(purgeRes); } async handleBlockCommunity(form: BlockCommunity) { const blockCommunityRes = await HttpService.client.blockCommunity(form); if (blockCommunityRes.state == "success") { updateCommunityBlock(blockCommunityRes.data); this.setState(s => { if (s.communityRes.state == "success") { s.communityRes.data.community_view.blocked = blockCommunityRes.data.blocked; } }); } } async handleBlockPerson(form: BlockPerson) { const blockPersonRes = await HttpService.client.blockPerson(form); if (blockPersonRes.state == "success") { updatePersonBlock(blockPersonRes.data); } } async handleRemoveCommunity(form: RemoveCommunity) { const removeCommunityRes = await HttpService.client.removeCommunity(form); this.updateCommunity(removeCommunityRes); } async handleEditCommunity(form: EditCommunity) { const res = await HttpService.client.editCommunity(form); this.updateCommunity(res); return res; } async handleCreateComment(form: CreateComment) { const createCommentRes = await HttpService.client.createComment(form); this.createAndUpdateComments(createCommentRes); return createCommentRes; } async handleEditComment(form: EditComment) { const editCommentRes = await HttpService.client.editComment(form); this.findAndUpdateComment(editCommentRes); return editCommentRes; } async handleDeleteComment(form: DeleteComment) { const deleteCommentRes = await HttpService.client.deleteComment(form); this.findAndUpdateComment(deleteCommentRes); } async handleDeletePost(form: DeletePost) { const deleteRes = await HttpService.client.deletePost(form); this.findAndUpdatePost(deleteRes); } async handleRemovePost(form: RemovePost) { const removeRes = await HttpService.client.removePost(form); this.findAndUpdatePost(removeRes); } async handleRemoveComment(form: RemoveComment) { const removeCommentRes = await HttpService.client.removeComment(form); this.findAndUpdateComment(removeCommentRes); } async handleSaveComment(form: SaveComment) { const saveCommentRes = await HttpService.client.saveComment(form); this.findAndUpdateComment(saveCommentRes); } async handleSavePost(form: SavePost) { const saveRes = await HttpService.client.savePost(form); this.findAndUpdatePost(saveRes); } async handleFeaturePost(form: FeaturePost) { const featureRes = await HttpService.client.featurePost(form); this.findAndUpdatePost(featureRes); } async handleCommentVote(form: CreateCommentLike) { const voteRes = await HttpService.client.likeComment(form); this.findAndUpdateComment(voteRes); } async handlePostEdit(form: EditPost) { const res = await HttpService.client.editPost(form); this.findAndUpdatePost(res); } async handlePostVote(form: CreatePostLike) { const voteRes = await HttpService.client.likePost(form); this.findAndUpdatePost(voteRes); } async handleCommentReport(form: CreateCommentReport) { const reportRes = await HttpService.client.createCommentReport(form); if (reportRes.state == "success") { toast(i18n.t("report_created")); } } async handlePostReport(form: CreatePostReport) { const reportRes = await HttpService.client.createPostReport(form); if (reportRes.state == "success") { toast(i18n.t("report_created")); } } async handleLockPost(form: LockPost) { const lockRes = await HttpService.client.lockPost(form); this.findAndUpdatePost(lockRes); } async handleDistinguishComment(form: DistinguishComment) { const distinguishRes = await HttpService.client.distinguishComment(form); this.findAndUpdateComment(distinguishRes); } async handleAddAdmin(form: AddAdmin) { const addAdminRes = await HttpService.client.addAdmin(form); if (addAdminRes.state == "success") { this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s)); } } async handleTransferCommunity(form: TransferCommunity) { const transferCommunityRes = await HttpService.client.transferCommunity( form ); toast(i18n.t("transfer_community")); this.updateCommunityFull(transferCommunityRes); } async handleCommentReplyRead(form: MarkCommentReplyAsRead) { const readRes = await HttpService.client.markCommentReplyAsRead(form); this.findAndUpdateCommentReply(readRes); } async handlePersonMentionRead(form: MarkPersonMentionAsRead) { // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it. await HttpService.client.markPersonMentionAsRead(form); } async handleBanFromCommunity(form: BanFromCommunity) { const banRes = await HttpService.client.banFromCommunity(form); this.updateBanFromCommunity(banRes); } async handleBanPerson(form: BanPerson) { const banRes = await HttpService.client.banPerson(form); this.updateBan(banRes); } updateBanFromCommunity(banRes: RequestState) { // Maybe not necessary if (banRes.state == "success") { this.setState(s => { if (s.postsRes.state == "success") { s.postsRes.data.posts .filter(c => c.creator.id == banRes.data.person_view.person.id) .forEach( c => (c.creator_banned_from_community = banRes.data.banned) ); } if (s.commentsRes.state == "success") { s.commentsRes.data.comments .filter(c => c.creator.id == banRes.data.person_view.person.id) .forEach( c => (c.creator_banned_from_community = banRes.data.banned) ); } return s; }); } } updateBan(banRes: RequestState) { // Maybe not necessary if (banRes.state == "success") { this.setState(s => { if (s.postsRes.state == "success") { s.postsRes.data.posts .filter(c => c.creator.id == banRes.data.person_view.person.id) .forEach(c => (c.creator.banned = banRes.data.banned)); } if (s.commentsRes.state == "success") { s.commentsRes.data.comments .filter(c => c.creator.id == banRes.data.person_view.person.id) .forEach(c => (c.creator.banned = banRes.data.banned)); } return s; }); } } updateCommunity(res: RequestState) { this.setState(s => { if (s.communityRes.state == "success" && res.state == "success") { s.communityRes.data.community_view = res.data.community_view; s.communityRes.data.discussion_languages = res.data.discussion_languages; } return s; }); } updateCommunityFull(res: RequestState) { this.setState(s => { if (s.communityRes.state == "success" && res.state == "success") { s.communityRes.data.community_view = res.data.community_view; s.communityRes.data.moderators = res.data.moderators; } return s; }); } purgeItem(purgeRes: RequestState) { if (purgeRes.state == "success") { toast(i18n.t("purge_success")); this.context.router.history.push(`/`); } } findAndUpdateComment(res: RequestState) { this.setState(s => { if (s.commentsRes.state == "success" && res.state == "success") { s.commentsRes.data.comments = editComment( res.data.comment_view, s.commentsRes.data.comments ); s.finished.set(res.data.comment_view.comment.id, true); } return s; }); } createAndUpdateComments(res: RequestState) { this.setState(s => { if (s.commentsRes.state == "success" && res.state == "success") { s.commentsRes.data.comments.unshift(res.data.comment_view); // Set finished for the parent s.finished.set( getCommentParentId(res.data.comment_view.comment) ?? 0, true ); } return s; }); } findAndUpdateCommentReply(res: RequestState) { this.setState(s => { if (s.commentsRes.state == "success" && res.state == "success") { s.commentsRes.data.comments = editWith( res.data.comment_reply_view, s.commentsRes.data.comments ); } return s; }); } findAndUpdatePost(res: RequestState) { this.setState(s => { if (s.postsRes.state == "success" && res.state == "success") { s.postsRes.data.posts = editPost( res.data.post_view, s.postsRes.data.posts ); } return s; }); } updateModerators(res: RequestState) { // Update the moderators this.setState(s => { if (s.communityRes.state == "success" && res.state == "success") { s.communityRes.data.moderators = res.data.moderators; } return s; }); } }