Move notifications to the sidebar (#3317)

* Move notifications to the sidebar

* Close when navigating

* Lint
This commit is contained in:
Paulus Schoutsen 2019-06-28 14:23:29 -07:00 committed by GitHub
parent 58e6be12af
commit 42e75e7cdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 277 additions and 276 deletions

View File

@ -21,6 +21,11 @@ import {
getExternalConfig,
ExternalConfig,
} from "../external_app/external_config";
import {
PersistentNotification,
subscribeNotifications,
} from "../data/persistent_notification";
import computeDomain from "../common/entity/compute_domain";
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
@ -102,12 +107,14 @@ const renderPanel = (hass, panel) => html`
* @appliesMixin LocalizeMixin
*/
class HaSidebar extends LitElement {
@property() public hass?: HomeAssistant;
@property() public hass!: HomeAssistant;
@property({ type: Boolean }) public alwaysExpand = false;
@property({ type: Boolean, reflect: true }) public expanded = false;
@property() public _defaultPage?: string =
localStorage.defaultPage || DEFAULT_PANEL;
@property() private _externalConfig?: ExternalConfig;
@property() private _notifications?: PersistentNotification[];
protected render() {
const hass = this.hass;
@ -118,6 +125,15 @@ class HaSidebar extends LitElement {
const [beforeSpacer, afterSpacer] = computePanels(hass);
let notificationCount = this._notifications
? this._notifications.length
: 0;
for (const entityId in hass.states) {
if (computeDomain(entityId) === "configurator") {
notificationCount++;
}
}
return html`
${this.expanded
? html`
@ -167,57 +183,60 @@ class HaSidebar extends LitElement {
slot="item-icon"
icon="hass:cellphone-settings-variant"
></ha-icon>
<span class="item-text"
>${hass.localize(
"ui.sidebar.external_app_configuration"
)}</span
>
</paper-icon-item>
</a>
`
: ""}
${hass.user
? html`
<a
href="/profile"
data-panel="panel"
tabindex="-1"
aria-role="option"
aria-label=${hass.localize("panel.profile")}
>
<paper-icon-item class="profile">
<ha-user-badge
slot="item-icon"
.user=${hass.user}
></ha-user-badge>
<span class="item-text">
${hass.user.name}
${hass.localize("ui.sidebar.external_app_configuration")}
</span>
</paper-icon-item>
</a>
`
: html`
<paper-icon-item
@click=${this._handleLogOut}
class="logout"
aria-role="option"
>
<ha-icon slot="item-icon" icon="hass:exit-to-app"></ha-icon>
<span class="item-text"
>${hass.localize("ui.sidebar.log_out")}</span
>
</paper-icon-item>
`}
: ""}
<div disabled class="divider sticky-el"></div>
<paper-icon-item
class="notifications sticky-el"
aria-role="option"
@click=${this._handleShowNotificationDrawer}
>
<ha-icon slot="item-icon" icon="hass:bell"></ha-icon>
${notificationCount > 0
? html`
<span class="notification-badge" slot="item-icon">
${notificationCount}
</span>
`
: ""}
<span class="item-text">
${hass.localize("ui.notification_drawer.title")}
</span>
</paper-icon-item>
<a
class="profile sticky-el"
href="/profile"
data-panel="panel"
tabindex="-1"
aria-role="option"
aria-label=${hass.localize("panel.profile")}
>
<paper-icon-item>
<ha-user-badge slot="item-icon" .user=${hass.user}></ha-user-badge>
<span class="item-text">
${hass.user ? hass.user.name : ""}
</span>
</paper-icon-item>
</a>
</paper-listbox>
`;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (
changedProps.has("_externalConfig") ||
changedProps.has("expanded") ||
changedProps.has("alwaysExpand")
changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_notifications")
) {
return true;
}
@ -233,7 +252,8 @@ class HaSidebar extends LitElement {
hass.panels !== oldHass.panels ||
hass.panelUrl !== oldHass.panelUrl ||
hass.user !== oldHass.user ||
hass.localize !== oldHass.localize
hass.localize !== oldHass.localize ||
hass.states !== oldHass.states
);
}
@ -253,6 +273,17 @@ class HaSidebar extends LitElement {
this.addEventListener("mouseleave", () => {
this._contract();
});
subscribeNotifications(this.hass.connection, (notifications) => {
this._notifications = notifications;
});
// Deal with configurator
// private _updateNotifications(
// states: HassEntities,
// persistent: unknown[]
// ): unknown[] {
// const configurator = computeNotifications(states);
// return persistent.concat(configurator);
// }
}
protected updated(changedProps) {
@ -266,13 +297,13 @@ class HaSidebar extends LitElement {
this.expanded = this.alwaysExpand || false;
}
private _handleLogOut() {
fireEvent(this, "hass-logout");
private _handleShowNotificationDrawer() {
fireEvent(this, "hass-show-notifications");
}
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass!.auth.external!.fireMessage({
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
@ -386,24 +417,64 @@ class HaSidebar extends LitElement {
color: var(--sidebar-selected-text-color);
}
a .item-text {
paper-icon-item .item-text {
display: none;
}
:host([expanded]) a .item-text {
:host([expanded]) paper-icon-item .item-text {
display: block;
}
paper-icon-item.logout {
margin-top: 16px;
.divider {
bottom: 88px;
padding: 10px 0;
}
paper-icon-item.profile {
.divider::before {
content: " ";
display: block;
height: 1px;
background-color: var(--divider-color);
}
.notifications {
margin-top: 0;
margin-bottom: 0;
bottom: 48px;
cursor: pointer;
}
.profile {
bottom: 0;
}
.profile paper-icon-item {
padding-left: 4px;
}
.profile .item-text {
margin-left: 8px;
}
.sticky-el {
position: sticky;
background-color: var(
--sidebar-background-color,
var(--primary-background-color)
);
}
.notification-badge {
position: absolute;
font-weight: 400;
bottom: 14px;
left: 26px;
border-radius: 50%;
background-color: var(--primary-color);
height: 20px;
line-height: 20px;
text-align: center;
padding: 0px 6px;
font-size: 0.65em;
color: var(--text-primary-color);
}
.spacer {
flex: 1;
pointer-events: none;

View File

@ -0,0 +1,43 @@
import {
createCollection,
Connection,
HassEntity,
} from "home-assistant-js-websocket";
export interface PersitentNotificationEntity extends HassEntity {
notification_id?: string;
created_at?: string;
title?: string;
message?: string;
}
export interface PersistentNotification {
created_at: string;
message: string;
notification_id: string;
title: string;
status: "read" | "unread";
}
const fetchNotifications = (conn) =>
conn.sendMessagePromise({
type: "persistent_notification/get",
});
const subscribeUpdates = (conn, store) =>
conn.subscribeEvents(
() => fetchNotifications(conn).then((ntf) => store.setState(ntf, true)),
"persistent_notifications_updated"
);
export const subscribeNotifications = (
conn: Connection,
onChange: (notifications: PersistentNotification[]) => void
) =>
createCollection<PersistentNotification[]>(
"_ntf",
fetchNotifications,
subscribeUpdates,
conn,
onChange
);

View File

@ -1,24 +0,0 @@
import { createCollection, Connection } from "home-assistant-js-websocket";
const fetchNotifications = (conn) =>
conn.sendMessagePromise({
type: "persistent_notification/get",
});
const subscribeUpdates = (conn, store) =>
conn.subscribeEvents(
() => fetchNotifications(conn).then((ntf) => store.setState(ntf, true)),
"persistent_notifications_updated"
);
export const subscribeNotifications = (
conn: Connection,
onChange: (notifications: Notification[]) => void
) =>
createCollection<Notification[]>(
"_ntf",
fetchNotifications,
subscribeUpdates,
conn,
onChange
);

View File

@ -7,17 +7,17 @@ import {
} from "lit-element";
import "@material/mwc-button";
import "./hui-notification-item-template";
import "./notification-item-template";
import { HomeAssistant } from "../../../../types";
import { fireEvent } from "../../../../common/dom/fire_event";
import { HassNotification } from "./types";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import { PersitentNotificationEntity } from "../../data/persistent_notification";
@customElement("hui-configurator-notification-item")
@customElement("configurator-notification-item")
export class HuiConfiguratorNotificationItem extends LitElement {
@property() public hass?: HomeAssistant;
@property() public notification?: HassNotification;
@property() public notification?: PersitentNotificationEntity;
protected render(): TemplateResult | void {
if (!this.hass || !this.notification) {
@ -25,7 +25,7 @@ export class HuiConfiguratorNotificationItem extends LitElement {
}
return html`
<hui-notification-item-template>
<notification-item-template>
<span slot="header">${this.hass.localize("domain.configurator")}</span>
<div>
@ -41,7 +41,7 @@ export class HuiConfiguratorNotificationItem extends LitElement {
`state.configurator.${this.notification.state}`
)}</mwc-button
>
</hui-notification-item-template>
</notification-item-template>
`;
}
@ -54,6 +54,6 @@ export class HuiConfiguratorNotificationItem extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"hui-configurator-notification-item": HuiConfiguratorNotificationItem;
"configurator-notification-item": HuiConfiguratorNotificationItem;
}
}

View File

@ -5,13 +5,14 @@ import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./hui-notification-item";
import "../../../../components/ha-paper-icon-button-next";
import { EventsMixin } from "../../../../mixins/events-mixin";
import LocalizeMixin from "../../../../mixins/localize-mixin";
import { computeRTL } from "../../../../common/util/compute_rtl";
import "./notification-item";
import "../../components/ha-paper-icon-button-next";
import { EventsMixin } from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin";
import { computeRTL } from "../../common/util/compute_rtl";
import { subscribeNotifications } from "../../data/persistent_notification";
import computeDomain from "../../common/entity/compute_domain";
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
@ -129,7 +130,7 @@ export class HuiNotificationDrawer extends EventsMixin(
<dom-repeat items="[[notifications]]">
<template>
<div class="notification">
<hui-notification-item hass="[[hass]]" notification="[[item]]"></hui-notification-item>
<notification-item hass="[[hass]]" notification="[[item]]"></notification-item>
</div>
</template>
</dom-repeat>
@ -160,6 +161,10 @@ export class HuiNotificationDrawer extends EventsMixin(
reflectToAttribute: true,
},
notifications: {
type: Array,
computed: "_computeNotifications(open, hass, _notificationsBackend)",
},
_notificationsBackend: {
type: Array,
value: [],
},
@ -171,6 +176,16 @@ export class HuiNotificationDrawer extends EventsMixin(
};
}
ready() {
super.ready();
window.addEventListener("location-changed", () => {
// close drawer when we navigate away.
if (this.open) {
this.open = false;
}
});
}
_closeDrawer(ev) {
ev.stopPropagation();
this.open = false;
@ -188,17 +203,44 @@ export class HuiNotificationDrawer extends EventsMixin(
this._openTimer = setTimeout(() => {
this.classList.add("open");
}, 50);
this._unsubNotifications = subscribeNotifications(
this.hass.connection,
(notifications) => {
this._notificationsBackend = notifications;
}
);
} else {
// Animate closed then hide
this.classList.remove("open");
this._openTimer = setTimeout(() => {
this.hidden = true;
}, 250);
if (this._unsubNotifications) {
this._unsubNotifications();
this._unsubNotifications = undefined;
}
}
}
_computeRTL(hass) {
return computeRTL(hass);
}
_computeNotifications(open, hass, notificationsBackend) {
if (!open) {
return [];
}
const configuratorEntities = Object.keys(hass.states)
.filter((entityId) => computeDomain(entityId) === "configurator")
.map((entityId) => hass.states[entityId]);
return notificationsBackend.concat(configuratorEntities);
}
showDialog({ narrow }) {
this.open = true;
this.narrow = narrow;
}
}
customElements.define("hui-notification-drawer", HuiNotificationDrawer);
customElements.define("notification-drawer", HuiNotificationDrawer);

View File

@ -7,9 +7,9 @@ import {
CSSResult,
} from "lit-element";
import "../../../../components/ha-card";
import "../../components/ha-card";
@customElement("hui-notification-item-template")
@customElement("notification-item-template")
export class HuiNotificationItemTemplate extends LitElement {
protected render(): TemplateResult | void {
return html`
@ -60,6 +60,6 @@ export class HuiNotificationItemTemplate extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"hui-notification-item-template": HuiNotificationItemTemplate;
"notification-item-template": HuiNotificationItemTemplate;
}
}

