]> Untitled Git - lemmy-ui.git/commitdiff
Merge branch 'main' into breakout-role-utils
authorDessalines <dessalines@users.noreply.github.com>
Mon, 19 Jun 2023 14:04:19 +0000 (10:04 -0400)
committerGitHub <noreply@github.com>
Mon, 19 Jun 2023 14:04:19 +0000 (10:04 -0400)
1  2 
src/shared/components/home/home.tsx
src/shared/components/home/signup.tsx
src/shared/components/person/settings.tsx
src/shared/components/post/post-listing.tsx

index 54378f340fc4552f1dbd517c2bde9a8cdc01d7dd,4f9b9694cf501d857fdb6ba978d174294d6d7f1c..39753c48e698ee53f4729d827297815fa352edbe
@@@ -57,6 -57,7 +57,6 @@@ import { UserService } from "../../serv
  import { FirstLoadService } from "../../services/FirstLoadService";
  import { HttpService, RequestState } from "../../services/HttpService";
  import {
 -  canCreateCommunity,
    commentsToFlatNodes,
    editComment,
    editPost,
    getCommentParentId,
    getDataTypeString,
    getPageFromString,
 -  getQueryParams,
 -  getQueryString,
    getRandomFromList,
    mdToHtml,
    myAuth,
    postToCommentSortType,
 -  QueryParams,
    relTags,
    restoreScrollPosition,
    RouteDataResponse,
    trendingFetchLimit,
    updatePersonBlock,
  } from "../../utils";
 +import { getQueryParams } from "../../utils/helpers/get-query-params";
 +import { getQueryString } from "../../utils/helpers/get-query-string";
 +import { canCreateCommunity } from "../../utils/roles/can-create-community";
 +import type { QueryParams } from "../../utils/types/query-params";
  import { CommentNodes } from "../comment/comment-nodes";
  import { DataTypeSelect } from "../common/data-type-select";
  import { HtmlTags } from "../common/html-tags";
@@@ -443,16 -443,17 +443,17 @@@ export class Home extends Component<any
                admins={admins}
                counts={counts}
                showLocal={showLocal(this.isoData)}
+               isMobile={true}
              />
            )}
            {showTrendingMobile && (
-             <div className="col-12 card border-secondary mb-3">
-               <div className="card-body">{this.trendingCommunities(true)}</div>
+             <div className="card border-secondary mb-3">
+               {this.trendingCommunities()}
              </div>
            )}
            {showSubscribedMobile && (
-             <div className="col-12 card border-secondary mb-3">
-               <div className="card-body">{this.subscribedCommunities}</div>
+             <div className="card border-secondary mb-3">
+               {this.subscribedCommunities(true)}
              </div>
            )}
          </div>
      return (
        <div id="sidebarContainer">
          <section id="sidebarMain" className="card border-secondary mb-3">
-           <div className="card-body">
-             {this.trendingCommunities()}
-             {canCreateCommunity(this.state.siteRes) && (
-               <LinkButton
-                 path="/create_community"
-                 translationKey="create_a_community"
-               />
-             )}
-             <LinkButton
-               path="/communities"
-               translationKey="explore_communities"
-             />
-           </div>
+           {this.trendingCommunities()}
          </section>
          <SiteSidebar
            site={site}
            showLocal={showLocal(this.isoData)}
          />
          {this.hasFollows && (
-           <section
-             id="sidebarSubscribed"
-             className="card border-secondary mb-3"
-           >
-             <div className="card-body">{this.subscribedCommunities}</div>
-           </section>
+           <div className="accordion">
+             <section
+               id="sidebarSubscribed"
+               className="card border-secondary mb-3"
+             >
+               {this.subscribedCommunities(false)}
+             </section>
+           </div>
          )}
        </div>
      );
    }
  
