diff --git a/src/common/datetime/absolute_time.ts b/src/common/datetime/absolute_time.ts new file mode 100644 index 0000000000..df67e845df --- /dev/null +++ b/src/common/datetime/absolute_time.ts @@ -0,0 +1,23 @@ +import { isSameDay, isSameYear } from "date-fns"; +import { FrontendLocaleData } from "../../data/translation"; +import { + formatShortDateTime, + formatShortDateTimeWithYear, +} from "./format_date_time"; +import { formatTime } from "./format_time"; + +export const absoluteTime = ( + from: Date, + locale: FrontendLocaleData, + to?: Date +): string => { + const _to = to ?? new Date(); + + if (isSameDay(from, _to)) { + return formatTime(from, locale); + } + if (isSameYear(from, _to)) { + return formatShortDateTime(from, locale); + } + return formatShortDateTimeWithYear(from, locale); +}; diff --git a/src/common/datetime/format_date_time.ts b/src/common/datetime/format_date_time.ts index 0b6e1dcf8c..ba5b73e2e9 100644 --- a/src/common/datetime/format_date_time.ts +++ b/src/common/datetime/format_date_time.ts @@ -24,6 +24,29 @@ const formatDateTimeMem = memoizeOne( ) ); +// Aug 9, 2021, 8:23 AM +export const formatShortDateTimeWithYear = ( + dateObj: Date, + locale: FrontendLocaleData +) => formatShortDateTimeWithYearMem(locale).format(dateObj); + +const formatShortDateTimeWithYearMem = memoizeOne( + (locale: FrontendLocaleData) => + new Intl.DateTimeFormat( + locale.language === "en" && !useAmPm(locale) + ? "en-u-hc-h23" + : locale.language, + { + year: "numeric", + month: "short", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hour12: useAmPm(locale), + } + ) +); + // Aug 9, 8:23 AM export const formatShortDateTime = ( dateObj: Date, diff --git a/src/components/ha-absolute-time.ts b/src/components/ha-absolute-time.ts new file mode 100644 index 0000000000..f782b32317 --- /dev/null +++ b/src/components/ha-absolute-time.ts @@ -0,0 +1,76 @@ +import { addDays, differenceInMilliseconds, startOfDay } from "date-fns"; +import { PropertyValues, ReactiveElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { absoluteTime } from "../common/datetime/absolute_time"; +import type { HomeAssistant } from "../types"; + +const SAFE_MARGIN = 5 * 1000; + +@customElement("ha-absolute-time") +class HaAbsoluteTime extends ReactiveElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public datetime?: string | Date; + + private _timeout?: number; + + public disconnectedCallback(): void { + super.disconnectedCallback(); + this._clearTimeout(); + } + + public connectedCallback(): void { + super.connectedCallback(); + if (this.datetime) { + this._updateNextDay(); + } + } + + protected createRenderRoot() { + return this; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this._updateAbsolute(); + } + + protected update(changedProps: PropertyValues) { + super.update(changedProps); + this._updateAbsolute(); + } + + private _clearTimeout(): void { + if (this._timeout) { + window.clearTimeout(this._timeout); + this._timeout = undefined; + } + } + + private _updateNextDay(): void { + this._clearTimeout(); + + const now = new Date(); + const nextDay = addDays(startOfDay(now), 1); + const ms = differenceInMilliseconds(nextDay, now) + SAFE_MARGIN; + + this._timeout = window.setTimeout(() => { + this._updateNextDay(); + this._updateAbsolute(); + }, ms); + } + + private _updateAbsolute(): void { + if (!this.datetime) { + this.innerHTML = this.hass.localize("ui.components.absolute_time.never"); + } else { + this.innerHTML = absoluteTime(new Date(this.datetime), this.hass.locale); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-absolute-time": HaAbsoluteTime; + } +} diff --git a/src/dialogs/more-info/components/ha-more-info-state-header.ts b/src/dialogs/more-info/components/ha-more-info-state-header.ts index 8ba2ceb8bb..1e7430cb23 100644 --- a/src/dialogs/more-info/components/ha-more-info-state-header.ts +++ b/src/dialogs/more-info/components/ha-more-info-state-header.ts @@ -1,7 +1,9 @@ import { HassEntity } from "home-assistant-js-websocket"; -import { html, LitElement, TemplateResult, css, CSSResultGroup } from "lit"; -import { customElement, property } from "lit/decorators"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; import { computeStateDisplay } from "../../../common/entity/compute_state_display"; +import "../../../components/ha-absolute-time"; +import "../../../components/ha-relative-time"; import { isUnavailableState } from "../../../data/entity"; import { LightEntity } from "../../../data/light"; import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor"; @@ -16,6 +18,8 @@ export class HaMoreInfoStateHeader extends LitElement { @property({ attribute: false }) public stateOverride?: string; + @state() private _absoluteTime = false; + private _computeStateDisplay(stateObj: HassEntity): TemplateResult | string { if ( stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP && @@ -41,15 +45,32 @@ export class HaMoreInfoStateHeader extends LitElement { return stateDisplay; } - protected render(): TemplateResult { - const name = this.stateObj.attributes.friendly_name; + private _toggleAbsolute() { + this._absoluteTime = !this._absoluteTime; + } + protected render(): TemplateResult { const stateDisplay = this.stateOverride ?? this._computeStateDisplay(this.stateObj); return html` -
${name}
${stateDisplay}
+
+ ${this._absoluteTime
+ ? html`
+