From 2274f51e8d5148e7591bd6190942c5b98044c4b5 Mon Sep 17 00:00:00 2001 From: Dessalines <dessalines@users.noreply.github.com> Date: Thu, 28 Apr 2022 16:42:15 -0400 Subject: [PATCH] Adding site sidebar for remote communities. Fixes #626 (#640) --- package.json | 2 +- src/shared/components/community/community.tsx | 25 +- src/shared/components/home/home.tsx | 259 ++--------------- src/shared/components/home/site-form.tsx | 2 + src/shared/components/home/site-sidebar.tsx | 267 ++++++++++++++++++ yarn.lock | 8 +- 6 files changed, 310 insertions(+), 253 deletions(-) create mode 100644 src/shared/components/home/site-sidebar.tsx diff --git a/package.json b/package.json index f43e5f6..729b6b1 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "eslint-plugin-prettier": "^4.0.0", "husky": "^7.0.4", "import-sort-style-module": "^6.0.0", - "lemmy-js-client": "0.16.0-rc.1", + "lemmy-js-client": "0.16.4-rc.1", "lint-staged": "^12.4.1", "mini-css-extract-plugin": "^2.6.0", "node-fetch": "^2.6.1", diff --git a/src/shared/components/community/community.tsx b/src/shared/components/community/community.tsx index ab92308..dc26d94 100644 --- a/src/shared/components/community/community.tsx +++ b/src/shared/components/community/community.tsx @@ -60,6 +60,7 @@ 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"; @@ -268,13 +269,20 @@ export class Community extends Component<any, State> { /> </button> {this.state.showSidebarMobile && ( - <Sidebar - community_view={cv} - moderators={this.state.communityRes.moderators} - admins={this.state.siteRes.admins} - online={this.state.communityRes.online} - enableNsfw={this.state.siteRes.site_view.site.enable_nsfw} - /> + <> + <Sidebar + community_view={cv} + moderators={this.state.communityRes.moderators} + admins={this.state.siteRes.admins} + online={this.state.communityRes.online} + enableNsfw={ + this.state.siteRes.site_view.site.enable_nsfw + } + /> + {!cv.community.local && this.state.communityRes.site && ( + <SiteSidebar site={this.state.communityRes.site} /> + )} + </> )} </div> {this.selects()} @@ -292,6 +300,9 @@ export class Community extends Component<any, State> { online={this.state.communityRes.online} enableNsfw={this.state.siteRes.site_view.site.enable_nsfw} /> + {!cv.community.local && this.state.communityRes.site && ( + <SiteSidebar site={this.state.communityRes.site} /> + )} </div> </div> </> diff --git a/src/shared/components/home/home.tsx b/src/shared/components/home/home.tsx index 3f6282a..0010cd9 100644 --- a/src/shared/components/home/home.tsx +++ b/src/shared/components/home/home.tsx @@ -40,9 +40,7 @@ import { getListingTypeFromProps, getPageFromProps, getSortTypeFromProps, - mdToHtml, notifyPost, - numToSI, relTags, restoreScrollPosition, saveCommentRes, @@ -59,7 +57,6 @@ import { wsUserOp, } 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"; @@ -67,18 +64,15 @@ import { ListingTypeSelect } from "../common/listing-type-select"; import { Paginator } from "../common/paginator"; import { SortSelect } from "../common/sort-select"; import { CommunityLink } from "../community/community-link"; -import { PersonListing } from "../person/person-listing"; import { PostListings } from "../post/post-listings"; -import { SiteForm } from "./site-form"; +import { SiteSidebar } from "./site-sidebar"; interface HomeState { trendingCommunities: CommunityView[]; siteRes: GetSiteResponse; - showEditSite: boolean; showSubscribedMobile: boolean; showTrendingMobile: boolean; showSidebarMobile: boolean; - sidebarCollapsed: boolean; subscribedCollapsed: boolean; loading: boolean; posts: PostView[]; @@ -109,12 +103,10 @@ export class Home extends Component<any, HomeState> { private emptyState: HomeState = { trendingCommunities: [], siteRes: this.isoData.site_res, - showEditSite: false, showSubscribedMobile: false, showTrendingMobile: false, showSidebarMobile: false, subscribedCollapsed: false, - sidebarCollapsed: false, loading: true, posts: [], comments: [], @@ -128,7 +120,6 @@ export class Home extends Component<any, HomeState> { super(props, context); this.state = this.emptyState; - this.handleEditCancel = this.handleEditCancel.bind(this); this.handleSortChange = this.handleSortChange.bind(this); this.handleListingTypeChange = this.handleListingTypeChange.bind(this); this.handleDataTypeChange = this.handleDataTypeChange.bind(this); @@ -293,6 +284,7 @@ export class Home extends Component<any, HomeState> { } mobileView() { + let siteRes = this.state.siteRes; return ( <div class="row"> <div class="col-12"> @@ -337,19 +329,22 @@ export class Home extends Component<any, HomeState> { classes="icon-inline" /> </button> - {this.state.showSubscribedMobile && ( - <div class="col-12 card border-secondary mb-3"> - <div class="card-body">{this.subscribedCommunities()}</div> - </div> + {this.state.showSidebarMobile && ( + <SiteSidebar + site={siteRes.site_view.site} + admins={siteRes.admins} + counts={siteRes.site_view.counts} + online={siteRes.online} + /> )} {this.state.showTrendingMobile && ( <div class="col-12 card border-secondary mb-3"> <div class="card-body">{this.trendingCommunities()}</div> </div> )} - {this.state.showSidebarMobile && ( + {this.state.showSubscribedMobile && ( <div class="col-12 card border-secondary mb-3"> - <div class="card-body">{this.sidebar()}</div> + <div class="card-body">{this.subscribedCommunities()}</div> </div> )} </div> @@ -358,6 +353,7 @@ export class Home extends Component<any, HomeState> { } mySidebar() { + let siteRes = this.state.siteRes; return ( <div> {!this.state.loading && ( @@ -370,9 +366,12 @@ export class Home extends Component<any, HomeState> { </div> </div> - <div class="card border-secondary mb-3"> - <div class="card-body">{this.sidebar()}</div> - </div> + <SiteSidebar + site={siteRes.site_view.site} + admins={siteRes.admins} + counts={siteRes.site_view.counts} + online={siteRes.online} + /> {UserService.Instance.myUserInfo && UserService.Instance.myUserInfo.follows.length > 0 && ( @@ -460,30 +459,6 @@ export class Home extends Component<any, HomeState> { ); } - sidebar() { - let site = this.state.siteRes.site_view.site; - return ( - <div> - {!this.state.showEditSite ? ( - <div> - <div class="mb-2"> - {this.siteName()} - {this.adminButtons()} - </div> - {!this.state.sidebarCollapsed && ( - <> - <BannerIconHeader banner={site.banner} /> - {this.siteInfo()} - </> - )} - </div> - ) : ( - <SiteForm site={site} onCancel={this.handleEditCancel} /> - )} - </div> - ); - } - updateUrl(paramUpdates: UrlParams) { const listingTypeStr = paramUpdates.listingType || this.state.listingType; const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType]; @@ -494,179 +469,6 @@ export class Home extends Component<any, HomeState> { ); } - siteInfo() { - let site = this.state.siteRes.site_view.site; - return ( - <div> - {site.description && <h6>{site.description}</h6>} - {site.sidebar && this.siteSidebar()} - {this.badges()} - {this.admins()} - </div> - ); - } - - siteName() { - let site = this.state.siteRes.site_view.site; - return ( - site.name && ( - <h5 class="mb-0 d-inline"> - {site.name} - <button - class="btn btn-sm text-muted" - onClick={linkEvent(this, this.handleCollapseSidebar)} - aria-label={i18n.t("collapse")} - data-tippy-content={i18n.t("collapse")} - > - {this.state.sidebarCollapsed ? ( - <Icon icon="plus-square" classes="icon-inline" /> - ) : ( - <Icon icon="minus-square" classes="icon-inline" /> - )} - </button> - </h5> - ) - ); - } - - admins() { - return ( - <ul class="mt-1 list-inline small mb-0"> - <li class="list-inline-item">{i18n.t("admins")}:</li> - {this.state.siteRes.admins.map(av => ( - <li class="list-inline-item"> - <PersonListing person={av.person} /> - </li> - ))} - </ul> - ); - } - - badges() { - let counts = this.state.siteRes.site_view.counts; - return ( - <ul class="my-2 list-inline"> - <li className="list-inline-item badge badge-secondary"> - {i18n.t("number_online", { - count: this.state.siteRes.online, - formattedCount: numToSI(this.state.siteRes.online), - })} - </li> - <li - className="list-inline-item badge badge-secondary pointer" - data-tippy-content={i18n.t("active_users_in_the_last_day", { - count: counts.users_active_day, - formattedCount: numToSI(counts.users_active_day), - })} - > - {i18n.t("number_of_users", { - count: counts.users_active_day, - formattedCount: numToSI(counts.users_active_day), - })}{" "} - / {i18n.t("day")} - </li> - <li - className="list-inline-item badge badge-secondary pointer" - data-tippy-content={i18n.t("active_users_in_the_last_week", { - count: counts.users_active_week, - formattedCount: counts.users_active_week, - })} - > - {i18n.t("number_of_users", { - count: counts.users_active_week, - formattedCount: numToSI(counts.users_active_week), - })}{" "} - / {i18n.t("week")} - </li> - <li - className="list-inline-item badge badge-secondary pointer" - data-tippy-content={i18n.t("active_users_in_the_last_month", { - count: counts.users_active_month, - formattedCount: counts.users_active_month, - })} - > - {i18n.t("number_of_users", { - count: counts.users_active_month, - formattedCount: numToSI(counts.users_active_month), - })}{" "} - / {i18n.t("month")} - </li> - <li - className="list-inline-item badge badge-secondary pointer" - data-tippy-content={i18n.t("active_users_in_the_last_six_months", { - count: counts.users_active_half_year, - formattedCount: counts.users_active_half_year, - })} - > - {i18n.t("number_of_users", { - count: counts.users_active_half_year, - formattedCount: numToSI(counts.users_active_half_year), - })}{" "} - / {i18n.t("number_of_months", { count: 6, formattedCount: 6 })} - </li> - <li className="list-inline-item badge badge-secondary"> - {i18n.t("number_of_users", { - count: counts.users, - formattedCount: numToSI(counts.users), - })} - </li> - <li className="list-inline-item badge badge-secondary"> - {i18n.t("number_of_communities", { - count: counts.communities, - formattedCount: numToSI(counts.communities), - })} - </li> - <li className="list-inline-item badge badge-secondary"> - {i18n.t("number_of_posts", { - count: counts.posts, - formattedCount: numToSI(counts.posts), - })} - </li> - <li className="list-inline-item badge badge-secondary"> - {i18n.t("number_of_comments", { - count: counts.comments, - formattedCount: numToSI(counts.comments), - })} - </li> - <li className="list-inline-item"> - <Link className="badge badge-primary" to="/modlog"> - {i18n.t("modlog")} - </Link> - </li> - </ul> - ); - } - - adminButtons() { - return ( - this.canAdmin && ( - <ul class="list-inline mb-1 text-muted font-weight-bold"> - <li className="list-inline-item-action"> - <button - class="btn btn-link d-inline-block text-muted" - onClick={linkEvent(this, this.handleEditClick)} - aria-label={i18n.t("edit")} - data-tippy-content={i18n.t("edit")} - > - <Icon icon="edit" classes="icon-inline" /> - </button> - </li> - </ul> - ) - ); - } - - siteSidebar() { - return ( - <div - className="md-div" - dangerouslySetInnerHTML={mdToHtml( - this.state.siteRes.site_view.site.sidebar - )} - /> - ); - } - posts() { return ( <div class="main-content-wrapper"> @@ -767,25 +569,6 @@ export class Home extends Component<any, HomeState> { ); } - get canAdmin(): boolean { - return ( - UserService.Instance.myUserInfo && - this.state.siteRes.admins - .map(a => a.person.id) - .includes(UserService.Instance.myUserInfo.local_user_view.person.id) - ); - } - - handleEditClick(i: Home) { - i.state.showEditSite = true; - i.setState(i.state); - } - - handleEditCancel() { - this.state.showEditSite = false; - this.setState(this.state); - } - handleShowSubscribedMobile(i: Home) { i.state.showSubscribedMobile = !i.state.showSubscribedMobile; i.setState(i.state); @@ -806,11 +589,6 @@ export class Home extends Component<any, HomeState> { i.setState(i.state); } - handleCollapseSidebar(i: Home) { - i.state.sidebarCollapsed = !i.state.sidebarCollapsed; - i.setState(i.state); - } - handlePageChange(page: number) { this.updateUrl({ page }); window.scrollTo(0, 0); @@ -873,7 +651,6 @@ export class Home extends Component<any, HomeState> { } else if (op == UserOperation.EditSite) { let data = wsJsonToRes<SiteResponse>(msg).data; this.state.siteRes.site_view = data.site_view; - this.state.showEditSite = false; this.setState(this.state); toast(i18n.t("site_saved")); } else if (op == UserOperation.GetPosts) { diff --git a/src/shared/components/home/site-form.tsx b/src/shared/components/home/site-form.tsx index 20e1db0..50797e6 100644 --- a/src/shared/components/home/site-form.tsx +++ b/src/shared/components/home/site-form.tsx @@ -16,6 +16,7 @@ import { MarkdownTextArea } from "../common/markdown-textarea"; interface SiteFormProps { site?: Site; // If a site is given, that means this is an edit onCancel?(): any; + onEdit?(): any; } interface SiteFormState { @@ -404,6 +405,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> { i.state.loading = true; if (i.props.site) { WebSocketService.Instance.send(wsClient.editSite(i.state.siteForm)); + i.props.onEdit(); } else { let form: CreateSite = { name: i.state.siteForm.name || "My site", diff --git a/src/shared/components/home/site-sidebar.tsx b/src/shared/components/home/site-sidebar.tsx new file mode 100644 index 0000000..b5bf2bb --- /dev/null +++ b/src/shared/components/home/site-sidebar.tsx @@ -0,0 +1,267 @@ +import { Component, linkEvent } from "inferno"; +import { Link } from "inferno-router"; +import { PersonViewSafe, Site, SiteAggregates } from "lemmy-js-client"; +import { i18n } from "../../i18next"; +import { UserService } from "../../services"; +import { mdToHtml, numToSI } from "../../utils"; +import { BannerIconHeader } from "../common/banner-icon-header"; +import { Icon } from "../common/icon"; +import { PersonListing } from "../person/person-listing"; +import { SiteForm } from "./site-form"; + +interface SiteSidebarProps { + site: Site; + counts?: SiteAggregates; + admins?: PersonViewSafe[]; + online?: number; +} + +interface SiteSidebarState { + collapsed: boolean; + showEdit: boolean; +} + +export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> { + private emptyState: SiteSidebarState = { + collapsed: false, + showEdit: false, + }; + + constructor(props: any, context: any) { + super(props, context); + this.state = this.emptyState; + this.handleEditCancel = this.handleEditCancel.bind(this); + this.handleEditSite = this.handleEditSite.bind(this); + } + + render() { + let site = this.props.site; + return ( + <div class="card border-secondary mb-3"> + <div class="card-body"> + {!this.state.showEdit ? ( + <div> + <div class="mb-2"> + {this.siteName()} + {this.props.admins && this.adminButtons()} + </div> + {!this.state.collapsed && ( + <> + <BannerIconHeader banner={site.banner} /> + {this.siteInfo()} + </> + )} + </div> + ) : ( + <SiteForm + site={site} + onEdit={this.handleEditSite} + onCancel={this.handleEditCancel} + /> + )} + </div> + </div> + ); + } + + siteName() { + let site = this.props.site; + return ( + site.name && ( + <h5 class="mb-0 d-inline"> + {site.name} + <button + class="btn btn-sm text-muted" + onClick={linkEvent(this, this.handleCollapseSidebar)} + aria-label={i18n.t("collapse")} + data-tippy-content={i18n.t("collapse")} + > + {this.state.collapsed ? ( + <Icon icon="plus-square" classes="icon-inline" /> + ) : ( + <Icon icon="minus-square" classes="icon-inline" /> + )} + </button> + </h5> + ) + ); + } + + siteInfo() { + let site = this.props.site; + return ( + <div> + {site.description && <h6>{site.description}</h6>} + {site.sidebar && this.siteSidebar()} + {this.props.counts && this.badges()} + {this.props.admins && this.admins()} + </div> + ); + } + + adminButtons() { + return ( + this.canAdmin && ( + <ul class="list-inline mb-1 text-muted font-weight-bold"> + <li className="list-inline-item-action"> + <button + class="btn btn-link d-inline-block text-muted" + onClick={linkEvent(this, this.handleEditClick)} + aria-label={i18n.t("edit")} + data-tippy-content={i18n.t("edit")} + > + <Icon icon="edit" classes="icon-inline" /> + </button> + </li> + </ul> + ) + ); + } + + siteSidebar() { + return ( + <div + className="md-div" + dangerouslySetInnerHTML={mdToHtml(this.props.site.sidebar)} + /> + ); + } + + admins() { + return ( + <ul class="mt-1 list-inline small mb-0"> + <li class="list-inline-item">{i18n.t("admins")}:</li> + {this.props.admins?.map(av => ( + <li class="list-inline-item"> + <PersonListing person={av.person} /> + </li> + ))} + </ul> + ); + } + + badges() { + let counts = this.props.counts; + let online = this.props.online; + return ( + <ul class="my-2 list-inline"> + <li className="list-inline-item badge badge-secondary"> + {i18n.t("number_online", { + count: online, + formattedCount: numToSI(online), + })} + </li> + <li + className="list-inline-item badge badge-secondary pointer" + data-tippy-content={i18n.t("active_users_in_the_last_day", { + count: counts.users_active_day, + formattedCount: numToSI(counts.users_active_day), + })} + > + {i18n.t("number_of_users", { + count: counts.users_active_day, + formattedCount: numToSI(counts.users_active_day), + })}{" "} + / {i18n.t("day")} + </li> + <li + className="list-inline-item badge badge-secondary pointer" + data-tippy-content={i18n.t("active_users_in_the_last_week", { + count: counts.users_active_week, + formattedCount: counts.users_active_week, + })} + > + {i18n.t("number_of_users", { + count: counts.users_active_week, + formattedCount: numToSI(counts.users_active_week), + })}{" "} + / {i18n.t("week")} + </li> + <li + className="list-inline-item badge badge-secondary pointer" + data-tippy-content={i18n.t("active_users_in_the_last_month", { + count: counts.users_active_month, + formattedCount: counts.users_active_month, + })} + > + {i18n.t("number_of_users", { + count: counts.users_active_month, + formattedCount: numToSI(counts.users_active_month), + })}{" "} + / {i18n.t("month")} + </li> + <li + className="list-inline-item badge badge-secondary pointer" + data-tippy-content={i18n.t("active_users_in_the_last_six_months", { + count: counts.users_active_half_year, + formattedCount: counts.users_active_half_year, + })} + > + {i18n.t("number_of_users", { + count: counts.users_active_half_year, + formattedCount: numToSI(counts.users_active_half_year), + })}{" "} + / {i18n.t("number_of_months", { count: 6, formattedCount: 6 })} + </li> + <li className="list-inline-item badge badge-secondary"> + {i18n.t("number_of_users", { + count: counts.users, + formattedCount: numToSI(counts.users), + })} + </li> + <li className="list-inline-item badge badge-secondary"> + {i18n.t("number_of_communities", { + count: counts.communities, + formattedCount: numToSI(counts.communities), + })} + </li> + <li className="list-inline-item badge badge-secondary"> + {i18n.t("number_of_posts", { + count: counts.posts, + formattedCount: numToSI(counts.posts), + })} + </li> + <li className="list-inline-item badge badge-secondary"> + {i18n.t("number_of_comments", { + count: counts.comments, + formattedCount: numToSI(counts.comments), + })} + </li> + <li className="list-inline-item"> + <Link className="badge badge-primary" to="/modlog"> + {i18n.t("modlog")} + </Link> + </li> + </ul> + ); + } + + get canAdmin(): boolean { + return ( + UserService.Instance.myUserInfo && + this.props.admins + .map(a => a.person.id) + .includes(UserService.Instance.myUserInfo.local_user_view.person.id) + ); + } + + handleCollapseSidebar(i: SiteSidebar) { + i.state.collapsed = !i.state.collapsed; + i.setState(i.state); + } + + handleEditClick(i: SiteSidebar) { + i.state.showEdit = true; + i.setState(i.state); + } + + handleEditSite() { + this.state.showEdit = false; + this.setState(this.state); + } + + handleEditCancel() { + this.state.showEdit = false; + this.setState(this.state); + } +} diff --git a/yarn.lock b/yarn.lock index 0e2321e..71c4304 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4813,10 +4813,10 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" -lemmy-js-client@0.16.0-rc.1: - version "0.16.0-rc.1" - resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.16.0-rc.1.tgz#14c4a526abf4b171c8afe4efbe2a62dcaf6a6f17" - integrity sha512-0hR/gHHsokp46whIHGMBQO2zBKWM7bT6mwKNMZxPvyJo+YW9EbKTO5edjF5E4v8nf3FuIE+gFtm5NFAjCaeWJg== +lemmy-js-client@0.16.4-rc.1: + version "0.16.4-rc.1" + resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.16.4-rc.1.tgz#dfa94a152a7abe75f50f2599a8d9b40c143f37ff" + integrity sha512-94Xh7A/WDywRaJ0GPXPaXZhyXqMzK0gAISNSB8m++2mC1WJalOqfjR72q/7PmLGxfjYO88/aWSz4Sk0SXWJjCw== levn@^0.4.1: version "0.4.1" -- 2.44.1