-   trendingCommunities(isMobile = false) {
+   trendingCommunities() {
      switch (this.state.trendingCommunitiesRes?.state) {
        case "loading":
          return (
        case "success": {
          const trending = this.state.trendingCommunitiesRes.data.communities;
          return (
-           <div className={!isMobile ? "mb-2" : ""}>
-             <h5>
-               <T i18nKey="trending_communities">
-                 #
-                 <Link className="text-body" to="/communities">
+           <>
+             <header className="card-header d-flex align-items-center">
+               <h5 className="mb-0">
+                 <T i18nKey="trending_communities">
                    #
-                 </Link>
-               </T>
-             </h5>
-             <ul className="list-inline mb-0">
-               {trending.map(cv => (
-                 <li
-                   key={cv.community.id}
-                   className="list-inline-item d-inline-block"
-                 >
-                   <CommunityLink community={cv.community} />
-                 </li>
-               ))}
-             </ul>
-           </div>
+                   <Link className="text-body" to="/communities">
+                     #
+                   </Link>
+                 </T>
+               </h5>
+             </header>
+             <div className="card-body">
+               {trending.length > 0 && (
+                 <ul className="list-inline">
+                   {trending.map(cv => (
+                     <li key={cv.community.id} className="list-inline-item">
+                       <CommunityLink community={cv.community} />
+                     </li>
+                   ))}
+                 </ul>
+               )}
+               {canCreateCommunity(this.state.siteRes) && (
+                 <LinkButton
+                   path="/create_community"
+                   translationKey="create_a_community"
+                 />
+               )}
+               <LinkButton
+                 path="/communities"
+                 translationKey="explore_communities"
+               />
+             </div>
+           </>
          );
        }
      }
    }
  
-   get subscribedCommunities() {
+   subscribedCommunities(isMobile = false) {
      const { subscribedCollapsed } = this.state;
  
      return (
-       <div>
-         <h5>
-           <T class="d-inline" i18nKey="subscribed_to_communities">
-             #
-             <Link className="text-body" to="/communities">
+       <>
+         <header
+           className="card-header d-flex align-items-center"
+           id="sidebarSubscribedHeader"
+         >
+           <h5 className="mb-0 d-inline">
+             <T class="d-inline" i18nKey="subscribed_to_communities">
                #
-             </Link>
-           </T>
-           <button
-             className="btn btn-sm text-muted"
-             onClick={linkEvent(this, this.handleCollapseSubscribe)}
-             aria-label={i18n.t("collapse")}
-             data-tippy-content={i18n.t("collapse")}
-           >
-             <Icon
-               icon={`${subscribedCollapsed ? "plus" : "minus"}-square`}
-               classes="icon-inline"
-             />
-           </button>
-         </h5>
-         {!subscribedCollapsed && (
-           <ul className="list-inline mb-0">
-             {UserService.Instance.myUserInfo?.follows.map(cfv => (
-               <li
-                 key={cfv.community.id}
-                 className="list-inline-item d-inline-block"
-               >
-                 <CommunityLink community={cfv.community} />
-               </li>
-             ))}
-           </ul>
-         )}
-       </div>
+               <Link className="text-body" to="/communities">
+                 #
+               </Link>
+             </T>
+           </h5>
+           {!isMobile && (
+             <button
+               type="button"
+               className="btn btn-sm text-muted"
+               onClick={linkEvent(this, this.handleCollapseSubscribe)}
+               aria-label={
+                 subscribedCollapsed ? i18n.t("expand") : i18n.t("collapse")
+               }
+               data-tippy-content={
+                 subscribedCollapsed ? i18n.t("expand") : i18n.t("collapse")
+               }
+               data-bs-toggle="collapse"
+               data-bs-target="#sidebarSubscribedBody"
+               aria-expanded="true"
+               aria-controls="sidebarSubscribedBody"
+             >
+               <Icon
+                 icon={`${subscribedCollapsed ? "plus" : "minus"}-square`}
+                 classes="icon-inline"
+               />
+             </button>
+           )}
+         </header>
+         <div
+           id="sidebarSubscribedBody"
+           className="collapse show"
+           aria-labelledby="sidebarSubscribedHeader"
+         >
+           <div className="card-body">
+             <ul className="list-inline mb-0">
+               {UserService.Instance.myUserInfo?.follows.map(cfv => (
+                 <li
+                   key={cfv.community.id}
+                   className="list-inline-item d-inline-block"
+                 >
+                   <CommunityLink community={cfv.community} />
+                 </li>
+               ))}
+             </ul>
+           </div>
+         </div>
+       </>
      );
    }
  
index 192393db98bee758eaab96cdc050ff13936d87df,71398a8dbf221bdb0619fe43190a34fac9b4d83b..c7306950b515df029f8dde68fb3e6d43a33e74bb
@@@ -13,6 -13,7 +13,6 @@@ import { i18n } from "../../i18next"
  import { UserService } from "../../services";
  import { HttpService, RequestState } from "../../services/HttpService";
  import {
 -  isBrowser,
    joinLemmyUrl,
    mdToHtml,
    myAuth,
@@@ -20,7 -21,6 +20,7 @@@
    toast,
    validEmail,
  } from "../../utils";
 +import { isBrowser } from "../../utils/browser/is-browser";
  import { HtmlTags } from "../common/html-tags";
  import { Icon, Spinner } from "../common/icon";
  import { MarkdownTextArea } from "../common/markdown-textarea";
@@@ -298,27 -298,22 +298,22 @@@ export class Signup extends Component<a
            </>
          )}
          {this.renderCaptcha()}
-         {siteView.local_site.enable_nsfw && (
-           <div className="form-group row">
-             <div className="col-sm-10">
-               <div className="form-check">
-                 <input
-                   className="form-check-input"
-                   id="register-show-nsfw"
-                   type="checkbox"
-                   checked={this.state.form.show_nsfw}
-                   onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
-                 />
-                 <label
-                   className="form-check-label"
-                   htmlFor="register-show-nsfw"
-                 >
-                   {i18n.t("show_nsfw")}
-                 </label>
-               </div>
+         <div className="form-group row">
+           <div className="col-sm-10">
+             <div className="form-check">
+               <input
+                 className="form-check-input"
+                 id="register-show-nsfw"
+                 type="checkbox"
+                 checked={this.state.form.show_nsfw}
+                 onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
+               />
+               <label className="form-check-label" htmlFor="register-show-nsfw">
+                 {i18n.t("show_nsfw")}
+               </label>
              </div>
            </div>
-         )}
+         </div>
          <input
            tabIndex={-1}
            autoComplete="false"
index 3c8e9fcdd743444b24ace411615d5aa7746d208c,34303cb2775f56ecd32067b29578451de7f904f4..971ec8911a63d9915c5abcd1f791d17e5a4ca434
@@@ -18,9 -18,9 +18,8 @@@ import 
    Choice,
    capitalizeFirstLetter,
    communityToChoice,
 -  debounce,
    elementUrl,
    emDash,
-   enableNsfw,
    fetchCommunities,
    fetchThemeList,
    fetchUsers,
@@@ -36,7 -36,6 +35,7 @@@
    updateCommunityBlock,
    updatePersonBlock,
  } from "../../utils";
 +import { debounce } from "../../utils/helpers/debounce";
  import { HtmlTags } from "../common/html-tags";
  import { Icon, Spinner } from "../common/icon";
  import { ImageUploadForm } from "../common/image-upload-form";
