This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/app/widgets/header.js
Alan Guo Xiang Tan 7a05a9d411
UX: Make Sidebar more consistent with user menu on mobile (#17940)
Before this commit, we carried custom code and styles for the sidebar on
mobile. This meant the look and feel of bringing up the sidebar on
mobile was very different from the user menu resulting in a very
inconsistent experience on mobile. Also, we could not leverage on the
existing swipe to close support on mobile.

In this commit, we made it such that the sidebar dropdown is always
rendered on mobile and made the interaction with the dropdown more
consistent with the user menu. There is also more parity with the old
hamburger dropdown when the experimental sidebar is disabled.
2022-08-16 13:45:32 +08:00

748 lines
18 KiB
JavaScript

import DiscourseURL, { userPath } from "discourse/lib/url";
import I18n from "I18n";
import { addExtraUserClasses } from "discourse/helpers/user-avatar";
import { ajax } from "discourse/lib/ajax";
import { avatarImg } from "discourse/widgets/post";
import { createWidget } from "discourse/widgets/widget";
import getURL from "discourse-common/lib/get-url";
import { h } from "virtual-dom";
import { iconNode } from "discourse-common/lib/icon-library";
import { schedule } from "@ember/runloop";
import { scrollTop } from "discourse/mixins/scroll-top";
import { wantsNewWindow } from "discourse/lib/intercept-click";
import { logSearchLinkClick } from "discourse/lib/search";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import { hbs } from "ember-cli-htmlbars";
let _extraHeaderIcons = [];
export function addToHeaderIcons(icon) {
_extraHeaderIcons.push(icon);
}
export function clearExtraHeaderIcons() {
_extraHeaderIcons = [];
}
const dropdown = {
buildClasses(attrs) {
let classes = attrs.classNames || [];
if (attrs.active) {
classes.push("active");
}
return classes;
},
click(e) {
if (wantsNewWindow(e)) {
return;
}
e.preventDefault();
if (!this.attrs.active) {
this.sendWidgetAction(this.attrs.action);
}
},
};
createWidget("header-notifications", {
settings: {
avatarSize: "medium",
},
html(attrs) {
const { user } = attrs;
let avatarAttrs = {
template: user.get("avatar_template"),
username: user.get("username"),
};
if (this.siteSettings.enable_names) {
avatarAttrs.name = user.get("name");
}
const contents = [
avatarImg(
this.settings.avatarSize,
Object.assign(
{
alt: "user.avatar.header_title",
},
addExtraUserClasses(user, avatarAttrs)
)
),
];
if (this.currentUser.status) {
contents.push(this.attach("user-status-bubble", this.currentUser.status));
}
if (user.isInDoNotDisturb()) {
contents.push(h("div.do-not-disturb-background", iconNode("moon")));
} else {
if (this.currentUser.redesigned_user_menu_enabled) {
const unread = user.all_unread_notifications_count || 0;
const reviewables = user.unseen_reviewable_count || 0;
const count = unread + reviewables;
if (count > 0) {
if (this._shouldHighlightAvatar()) {
this._addAvatarHighlight(contents);
}
contents.push(
this.attach("link", {
action: attrs.action,
className: "badge-notification unread-notifications",
rawLabel: count,
omitSpan: true,
title: "notifications.tooltip.regular",
titleOptions: { count },
})
);
}
} else {
const unreadNotifications = user.unread_notifications;
if (!!unreadNotifications) {
contents.push(
this.attach("link", {
action: attrs.action,
className: "badge-notification unread-notifications",
rawLabel: unreadNotifications,
omitSpan: true,
title: "notifications.tooltip.regular",
titleOptions: { count: unreadNotifications },
})
);
}
const unreadHighPriority = user.unread_high_priority_notifications;
if (!!unreadHighPriority) {
if (this._shouldHighlightAvatar()) {
this._addAvatarHighlight(contents);
}
// add the counter for the unread high priority
contents.push(
this.attach("link", {
action: attrs.action,
className:
"badge-notification unread-high-priority-notifications",
rawLabel: unreadHighPriority,
omitSpan: true,
title: "notifications.tooltip.high_priority",
titleOptions: { count: unreadHighPriority },
})
);
}
}
}
return contents;
},
_shouldHighlightAvatar() {
const attrs = this.attrs;
const { user } = attrs;
return (
!user.read_first_notification &&
!user.enforcedSecondFactor &&
!attrs.active &&
attrs.ringBackdrop
);
},
_addAvatarHighlight(contents) {
contents.push(h("span.ring"));
contents.push(h("span.ring-backdrop-spotlight"));
contents.push(
h(
"span.ring-backdrop",
{},
h("h1.ring-first-notification", {}, [
h(
"span",
{ className: "first-notification" },
I18n.t("user.first_notification")
),
h("span", { className: "read-later" }, [
this.attach("link", {
action: "readLater",
className: "read-later-link",
label: "user.skip_new_user_tips.read_later",
}),
]),
h("span", {}, [
I18n.t("user.skip_new_user_tips.not_first_time"),
" ",
this.attach("link", {
action: "skipNewUserTips",
className: "skip-new-user-tips",
label: "user.skip_new_user_tips.skip_link",
title: "user.skip_new_user_tips.description",
}),
]),
])
)
);
},
});
createWidget(
"user-dropdown",
Object.assign(
{
tagName: "li.header-dropdown-toggle.current-user",
buildId() {
return "current-user";
},
html(attrs) {
return h(
"a.icon",
{
attributes: {
"aria-haspopup": true,
"aria-expanded": attrs.active,
href: attrs.user.path,
title: attrs.user.name || attrs.user.username,
"data-auto-route": true,
},
},
this.attach("header-notifications", attrs)
);
},
},
dropdown
)
);
createWidget(
"header-dropdown",
Object.assign(
{
tagName: "li.header-dropdown-toggle",
html(attrs) {
const title = I18n.t(attrs.title);
const body = [iconNode(attrs.icon)];
if (attrs.contents) {
body.push(attrs.contents.call(this));
}
return h(
"a.icon.btn-flat",
{
attributes: {
"aria-expanded": attrs.active,
"aria-haspopup": true,
href: attrs.href,
"data-auto-route": true,
title,
"aria-label": title,
id: attrs.iconId,
},
},
body
);
},
},
dropdown
)
);
createWidget("header-icons", {
tagName: "ul.icons.d-header-icons",
html(attrs) {
if (this.siteSettings.login_required && !this.currentUser) {
return [];
}
const icons = [];
if (_extraHeaderIcons) {
_extraHeaderIcons.forEach((icon) => {
icons.push(this.attach(icon));
});
}
const search = this.attach("header-dropdown", {
title: "search.title",
icon: "search",
iconId: "search-button",
action: "toggleSearchMenu",
active: attrs.searchVisible,
href: getURL("/search"),
classNames: ["search-dropdown"],
});
icons.push(search);
const hamburger = this.attach("header-dropdown", {
title: "hamburger_menu",
icon: "bars",
iconId: "toggle-hamburger-menu",
active: attrs.hamburgerVisible,
action: "toggleHamburger",
href: "",
classNames: ["hamburger-dropdown"],
contents() {
let { currentUser } = this;
if (
currentUser?.reviewable_count &&
!this.currentUser.redesigned_user_menu_enabled
) {
return h(
"div.badge-notification.reviewables",
{
attributes: {
title: I18n.t("notifications.reviewable_items"),
},
},
this.currentUser.reviewable_count
);
}
},
});
if (
!this.siteSettings.enable_experimental_sidebar_hamburger ||
(this.siteSettings.enable_experimental_sidebar_hamburger &&
!attrs.sidebarEnabled) ||
this.site.mobileView
) {
icons.push(hamburger);
}
if (attrs.user) {
icons.push(
this.attach("user-dropdown", {
active: attrs.userVisible,
action: "toggleUserMenu",
ringBackdrop: attrs.ringBackdrop,
user: attrs.user,
})
);
}
return icons;
},
});
createWidget("header-buttons", {
tagName: "span.header-buttons",
html(attrs) {
if (this.currentUser) {
return;
}
const buttons = [];
if (attrs.canSignUp && !attrs.topic) {
buttons.push(
this.attach("button", {
label: "sign_up",
className: "btn-primary btn-small sign-up-button",
action: "showCreateAccount",
})
);
}
buttons.push(
this.attach("button", {
label: "log_in",
className: "btn-primary btn-small login-button",
action: "showLogin",
icon: "user",
})
);
return buttons;
},
});
createWidget("header-cloak", {
tagName: "div.header-cloak",
html() {
return "";
},
click() {},
scheduleRerender() {},
});
let additionalPanels = [];
export function attachAdditionalPanel(name, toggle, transformAttrs) {
additionalPanels.push({ name, toggle, transformAttrs });
}
createWidget("revamped-hamburger-menu-wrapper", {
buildAttributes() {
return { "data-click-outside": true };
},
html() {
return [
new RenderGlimmer(
this,
"div.widget-component-connector",
hbs`<Sidebar::HamburgerDropdown />`
),
];
},
clickOutside() {
this.sendWidgetAction("toggleHamburger");
},
});
createWidget("revamped-user-menu-wrapper", {
buildAttributes() {
return { "data-click-outside": true };
},
html() {
return [
new RenderGlimmer(
this,
"div.widget-component-connector",
hbs`<UserMenu::Menu />`
),
];
},
clickOutside() {
this.sendWidgetAction("toggleUserMenu");
},
});
export default createWidget("header", {
tagName: "header.d-header.clearfix",
buildKey: () => `header`,
services: ["router", "search"],
defaultState() {
let states = {
searchVisible: false,
hamburgerVisible: false,
userVisible: false,
ringBackdrop: true,
inTopicContext: false,
};
if (this.site.mobileView) {
states.skipSearchContext = true;
}
return states;
},
html(attrs, state) {
let inTopicRoute = false;
if (this.state.inTopicContext) {
inTopicRoute = this.router.currentRouteName.startsWith("topic.");
}
let contents = () => {
const headerIcons = this.attach("header-icons", {
hamburgerVisible: state.hamburgerVisible,
userVisible: state.userVisible,
searchVisible: state.searchVisible,
ringBackdrop: state.ringBackdrop,
flagCount: attrs.flagCount,
user: this.currentUser,
sidebarEnabled: attrs.sidebarEnabled,
});
if (attrs.onlyIcons) {
return headerIcons;
}
const panels = [this.attach("header-buttons", attrs), headerIcons];
if (state.searchVisible) {
panels.push(
this.attach("search-menu", {
inTopicContext: state.inTopicContext && inTopicRoute,
})
);
} else if (state.hamburgerVisible) {
if (this.siteSettings.enable_experimental_sidebar_hamburger) {
if (!attrs.sidebarEnabled) {
panels.push(this.attach("revamped-hamburger-menu-wrapper", {}));
}
} else {
panels.push(this.attach("hamburger-menu"));
}
} else if (state.userVisible) {
if (this.currentUser.redesigned_user_menu_enabled) {
panels.push(this.attach("revamped-user-menu-wrapper", {}));
} else {
panels.push(this.attach("user-menu"));
}
}
additionalPanels.map((panel) => {
if (this.state[panel.toggle]) {
panels.push(
this.attach(
panel.name,
panel.transformAttrs.call(this, attrs, state)
)
);
}
});
if (this.site.mobileView) {
panels.push(this.attach("header-cloak"));
}
return panels;
};
const contentsAttrs = {
contents,
minimized: !!attrs.topic,
};
return h(
"div.wrap",
this.attach("header-contents", Object.assign({}, attrs, contentsAttrs))
);
},
updateHighlight() {
if (!this.state.searchVisible) {
this.search.set("highlightTerm", "");
}
},
closeAll() {
this.state.userVisible = false;
this.state.hamburgerVisible = false;
this.state.searchVisible = false;
this.toggleBodyScrolling(false);
},
linkClickedEvent(attrs) {
let searchContextEnabled = false;
if (attrs) {
searchContextEnabled = attrs.searchContextEnabled;
const { searchLogId, searchResultId, searchResultType } = attrs;
if (searchLogId && searchResultId && searchResultType) {
logSearchLinkClick({ searchLogId, searchResultId, searchResultType });
}
}
if (!searchContextEnabled) {
this.closeAll();
}
this.updateHighlight();
},
toggleSearchMenu() {
if (this.site.mobileView) {
const context = this.search.searchContext;
let params = "";
if (context) {
params = `?context=${context.type}&context_id=${context.id}&skip_context=${this.state.skipSearchContext}`;
}
if (this.router.currentRouteName === "full-page-search") {
scrollTop();
$(".full-page-search").focus();
return false;
} else {
return DiscourseURL.routeTo("/search" + params);
}
}
this.state.searchVisible = !this.state.searchVisible;
this.updateHighlight();
if (this.state.searchVisible) {
this.focusSearchInput();
} else {
this.state.inTopicContext = false;
}
},
toggleUserMenu() {
if (this.currentUser.get("read_first_notification")) {
this.state.ringBackdrop = false;
}
this.state.userVisible = !this.state.userVisible;
this.toggleBodyScrolling(this.state.userVisible);
// auto focus on first button in dropdown
schedule("afterRender", () =>
document.querySelector(".user-menu button")?.focus()
);
},
toggleHamburger() {
if (
this.siteSettings.enable_experimental_sidebar_hamburger &&
this.attrs.sidebarEnabled
) {
this.sendWidgetAction("toggleSidebar");
} else {
this.state.hamburgerVisible = !this.state.hamburgerVisible;
this.toggleBodyScrolling(this.state.hamburgerVisible);
// auto focus on first link in dropdown
schedule("afterRender", () => {
document.querySelector(".hamburger-panel .menu-links a")?.focus();
});
}
},
toggleBodyScrolling(bool) {
if (!this.site.mobileView) {
return;
}
if (bool) {
document.body.addEventListener("touchmove", this.preventDefault, {
passive: false,
});
} else {
document.body.removeEventListener("touchmove", this.preventDefault, {
passive: false,
});
}
},
preventDefault(e) {
// prevent all scrolling on menu panels, except on overflow
const height = window.innerHeight ? window.innerHeight : $(window).height();
if (
!$(e.target).parents(".menu-panel").length ||
$(".menu-panel .panel-body-contents").height() <= height
) {
e.preventDefault();
}
},
togglePageSearch() {
const { state } = this;
state.inTopicContext = false;
let showSearch = this.router.currentRouteName.startsWith("topic.");
// If we're viewing a topic, only intercept search if there are cloaked posts
if (showSearch) {
const controller = this.register.lookup("controller:topic");
const total = controller.get("model.postStream.stream.length") || 0;
const chunkSize = controller.get("model.chunk_size") || 0;
showSearch =
total > chunkSize &&
$(".topic-post .cooked, .small-action:not(.time-gap)").length < total;
}
if (state.searchVisible) {
this.toggleSearchMenu();
return showSearch;
}
if (showSearch) {
state.inTopicContext = true;
this.toggleSearchMenu();
return false;
}
return true;
},
domClean() {
const { state } = this;
if (state.searchVisible || state.hamburgerVisible || state.userVisible) {
this.closeAll();
}
},
headerDismissFirstNotificationMask() {
// Dismiss notifications
if (document.body.classList.contains("unread-first-notification")) {
document.body.classList.remove("unread-first-notification");
}
this.store
.findStale(
"notification",
{
recent: true,
silent: this.get("currentUser.enforcedSecondFactor"),
limit: 5,
},
{ cacheKey: "recent-notifications" }
)
.refresh();
// Update UI
this.state.ringBackdrop = false;
this.scheduleRerender();
},
readLater() {
this.headerDismissFirstNotificationMask();
},
skipNewUserTips() {
this.headerDismissFirstNotificationMask();
ajax(userPath(this.currentUser.username_lower), {
type: "PUT",
data: {
skip_new_user_tips: true,
},
}).then(() => {
this.currentUser.set("skip_new_user_tips", true);
});
},
headerKeyboardTrigger(msg) {
switch (msg.type) {
case "search":
this.toggleSearchMenu();
break;
case "user":
this.toggleUserMenu();
break;
case "hamburger":
this.toggleHamburger();
break;
case "page-search":
if (!this.togglePageSearch()) {
msg.event.preventDefault();
msg.event.stopPropagation();
}
break;
}
},
focusSearchInput() {
if (this.state.searchVisible) {
schedule("afterRender", () => {
const searchInput = document.querySelector("#search-term");
searchInput.focus();
searchInput.select();
});
}
},
setTopicContext() {
this.state.inTopicContext = true;
this.focusSearchInput();
},
clearContext() {
this.state.inTopicContext = false;
this.focusSearchInput();
},
});