chromium/chrome/browser/resources/new_tab_page/modules/v2/calendar/calendar_event.ts

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_chip/cr_chip.js';

import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import type {PropertyValues} from 'chrome://resources/lit/v3_0/lit.rollup.js';

import type {CalendarEvent} from '../../../calendar_data.mojom-webui.js';
import {I18nMixinLit} from '../../../i18n_setup.js';
import {WindowProxy} from '../../../window_proxy.js';

import {getCss} from './calendar_event.css.js';
import {getHtml} from './calendar_event.html.js';
import {CalendarAction, recordCalendarAction, toJsTimestamp} from './common.js';

const kAttachmentScrollFadeBuffer: number = 4;
const kMillisecondsInMinute: number = 60000;
const kMinutesInHour: number = 60;

export interface CalendarEventElement {
  $: {
    header: HTMLAnchorElement,
    startTime: HTMLElement,
    timeStatus: HTMLElement,
    title: HTMLElement,
  };
}

const CalendarEventElementBase = I18nMixinLit(CrLitElement);

/**
 * The calendar event element for displaying a single event.
 */
export class CalendarEventElement extends CalendarEventElementBase {
  static get is() {
    return 'ntp-calendar-event';
  }

  static override get styles() {
    return getCss();
  }

  override render() {
    return getHtml.bind(this)();
  }

  static override get properties() {
    return {
      doubleBooked: {type: Boolean},
      event: {type: Object},

      expanded: {
        type: Boolean,
        reflect: true,
      },

      index: {type: Number},
      moduleName: {type: String},
      attachmentListClass_: {type: String},
      formattedStartTime_: {type: String},
      timeStatus_: {type: String},
    };
  }

  doubleBooked: boolean;
  event: CalendarEvent;
  expanded: boolean;
  index: number;
  moduleName: string;

  protected attachmentListClass_: string;
  protected formattedStartTime_: string;
  protected intersectionObserver_: IntersectionObserver;
  protected timeStatus_: string;

  override updated(changedProperties: PropertyValues<this>) {
    if ((changedProperties.has('event') || changedProperties.has('expanded')) &&
        (this.expanded && this.showAttachments_())) {
      const attachmentList = this.renderRoot.querySelector('#attachmentList');
      if (attachmentList && attachmentList.children.length > 1) {
        this.intersectionObserver_ =
            new IntersectionObserver(() => this.updateAttachmentListClass_(), {
              root: attachmentList,
              threshold: 1.0,
            });
        this.intersectionObserver_.observe(attachmentList.children[0]);
        this.intersectionObserver_.observe(
            attachmentList.children[attachmentList.children.length - 1]);
      }
    }
  }

  override willUpdate(changedProperties: PropertyValues<this>) {
    super.willUpdate(changedProperties);

    if (changedProperties.has('event')) {
      this.formattedStartTime_ = this.computeFormattedStartTime_();
    }

    if (changedProperties.has('event') || changedProperties.has('expanded')) {
      this.timeStatus_ = this.computeTimeStatus_();
    }
  }

  private computeFormattedStartTime_(): string {
    const offsetDate = toJsTimestamp(this.event.startTime);
    const dateObj = new Date(offsetDate);
    let timeStr =
        Intl.DateTimeFormat(undefined, {timeStyle: 'short'}).format(dateObj);
    // Remove extra spacing and make AM/PM lower case.
    timeStr = timeStr.replace(' AM', 'am').replace(' PM', 'pm');
    return timeStr;
  }

  private computeTimeStatus_(): string {
    if (!this.expanded) {
      return '';
    }

    // Start time of event in milliseconds since Windows epoch.
    const startTime = toJsTimestamp(this.event.startTime);
    // Current time in milliseconds since Windows epoch.
    const now = WindowProxy.getInstance().now().valueOf();

    const minutesUntilMeeting =
        Math.round((startTime - now) / kMillisecondsInMinute);
    if (minutesUntilMeeting <= 0) {
      return this.i18n('modulesCalendarInProgress');
    }

    if (minutesUntilMeeting < kMinutesInHour) {
      return this.i18n('modulesCalendarInXMin', minutesUntilMeeting.toString());
    }

    const hoursUntilMeeting = minutesUntilMeeting / kMinutesInHour;
    return this.i18n(
        'modulesCalendarInXHr', Math.round(hoursUntilMeeting).toString());
  }

  protected openAttachment_(e: Event) {
    this.dispatchEvent(new Event('usage', {composed: true, bubbles: true}));
    recordCalendarAction(CalendarAction.ATTACHMENT_CLICKED, this.moduleName);
    const currentTarget = e.currentTarget as HTMLElement;
    const index = Number(currentTarget.dataset['index']);
    WindowProxy.getInstance().navigate(
        this.event.attachments[index]!.resourceUrl.url);
  }

  protected openVideoConference_() {
    this.dispatchEvent(new Event('usage', {composed: true, bubbles: true}));
    recordCalendarAction(
        CalendarAction.CONFERENCE_CALL_CLICKED, this.moduleName);
    WindowProxy.getInstance().navigate(this.event.conferenceUrl!.url);
  }

  protected recordHeaderClick_() {
    this.dispatchEvent(new Event('usage', {composed: true, bubbles: true}));
    let action = CalendarAction.BASIC_EVENT_HEADER_CLICKED;
    if (this.expanded) {
      action = CalendarAction.EXPANDED_EVENT_HEADER_CLICKED;
    } else if (this.doubleBooked) {
      action = CalendarAction.DOUBLE_BOOKED_EVENT_HEADER_CLICKED;
    }
    recordCalendarAction(action, this.moduleName);
    chrome.metricsPrivate.recordSmallCount(
        `NewTabPage.${this.moduleName}.EventClickIndex`, this.index);
  }

  protected showConferenceButton_(): boolean {
    return !!this.event.conferenceUrl?.url;
  }

  protected showAttachments_(): boolean {
    return this.event.attachments.length > 0;
  }

  protected showLocation_(): boolean {
    return !!this.event.location;
  }

  protected updateAttachmentListClass_() {
    const attachmentList = this.renderRoot.querySelector('#attachmentList');
    if (!attachmentList) {
      this.attachmentListClass_ = '';
      return;
    }
    const scrollableRight =
        (attachmentList!.scrollWidth - attachmentList!.scrollLeft -
         kAttachmentScrollFadeBuffer) > attachmentList!.clientWidth;
    const scrollableLeft =
        attachmentList!.scrollLeft - kAttachmentScrollFadeBuffer > 0;

    if (scrollableRight && scrollableLeft) {
      this.attachmentListClass_ = 'scrollable';
    } else if (scrollableRight) {
      this.attachmentListClass_ = 'scrollable-right';
    } else if (scrollableLeft) {
      this.attachmentListClass_ = 'scrollable-left';
    } else {
      this.attachmentListClass_ = '';
    }
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'ntp-calendar-event': CalendarEventElement;
  }
}

customElements.define(CalendarEventElement.is, CalendarEventElement);