@@@ -642,22 -641,20 +641,20 @@@ export class Settings extends Component
                />
              </div>
            </form>
-           {enableNsfw(this.state.siteRes) && (
-             <div className="form-group">
-               <div className="form-check">
-                 <input
-                   className="form-check-input"
-                   id="user-show-nsfw"
-                   type="checkbox"
-                   checked={this.state.saveUserSettingsForm.show_nsfw}
-                   onChange={linkEvent(this, this.handleShowNsfwChange)}
-                 />
-                 <label className="form-check-label" htmlFor="user-show-nsfw">
-                   {i18n.t("show_nsfw")}
-                 </label>
-               </div>
+           <div className="form-group">
+             <div className="form-check">
+               <input
+                 className="form-check-input"
+                 id="user-show-nsfw"
+                 type="checkbox"
+                 checked={this.state.saveUserSettingsForm.show_nsfw}
+                 onChange={linkEvent(this, this.handleShowNsfwChange)}
+               />
+               <label className="form-check-label" htmlFor="user-show-nsfw">
+                 {i18n.t("show_nsfw")}
+               </label>
              </div>
-           )}
+           </div>
            <div className="form-group">
              <div className="form-check">
                <input
index a95599b258e74c5fd480ce1fa418d28a0078662c,80da4b327dd633fdeb97b28c852181c8ed5d38c0..922fa61efbb72c23508d7e3f80168ecd02dd0365
@@@ -28,9 -28,18 +28,9 @@@ import { i18n } from "../../i18next"
  import { BanType, PostFormParams, PurgeType, VoteType } from "../../interfaces";
  import { UserService } from "../../services";
  import {
 -  amAdmin,
 -  amCommunityCreator,
 -  amMod,
 -  canAdmin,
 -  canMod,
 -  canShare,
    futureDaysToUnixTime,
    hostname,
 -  isAdmin,
 -  isBanned,
    isImage,
 -  isMod,
    isVideo,
    mdNoImages,
    mdToHtml,
    numToSI,
    relTags,
    setupTippy,
 -  share,
    showScores,
  } from "../../utils";
 +import { canShare } from "../../utils/browser/can-share";
 +import { share } from "../../utils/browser/share";
 +import { amAdmin } from "../../utils/roles/am-admin";
 +import { amCommunityCreator } from "../../utils/roles/am-community-creator";
 +import { amMod } from "../../utils/roles/am-mod";
 +import { canAdmin } from "../../utils/roles/can-admin";
 +import { canMod } from "../../utils/roles/can-mod";
 +import { isAdmin } from "../../utils/roles/is-admin";
 +import { isBanned } from "../../utils/roles/is-banned";
 +import { isMod } from "../../utils/roles/is-mod";
  import { Icon, PurgeWarning, Spinner } from "../common/icon";
  import { MomentTime } from "../common/moment-time";
  import { PictrsImage } from "../common/pictrs-image";
