diff --git a/app/assets/javascripts/discourse/app/components/user-nav.hbs b/app/assets/javascripts/discourse/app/components/user-nav.hbs
new file mode 100644
index 0000000000..e13b2b0b60
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/user-nav.hbs
@@ -0,0 +1,100 @@
+
+
+ {{#unless @user.profile_hidden}}
+ -
+
+ {{d-icon "user"}}
+ {{i18n "user.summary.title"}}
+
+
+
+
+
+ <:submenu>
+ {{i18n "user.filters.all"}}
+ {{i18n "user_action_groups.4"}}
+ {{i18n "user_action_groups.5"}}
+
+ {{#if @showRead}}
+
+ {{i18n "user.read"}}
+
+ {{/if}}
+
+ {{#if @showDrafts}}
+
+ {{this.draftLabel}}
+
+ {{/if}}
+
+ {{#if (gt @user.pending_posts_count 0)}}
+
+ {{this.pendingLabel}}
+
+ {{/if}}
+
+ {{i18n "user_action_groups.1"}}
+
+ {{#if @showBookmarks}}
+ {{i18n "user_action_groups.3"}}
+ {{/if}}
+
+
+
+
+ {{/unless}}
+
+ {{#if @showNotificationsTab}}
+ -
+
+ {{d-icon "comment" class="glyph"}}{{i18n "user.notifications"}}
+
+
+ {{/if}}
+
+ {{#if @showPrivateMessages}}
+ -
+
+ {{d-icon "far-envelope"}}
+ {{i18n "user.private_messages"}}
+
+
+ {{/if}}
+
+ {{#if @canInviteToForum}}
+ -
+
+ {{d-icon "user-plus"}}
+ {{i18n "user.invited.title"}}
+
+
+ {{/if}}
+
+ {{#if @showBadges}}
+ -
+
+ {{d-icon "certificate"}}
+ {{i18n "badges.title"}}
+
+
+ {{/if}}
+
+
+
+ {{#if @user.can_edit}}
+ -
+
+ {{d-icon "cog"}}
+ {{i18n "user.preferences"}}
+
+
+ {{/if}}
+
+
diff --git a/app/assets/javascripts/discourse/app/components/user-nav.js b/app/assets/javascripts/discourse/app/components/user-nav.js
new file mode 100644
index 0000000000..d03668ac62
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/user-nav.js
@@ -0,0 +1,18 @@
+import I18n from "I18n";
+
+import Component from "@glimmer/component";
+import { inject as service } from "@ember/service";
+
+export default class UserNav extends Component {
+ @service currentUser;
+ @service site;
+ @service router;
+
+ get draftLabel() {
+ const count = this.currentUser.draft_count;
+
+ return count > 0
+ ? I18n.t("drafts.label_with_count", { count })
+ : I18n.t("drafts.label");
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/components/user-nav/dropdown-list.hbs b/app/assets/javascripts/discourse/app/components/user-nav/dropdown-list.hbs
new file mode 100644
index 0000000000..7839afe4a8
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/user-nav/dropdown-list.hbs
@@ -0,0 +1,18 @@
+
+
+
+ {{#if (and (has-block "submenu") this.displayList)}}
+
+ {{/if}}
+
diff --git a/app/assets/javascripts/discourse/app/components/user-nav/dropdown-list.js b/app/assets/javascripts/discourse/app/components/user-nav/dropdown-list.js
new file mode 100644
index 0000000000..0e6d095ca2
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/user-nav/dropdown-list.js
@@ -0,0 +1,54 @@
+import Component from "@glimmer/component";
+import { action } from "@ember/object";
+import { tracked } from "@glimmer/tracking";
+import { bind } from "discourse-common/utils/decorators";
+
+export default class UserNavDropdownList extends Component {
+ @tracked displayList = false;
+
+ get chevron() {
+ return this.displayList ? "chevron-up" : "chevron-down";
+ }
+
+ get defaultButtonClass() {
+ return "user-nav-dropdown-button";
+ }
+
+ get buttonClass() {
+ const props = [this.defaultButtonClass];
+
+ if (this.args.isActive) {
+ props.push("active");
+ }
+
+ return props.join(" ");
+ }
+
+ @action
+ toggleList() {
+ this.displayList = !this.displayList;
+ }
+
+ @bind
+ collapseList(e) {
+ const isClickOnButton = e.composedPath().some((element) => {
+ if (element?.classList?.contains(this.defaultButtonClass)) {
+ return true;
+ }
+ });
+
+ if (!isClickOnButton) {
+ this.displayList = false;
+ }
+ }
+
+ @action
+ registerClickListener() {
+ document.addEventListener("click", this.collapseList);
+ }
+
+ @action
+ deregisterClickListener() {
+ document.removeEventListener("click", this.collapseList);
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/controllers/user.js b/app/assets/javascripts/discourse/app/controllers/user.js
index d1e053ef34..47d2b1c59d 100644
--- a/app/assets/javascripts/discourse/app/controllers/user.js
+++ b/app/assets/javascripts/discourse/app/controllers/user.js
@@ -1,6 +1,6 @@
import Controller, { inject as controller } from "@ember/controller";
import EmberObject, { computed, set } from "@ember/object";
-import { and, equal, gt, not, or } from "@ember/object/computed";
+import { and, equal, gt, not, or, readOnly } from "@ember/object/computed";
import CanCheckEmails from "discourse/mixins/can-check-emails";
import User from "discourse/models/user";
import I18n from "I18n";
@@ -164,6 +164,8 @@ export default Controller.extend(CanCheckEmails, {
}
},
+ currentParentRoute: readOnly("router.currentRoute.parent.name"),
+
userNotificationLevel: computed(
"currentUser.ignored_ids",
"model.ignored",
diff --git a/app/assets/javascripts/discourse/app/templates/user.hbs b/app/assets/javascripts/discourse/app/templates/user.hbs
index 47dae2d8a3..489af32e93 100644
--- a/app/assets/javascripts/discourse/app/templates/user.hbs
+++ b/app/assets/javascripts/discourse/app/templates/user.hbs
@@ -224,47 +224,63 @@
{{/unless}}
-
-
-
- {{#unless this.model.profile_hidden}}
-
-
- {{d-icon "user"}}
- {{i18n 'user.summary.title'}}
-
-
-
-
- {{d-icon "stream"}}
- {{i18n 'user.activity_stream'}}
-
-
- {{/unless}}
- {{#if this.showNotificationsTab}}
-
-
- {{d-icon "comment" class="glyph"}}{{i18n 'user.notifications'}}
-
-
- {{/if}}
- {{#if this.showPrivateMessages}}
- {{d-icon "far-envelope"}}{{i18n 'user.private_messages'}}
- {{/if}}
- {{#if this.canInviteToForum}}
- {{d-icon "user-plus"}}{{i18n 'user.invited.title'}}
- {{/if}}
- {{#if this.showBadges}}
- {{d-icon "certificate"}}{{i18n 'badges.title'}}
- {{/if}}
-
- {{#if this.model.can_edit}}
- {{d-icon "cog"}}{{i18n 'user.preferences'}}
- {{/if}}
-
-
- {{outlet}}
-
+ {{#if this.currentUser.redesigned_user_page_nav_enabled}}
+
+ {{else}}
+
+
+
+ {{#unless this.model.profile_hidden}}
+ {{i18n 'user.summary.title'}}
+ {{i18n 'user.activity_stream'}}
+ {{/unless}}
+
+ {{#if this.showNotificationsTab}}
+
+
+ {{d-icon "comment" class="glyph"}}{{i18n 'user.notifications'}}
+
+
+ {{/if}}
+
+ {{#if this.showPrivateMessages}}
+ {{d-icon "far-envelope"}}{{i18n 'user.private_messages'}}
+ {{/if}}
+
+ {{#if this.canInviteToForum}}
+ {{d-icon "user-plus"}}{{i18n 'user.invited.title'}}
+ {{/if}}
+
+ {{#if this.showBadges}}
+ {{d-icon "certificate"}}{{i18n 'badges.title'}}
+ {{/if}}
+
+
+
+ {{#if this.model.can_edit}}
+ {{d-icon "cog"}}{{i18n 'user.preferences'}}
+ {{/if}}
+
+
+
+ {{outlet}}
+
+ {{/if}}
diff --git a/app/assets/javascripts/discourse/app/templates/user/activity.hbs b/app/assets/javascripts/discourse/app/templates/user/activity.hbs
index 5709d95e79..a115f1cf06 100644
--- a/app/assets/javascripts/discourse/app/templates/user/activity.hbs
+++ b/app/assets/javascripts/discourse/app/templates/user/activity.hbs
@@ -1,43 +1,46 @@
-
-
+
+
+ {{#if this.canDownloadPosts}}
+
+ {{/if}}
+{{/unless}}
{{outlet}}
diff --git a/app/assets/stylesheets/common/base/_index.scss b/app/assets/stylesheets/common/base/_index.scss
index dff01e90eb..0af5ac8581 100644
--- a/app/assets/stylesheets/common/base/_index.scss
+++ b/app/assets/stylesheets/common/base/_index.scss
@@ -32,6 +32,7 @@
@import "magnific-popup";
@import "menu-panel";
@import "modal";
+@import "new-user";
@import "not-found";
@import "onebox";
@import "personal-message";
diff --git a/app/assets/stylesheets/common/base/new-user.scss b/app/assets/stylesheets/common/base/new-user.scss
new file mode 100644
index 0000000000..f19e96f5b5
--- /dev/null
+++ b/app/assets/stylesheets/common/base/new-user.scss
@@ -0,0 +1,81 @@
+.new-user-wrapper {
+ .new-user-content-wrapper {
+ // Grid layout
+ width: 100%;
+ display: grid;
+ grid-template-columns: 1fr 5fr;
+ grid-template-rows: auto 1fr;
+ grid-gap: 20px;
+
+ .user-secondary-navigation {
+ grid-column-start: 1;
+ grid-column-end: 2;
+ grid-row-start: 1;
+ grid-row-end: 2;
+ }
+
+ .user-content {
+ grid-column-start: 1;
+ grid-column-end: 3;
+ grid-row-start: 1;
+ grid-row-end: 3;
+ }
+
+ .user-additional-controls {
+ align-self: start;
+ justify-self: start;
+ grid-row-start: 2;
+ }
+
+ .user-secondary-navigation ~ .user-content {
+ grid-column-start: 2;
+ grid-column-end: 3;
+ }
+ }
+
+ .user-nav-dropdown-list-item {
+ position: relative;
+ }
+
+ .user-nav-dropdown-button {
+ background: transparent;
+ }
+
+ .user-nav-dropdown-submenu-wrapper {
+ position: absolute;
+ top: 2em;
+ min-width: 10em;
+ padding: 0;
+ box-shadow: shadow("dropdown");
+ z-index: z("dropdown");
+ }
+
+ .user-nav-dropdown-submenu {
+ background: var(--secondary);
+ list-style-type: none;
+ margin: 0;
+
+ li a {
+ padding: 0.5em 1em;
+ color: var(--primary);
+ .discourse-no-touch & {
+ &:hover {
+ background: var(--highlight-medium);
+ color: currentColor;
+ }
+ }
+
+ &.active {
+ background: var(--tertiary-low);
+ color: currentColor;
+ }
+
+ &:first-of-type {
+ padding-top: 0.5em;
+ }
+ &:last-of-type {
+ padding-bottom: 0.5em;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/common/components/navs.scss b/app/assets/stylesheets/common/components/navs.scss
index 49a6a767fd..4e0f2b2a52 100644
--- a/app/assets/stylesheets/common/components/navs.scss
+++ b/app/assets/stylesheets/common/components/navs.scss
@@ -29,7 +29,8 @@
display: flex;
margin-right: 0.5em;
- > a {
+ > a,
+ button {
border: none;
padding: 6px 12px;
color: var(--primary);
@@ -52,7 +53,8 @@
}
}
- a.active {
+ a.active,
+ button.active {
color: var(--secondary);
background-color: var(--quaternary);
diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb
index 64597982ae..ead635ea80 100644
--- a/app/serializers/current_user_serializer.rb
+++ b/app/serializers/current_user_serializer.rb
@@ -78,13 +78,15 @@ class CurrentUserSerializer < BasicUserSerializer
:sidebar_category_ids,
:likes_notifications_disabled,
:grouped_unread_notifications,
- :redesigned_user_menu_enabled
+ :redesigned_user_menu_enabled,
+ :redesigned_user_page_nav_enabled
delegate :user_stat, to: :object, private: true
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
def groups
owned_group_ids = GroupUser.where(user_id: id, owner: true).pluck(:group_id).to_set
+
object.visible_groups.pluck(:id, :name, :has_messages).map do |id, name, has_messages|
group = { id: id, name: name, has_messages: has_messages }
group[:owner] = true if owned_group_ids.include?(id)
@@ -342,4 +344,12 @@ class CurrentUserSerializer < BasicUserSerializer
def include_unseen_reviewable_count?
redesigned_user_menu_enabled
end
+
+ def redesigned_user_page_nav_enabled
+ if SiteSetting.enable_new_user_profile_nav_groups.present?
+ GroupUser.exists?(user_id: object.id, group_id: SiteSetting.enable_new_user_profile_nav_groups.split("|"))
+ else
+ false
+ end
+ end
end
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 40663a80a1..ba7d2671ea 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -2002,6 +2002,14 @@ developer:
type: tag_list
default: ""
client: true
+ enable_new_user_profile_nav_groups:
+ client: true
+ type: group_list
+ list_type: compact
+ default: ""
+ allow_any: false
+ refresh: true
+ hidden: true
embedding:
embed_by_username:
diff --git a/spec/serializers/current_user_serializer_spec.rb b/spec/serializers/current_user_serializer_spec.rb
index 7e5a79038b..b8f3ced181 100644
--- a/spec/serializers/current_user_serializer_spec.rb
+++ b/spec/serializers/current_user_serializer_spec.rb
@@ -350,4 +350,26 @@ RSpec.describe CurrentUserSerializer do
expect(serializer.as_json[:likes_notifications_disabled]).to eq(false)
end
end
+
+ describe '#redesigned_user_page_nav_enabled' do
+ fab!(:group) { Fabricate(:group) }
+ fab!(:group2) { Fabricate(:group) }
+
+ it "is false when enable_new_user_profile_nav_groups site setting has not been set" do
+ expect(serializer.as_json[:redesigned_user_page_nav_enabled]).to eq(false)
+ end
+
+ it 'is false if user does not belong to any of the configured groups in the enable_new_user_profile_nav_groups site setting' do
+ SiteSetting.enable_new_user_profile_nav_groups = "#{group.id}|#{group2.id}"
+
+ expect(serializer.as_json[:redesigned_user_page_nav_enabled]).to eq(false)
+ end
+
+ it 'is true if user belongs one of the configured groups in the enable_new_user_profile_nav_groups site setting' do
+ SiteSetting.enable_new_user_profile_nav_groups = "#{group.id}|#{group2.id}"
+ group.add(user)
+
+ expect(serializer.as_json[:redesigned_user_page_nav_enabled]).to eq(true)
+ end
+ end
end