View File

@ -6,18 +6,19 @@ import {
TemplateResult,
html,
} from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import "./hui-configurator-notification-item";
import "./hui-persistent-notification-item";
import "./configurator-notification-item";
import "./persistent-notification-item";
import { HomeAssistant } from "../../../../types";
import { HassNotification } from "./types";
import { HomeAssistant } from "../../types";
import { PersistentNotification } from "../../data/persistent_notification";
@customElement("hui-notification-item")
@customElement("notification-item")
export class HuiNotificationItem extends LitElement {
@property() public hass?: HomeAssistant;
@property() public notification?: HassNotification;
@property() public notification?: HassEntity | PersistentNotification;
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (!this.hass || !this.notification || changedProps.has("notification")) {
@ -32,24 +33,24 @@ export class HuiNotificationItem extends LitElement {
return html``;
}
return this.notification.entity_id
return "entity_id" in this.notification
? html`
<hui-configurator-notification-item
<configurator-notification-item
.hass="${this.hass}"
.notification="${this.notification}"
></hui-configurator-notification-item>
></configurator-notification-item>
`
: html`
<hui-persistent-notification-item
<persistent-notification-item
.hass="${this.hass}"
.notification="${this.notification}"
></hui-persistent-notification-item>
></persistent-notification-item>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-notification-item": HuiNotificationItem;
"notification-item": HuiNotificationItem;
}
}

View File

@ -10,18 +10,18 @@ import {
import "@material/mwc-button";
import "@polymer/paper-tooltip/paper-tooltip";
import "../../../../components/ha-relative-time";
import "../../../../components/ha-markdown";
import "./hui-notification-item-template";
import "../../components/ha-relative-time";
import "../../components/ha-markdown";
import "./notification-item-template";
import { HomeAssistant } from "../../../../types";
import { HassNotification } from "./types";
import { HomeAssistant } from "../../types";
import { PersistentNotification } from "../../data/persistent_notification";
@customElement("hui-persistent-notification-item")
@customElement("persistent-notification-item")
export class HuiPersistentNotificationItem extends LitElement {
@property() public hass?: HomeAssistant;
@property() public notification?: HassNotification;
@property() public notification?: PersistentNotification;
protected render(): TemplateResult | void {
if (!this.hass || !this.notification) {
@ -29,8 +29,10 @@ export class HuiPersistentNotificationItem extends LitElement {
}
return html`
<hui-notification-item-template>
<span slot="header">${this._computeTitle(this.notification)}</span>
<notification-item-template>
<span slot="header">
${this.notification.title || this.notification.notification_id}
</span>
<ha-markdown content="${this.notification.message}"></ha-markdown>
@ -54,7 +56,7 @@ export class HuiPersistentNotificationItem extends LitElement {
"ui.card.persistent_notification.dismiss"
)}</mwc-button
>
</hui-notification-item-template>
</notification-item-template>
`;
}
@ -80,13 +82,9 @@ export class HuiPersistentNotificationItem extends LitElement {
});
}
private _computeTitle(notification: HassNotification): string | undefined {
return notification.title || notification.notification_id;
}
private _computeTooltip(
hass: HomeAssistant,
notification: HassNotification
notification: PersistentNotification
): string | undefined {
if (!hass || !notification) {
return undefined;
@ -105,6 +103,6 @@ export class HuiPersistentNotificationItem extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"hui-persistent-notification-item": HuiPersistentNotificationItem;
"persistent-notification-item": HuiPersistentNotificationItem;
}
}

View File

@ -0,0 +1,16 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface NotificationDrawerParams {
narrow: boolean;
}
export const showNotificationDrawer = (
element: HTMLElement,
dialogParams: NotificationDrawerParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "notification-drawer" as any, // Not in TS yet
dialogImport: () => import("./notification-drawer"),
dialogParams,
});
};

View File

@ -20,6 +20,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types";
// tslint:disable-next-line: no-duplicate-imports
import { AppDrawerLayoutElement } from "@polymer/app-layout/app-drawer-layout/app-drawer-layout";
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
const NON_SWIPABLE_PANELS = ["kiosk", "map"];
@ -27,6 +28,7 @@ declare global {
// for fire event
interface HASSDomEvents {
"hass-toggle-menu": undefined;
"hass-show-notifications": undefined;
}
}
@ -66,6 +68,7 @@ class HomeAssistantMain extends LitElement {
>
<ha-sidebar
.hass=${hass}
.narrow=${this.narrow}
.alwaysExpand=${this.narrow || hass.dockedSidebar}
></ha-sidebar>
</app-drawer>
@ -96,6 +99,12 @@ class HomeAssistantMain extends LitElement {
setTimeout(() => this.appLayout.resetLayout());
}
});
this.addEventListener("hass-show-notifications", () => {
showNotificationDrawer(this, {
narrow: this.narrow!,
});
});
}
protected updated(changedProps: PropertyValues) {

View File

@ -1,9 +0,0 @@
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
import computeDomain from "../../../common/entity/compute_domain";
export const computeNotifications = (states: HassEntities): HassEntity[] => {
return Object.keys(states)
.filter((entityId) => computeDomain(entityId) === "configurator")
.map((entityId) => states[entityId]);
};

View File

@ -1,81 +0,0 @@
import {
html,
LitElement,
TemplateResult,
css,
CSSResult,
property,
} from "lit-element";
import "@polymer/paper-icon-button/paper-icon-button";
import { fireEvent } from "../../../../common/dom/fire_event";
declare global {
// tslint:disable-next-line
interface HASSDomEvents {
"opened-changed": { value: boolean };
}
}
class HuiNotificationsButton extends LitElement {
@property() public notifications?: string[];
@property() public opened?: boolean;
protected render(): TemplateResult | void {
return html`
<paper-icon-button
aria-label="Show Notifications"
icon="hass:bell"
@click="${this._clicked}"
></paper-icon-button>
${this.notifications && this.notifications.length > 0
? html`
<span class="indicator">
<div>${this.notifications.length}</div>
</span>
`
: ""}
`;
}
static get styles(): CSSResult[] {
return [
css`
:host {
position: relative;
}
.indicator {
position: absolute;
top: 0px;
right: -3px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--accent-color);
pointer-events: none;
z-index: 1;
}
.indicator > div {
right: 7px;
top: 3px;
position: absolute;
font-size: 0.55em;
}
`,
];
}
private _clicked() {
this.opened = true;
fireEvent(this, "opened-changed", { value: this.opened });
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-notifications-button": HuiNotificationsButton;
}
}
customElements.define("hui-notifications-button", HuiNotificationsButton);

View File

@ -1,8 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
export declare type HassNotification = HassEntity & {
notification_id?: string;
created_at?: string;
title?: string;
message?: string;
};

View File

@ -20,7 +20,6 @@ import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-menu-button/paper-menu-button";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import { HassEntities } from "home-assistant-js-websocket";
import scrollToTarget from "../../common/dom/scroll-to-target";
@ -30,17 +29,13 @@ import "../../components/ha-paper-icon-button-arrow-next";
import "../../components/ha-paper-icon-button-arrow-prev";
import "../../components/ha-icon";
import { loadModule, loadCSS, loadJS } from "../../common/dom/load_resource";
import { subscribeNotifications } from "../../data/ws-notifications";
import { debounce } from "../../common/util/debounce";
import { HomeAssistant } from "../../types";
import { LovelaceConfig } from "../../data/lovelace";
import { navigate } from "../../common/navigate";
import { fireEvent } from "../../common/dom/fire_event";
import { computeNotifications } from "./common/compute-notifications";
import { swapView } from "./editor/config-util";
import "./components/notifications/hui-notification-drawer";
import "./components/notifications/hui-notifications-button";
import "./hui-view";
// Not a duplicate import, this one is for type
// tslint:disable-next-line
@ -65,12 +60,9 @@ class HUIRoot extends LitElement {
@property() public route?: { path: string; prefix: string };
@property() private _routeData?: { view: string };
@property() private _curView?: number | "hass-unused-entities";
@property() private _notificationsOpen = false;
@property() private _persistentNotifications?: Notification[];
private _viewCache?: { [viewId: string]: HUIView };
private _debouncedConfigChanged: () => void;
private _unsubNotifications?: () => void;
constructor() {
super();
@ -83,35 +75,11 @@ class HUIRoot extends LitElement {
);
}
public connectedCallback(): void {
super.connectedCallback();
this._unsubNotifications = subscribeNotifications(
this.hass!.connection,
(notifications) => {
this._persistentNotifications = notifications;
}
);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
if (this._unsubNotifications) {
this._unsubNotifications();
}
}
protected render(): TemplateResult | void {
return html`
<app-route .route="${this.route}" pattern="/:view" data="${
this._routeData
}" @data-changed="${this._routeDataChanged}"></app-route>
<hui-notification-drawer
.hass="${this.hass}"
.notifications="${this._notifications}"
.open="${this._notificationsOpen}"
@open-changed="${this._handleNotificationsOpenChanged}"
.narrow="${this.narrow}"
></hui-notification-drawer>
<ha-app-layout id="layout">
<app-header slot="header" effects="waterfall" class="${classMap({
"edit-mode": this._editMode,
@ -165,12 +133,6 @@ class HUIRoot extends LitElement {
<app-toolbar>
<ha-menu-button></ha-menu-button>
<div main-title>${this.config.title || "Home Assistant"}</div>
<hui-notifications-button
.hass="${this.hass}"
.opened="${this._notificationsOpen}"
@opened-changed="${this._handleNotificationsOpenChanged}"
.notifications="${this._notifications}"
></hui-notifications-button>
<ha-start-voice-button
.hass="${this.hass}"
></ha-start-voice-button>
@ -449,13 +411,6 @@ class HUIRoot extends LitElement {
}
}
private get _notifications() {
return this._updateNotifications(
this.hass!.states,
this._persistentNotifications! || []
);
}
private get config(): LovelaceConfig {
return this.lovelace!.config;
}
@ -480,18 +435,6 @@ class HUIRoot extends LitElement {
this._routeData = ev.detail.value;
}
private _handleNotificationsOpenChanged(ev): void {
this._notificationsOpen = ev.detail.value;
}
private _updateNotifications(
states: HassEntities,
persistent: unknown[]
): unknown[] {
const configurator = computeNotifications(states);
return persistent.concat(configurator);
}
private _handleRefresh(): void {
fireEvent(this, "config-refresh");
}