@@@ -201,7 -201,7 +201,7 @@@ export class PostListing extends Compon
      const post = this.postView.post;
  
      return (
-       <div className="post-listing">
+       <div className="post-listing mt-2">
          {!this.state.showEdit ? (
            <>
              {this.listing()}
              </span>
            )}
            {this.props.showCommunity && (
-             <span>
-               <span className="mx-1"> {i18n.t("to")} </span>
-               <CommunityLink community={post_view.community} />
-             </span>
+             <>
+               {" "}
+               {i18n.t("to")} <CommunityLink community={post_view.community} />
+             </>
            )}
          </li>
          {post_view.post.language_id !== 0 && (
      const post = this.postView.post;
      return (
        <Link
-         className={`d-inline-block ${
+         className={`d-inline ${
            !post.featured_community && !post.featured_local
              ? "text-body"
              : "text-primary"
          to={`/post/${post.id}`}
          title={i18n.t("comments")}
        >
-         <div
-           className="d-inline-block"
+         <span
+           className="d-inline"
            dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
          />
        </Link>
  
      return (
        <div className="post-title overflow-hidden">
-         <h5>
-           {url ? (
-             this.props.showBody ? (
-               <a
-                 className={`d-inline-block ${
-                   !post.featured_community && !post.featured_local
-                     ? "text-body"
-                     : "text-primary"
-                 }`}
-                 href={url}
-                 title={url}
-                 rel={relTags}
-               >
-                 <div
-                   className="d-inline-block"
-                   dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
-                 />
-               </a>
-             ) : (
-               this.postLink
-             )
+         <h5 className="d-inline">
+           {url && this.props.showBody ? (
+             <a
+               className={
+                 !post.featured_community && !post.featured_local
+                   ? "text-body"
+                   : "text-primary"
+               }
+               href={url}
+               title={url}
+               rel={relTags}
+               dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
+             ></a>
            ) : (
              this.postLink
            )}
-           {(url && isImage(url)) ||
-             (post.thumbnail_url && (
-               <button
-                 className="btn btn-link text-monospace text-muted small d-inline-block"
-                 data-tippy-content={i18n.t("expand_here")}
-                 onClick={linkEvent(this, this.handleImageExpandClick)}
-               >
-                 <Icon
-                   icon={
-                     !this.state.imageExpanded ? "plus-square" : "minus-square"
-                   }
-                   classes="icon-inline"
-                 />
-               </button>
-             ))}
-           {post.removed && (
-             <small className="ml-2 text-muted font-italic">
-               {i18n.t("removed")}
-             </small>
-           )}
-           {post.deleted && (
-             <small
-               className="unselectable pointer ml-2 text-muted font-italic"
-               data-tippy-content={i18n.t("deleted")}
-             >
-               <Icon icon="trash" classes="icon-inline text-danger" />
-             </small>
-           )}
-           {post.locked && (
-             <small
-               className="unselectable pointer ml-2 text-muted font-italic"
-               data-tippy-content={i18n.t("locked")}
-             >
-               <Icon icon="lock" classes="icon-inline text-danger" />
-             </small>
-           )}
-           {post.featured_community && (
-             <small
-               className="unselectable pointer ml-2 text-muted font-italic"
-               data-tippy-content={i18n.t("featured")}
-             >
-               <Icon icon="pin" classes="icon-inline text-primary" />
-             </small>
-           )}
-           {post.featured_local && (
-             <small
-               className="unselectable pointer ml-2 text-muted font-italic"
-               data-tippy-content={i18n.t("featured")}
-             >
-               <Icon icon="pin" classes="icon-inline text-secondary" />
-             </small>
-           )}
-           {post.nsfw && (
-             <small className="ml-2 text-muted font-italic">
-               {i18n.t("nsfw")}
-             </small>
-           )}
          </h5>
+         {(url && isImage(url)) ||
+           (post.thumbnail_url && (
+             <button
+               className="btn btn-link text-monospace text-muted small d-inline-block"
+               data-tippy-content={i18n.t("expand_here")}
+               onClick={linkEvent(this, this.handleImageExpandClick)}
+             >
+               <Icon
+                 icon={
+                   !this.state.imageExpanded ? "plus-square" : "minus-square"
+                 }
+                 classes="icon-inline"
+               />
+             </button>
+           ))}
+         {post.removed && (
+           <small className="ml-2 badge text-bg-secondary">
+             {i18n.t("removed")}
+           </small>
+         )}
+         {post.deleted && (
+           <small
+             className="unselectable pointer ml-2 text-muted font-italic"
+             data-tippy-content={i18n.t("deleted")}
+           >
+             <Icon icon="trash" classes="icon-inline text-danger" />
+           </small>
+         )}
+         {post.locked && (
+           <small
+             className="unselectable pointer ml-2 text-muted font-italic"
+             data-tippy-content={i18n.t("locked")}
+           >
+             <Icon icon="lock" classes="icon-inline text-danger" />
+           </small>
+         )}
+         {post.featured_community && (
+           <small
+             className="unselectable pointer ml-2 text-muted font-italic"
+             data-tippy-content={i18n.t("featured")}
+           >
+             <Icon icon="pin" classes="icon-inline text-primary" />
+           </small>
+         )}
+         {post.featured_local && (
+           <small
+             className="unselectable pointer ml-2 text-muted font-italic"
+             data-tippy-content={i18n.t("featured")}
+           >
+             <Icon icon="pin" classes="icon-inline text-secondary" />
+           </small>
+         )}
+         {post.nsfw && (
+           <small className="ml-2 badge text-bg-danger">{i18n.t("nsfw")}</small>
+         )}
        </div>
      );
    }
      const post = this.postView.post;
  
      return (
-       <div className="d-flex align-items-center justify-content-start flex-wrap text-muted font-weight-bold mb-1">
+       <div className="d-flex align-items-center justify-content-start flex-wrap text-muted font-weight-bold">
          {this.commentsButton}
          {canShare() && (
            <button
-             className="btn btn-link"
+             className="btn btn-sm btn-link"
              onClick={linkEvent(this, this.handleShare)}
              type="button"
            >
          {mobile && !this.props.viewOnly && this.mobileVotes}
          {UserService.Instance.myUserInfo &&
            !this.props.viewOnly &&
-           this.postActions(mobile)}
+           this.postActions()}
        </div>
      );
    }
  
-   postActions(mobile = false) {
+   get hasAdvancedButtons() {
+     return (
+       this.myPost ||
+       (this.showBody && this.postView.post.body) ||
+       amMod(this.props.moderators) ||
+       amAdmin() ||
+       this.canMod_ ||
+       this.canAdmin_
+     );
+   }
+   postActions() {
      // Possible enhancement: Priority+ pattern instead of just hard coding which get hidden behind the show more button.
      // Possible enhancement: Make each button a component.
      const post_view = this.postView;
        <>
          {this.saveButton}
          {this.crossPostButton}
-         {mobile && this.showMoreButton}
-         {(!mobile || this.state.showAdvanced) && (
-           <>
-             {!this.myPost && (
-               <>
-                 {this.reportButton}
-                 {this.blockButton}
-               </>
-             )}
-             {this.myPost && (this.showBody || this.state.showAdvanced) && (
-               <>
-                 {this.editButton}
-                 {this.deleteButton}
-               </>
-             )}
-           </>
-         )}
-         {this.state.showAdvanced && (
-           <>
-             {this.showBody && post_view.post.body && this.viewSourceButton}
-             {/* Any mod can do these, not limited to hierarchy*/}
-             {(amMod(this.props.moderators) || amAdmin()) && (
-               <>
-                 {this.lockButton}
-                 {this.featureButton}
-               </>
-             )}
-             {(this.canMod_ || this.canAdmin_) && <>{this.modRemoveButton}</>}
-           </>
+         {this.showBody && post_view.post.body && this.viewSourceButton}
+         {this.hasAdvancedButtons && (
+           <div className="dropdown">
+             <button
+               className="btn btn-link btn-animate text-muted py-0 dropdown-toggle"
+               onClick={linkEvent(this, this.handleShowAdvanced)}
+               data-tippy-content={i18n.t("more")}
+               data-bs-toggle="dropdown"
+               aria-expanded="false"
+               aria-controls="advancedButtonsDropdown"
+               aria-label={i18n.t("more")}
+             >
+               <Icon icon="more-vertical" inline />
+             </button>
+             <ul className="dropdown-menu" id="advancedButtonsDropdown">
+               {!this.myPost ? (
+                 <>
+                   <li>{this.reportButton}</li>
+                   <li>{this.blockButton}</li>
+                 </>
+               ) : (
+                 <>
+                   <li>{this.editButton}</li>
+                   <li>{this.deleteButton}</li>
+                 </>
+               )}
+               {/* Any mod can do these, not limited to hierarchy*/}
+               {(amMod(this.props.moderators) || amAdmin()) && (
+                 <>
+                   <li>
+                     <hr className="dropdown-divider" />
+                   </li>
+                   <li>{this.lockButton}</li>
+                   {this.featureButtons}
+                 </>
+               )}
+               {(this.canMod_ || this.canAdmin_) && (
+                 <li>{this.modRemoveButton}</li>
+               )}
+             </ul>
+           </div>
          )}
-         {!mobile && this.showMoreButton}
        </>
      );
    }
      const post_view = this.postView;
      return (
        <Link
-         className="btn btn-link text-muted py-0 pl-0 text-muted"
+         className="btn btn-link text-muted pl-0 text-muted"
          title={i18n.t("number_of_comments", {
            count: Number(post_view.counts.comments),
            formattedCount: Number(post_view.counts.comments),
    get reportButton() {
      return (
        <button
-         className="btn btn-link btn-animate text-muted py-0"
+         className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
          onClick={linkEvent(this, this.handleShowReportDialog)}
-         data-tippy-content={i18n.t("show_report_dialog")}
          aria-label={i18n.t("show_report_dialog")}
        >
-         <Icon icon="flag" inline />
+         <Icon classes="mr-1" icon="flag" inline />
+         {i18n.t("create_report")}
        </button>
      );
    }
    get blockButton() {
      return (
        <button
-         className="btn btn-link btn-animate text-muted py-0"
+         className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
          onClick={linkEvent(this, this.handleBlockPersonClick)}
-         data-tippy-content={i18n.t("block_user")}
          aria-label={i18n.t("block_user")}
        >
-         {this.state.blockLoading ? <Spinner /> : <Icon icon="slash" inline />}
+         {this.state.blockLoading ? (
+           <Spinner />
+         ) : (
+           <Icon classes="mr-1" icon="slash" inline />
+         )}
+         {i18n.t("block_user")}
        </button>
      );
    }
    get editButton() {
      return (
        <button
-         className="btn btn-link btn-animate text-muted py-0"
+         className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
          onClick={linkEvent(this, this.handleEditClick)}
-         data-tippy-content={i18n.t("edit")}
          aria-label={i18n.t("edit")}
        >
-         <Icon icon="edit" inline />
+         <Icon classes="mr-1" icon="edit" inline />
+         {i18n.t("edit")}
        </button>
      );
    }
      const label = !deleted ? i18n.t("delete") : i18n.t("restore");
      return (
        <button
-         className="btn btn-link btn-animate text-muted py-0"
+         className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
          onClick={linkEvent(this, this.handleDeleteClick)}
-         data-tippy-content={label}
          aria-label={label}
        >
          {this.state.deleteLoading ? (
            <Spinner />
          ) : (
-           <Icon
-             icon="trash"
-             classes={classNames({ "text-danger": deleted })}
-             inline
-           />
+           <>
+             <Icon
+               icon="trash"
+               classes={classNames("mr-1", { "text-danger": deleted })}
+               inline
+             />
+             {label}
+           </>
          )}
        </button>
      );
    }
  
-   get showMoreButton() {
-     return (
-       <button
-         className="btn btn-link btn-animate text-muted py-0"
-         onClick={linkEvent(this, this.handleShowAdvanced)}
-         data-tippy-content={i18n.t("more")}
-         aria-label={i18n.t("more")}
-       >
-         <Icon icon="more-vertical" inline />
-       </button>
-     );
-   }
    get viewSourceButton() {
      return (
        <button
      const label = locked ? i18n.t("unlock") : i18n.t("lock");
      return (
        <button
-         className="btn btn-link btn-animate text-muted py-0"
+         className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
          onClick={linkEvent(this, this.handleModLock)}
-         data-tippy-content={label}
          aria-label={label}
        >
          {this.state.lockLoading ? (
            <Spinner />
          ) : (
-           <Icon
-             icon="lock"
-             classes={classNames({ "text-danger": locked })}
-             inline
-           />
+           <>
+             <Icon
+               icon="lock"
+               classes={classNames("mr-1", { "text-danger": locked })}
+               inline
+             />
+             {label}
+           </>
          )}
        </button>
      );
    }
  
-   get featureButton() {
+   get featureButtons() {
      const featuredCommunity = this.postView.post.featured_community;
      const labelCommunity = featuredCommunity
        ? i18n.t("unfeature_from_community")
        ? i18n.t("unfeature_from_local")
        : i18n.t("feature_in_local");
      return (
-       <span>
-         <button
-           className="btn btn-link btn-animate text-muted py-0 pl-0"
-           onClick={linkEvent(this, this.handleModFeaturePostCommunity)}
-           data-tippy-content={labelCommunity}
-           aria-label={labelCommunity}
-         >
-           {this.state.featureCommunityLoading ? (
-             <Spinner />
-           ) : (
-             <span>
-               <Icon
-                 icon="pin"
-                 classes={classNames({ "text-success": featuredCommunity })}
-                 inline
-               />
-               {i18n.t("community")}
-             </span>
-           )}
-         </button>
-         {amAdmin() && (
+       <>
+         <li>
            <button
-             className="btn btn-link btn-animate text-muted py-0"
-             onClick={linkEvent(this, this.handleModFeaturePostLocal)}
-             data-tippy-content={labelLocal}
-             aria-label={labelLocal}
+             className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
+             onClick={linkEvent(this, this.handleModFeaturePostCommunity)}
+             data-tippy-content={labelCommunity}
+             aria-label={labelCommunity}
            >
-             {this.state.featureLocalLoading ? (
+             {this.state.featureCommunityLoading ? (
                <Spinner />
              ) : (
-               <span>
+               <>
                  <Icon
                    icon="pin"
-                   classes={classNames({ "text-success": featuredLocal })}
+                   classes={classNames("mr-1", {
+                     "text-success": featuredCommunity,
+                   })}
                    inline
                  />
-                 {i18n.t("local")}
-               </span>
+                 {i18n.t("community")}
+               </>
              )}
            </button>
-         )}
-       </span>
+         </li>
+         <li>
+           {amAdmin() && (
+             <button
+               className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
+               onClick={linkEvent(this, this.handleModFeaturePostLocal)}
+               data-tippy-content={labelLocal}
+               aria-label={labelLocal}
+             >
+               {this.state.featureLocalLoading ? (
+                 <Spinner />
+               ) : (
+                 <>
+                   <Icon
+                     icon="pin"
+                     classes={classNames("mr-1", {
+                       "text-success": featuredLocal,
+                     })}
+                     inline
+                   />
+                   {i18n.t("local")}
+                 </>
+               )}
+             </button>
+           )}
+         </li>
+       </>
      );
    }
  
      const removed = this.postView.post.removed;
      return (
        <button
-         className="btn btn-link btn-animate text-muted py-0"
+         className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
          onClick={linkEvent(
            this,
            !removed ? this.handleModRemoveShow : this.handleModRemoveSubmit