chromium/chrome/browser/resources/settings/performance_page/tab_discard/exception_current_sites_list.ts

// Copyright 2023 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/polymer/v3_0/iron-list/iron-list.js';
import '../../controls/settings_checkbox_list_entry.js';
import '../../settings_shared.css.js';
import '../../site_favicon.js';

import type {PrefsMixinInterface} from '/shared/settings/prefs/prefs_mixin.js';
import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js';
import type {ListPropertyUpdateMixinInterface} from 'chrome://resources/cr_elements/list_property_update_mixin.js';
import {ListPropertyUpdateMixin} from 'chrome://resources/cr_elements/list_property_update_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import type {IronListElement} from 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import type {ScrollableMixinInterface} from '../../scrollable_mixin.js';
import {ScrollableMixin} from '../../scrollable_mixin.js';
import {convertDateToWindowsEpoch} from '../../time.js';
import type {PerformanceBrowserProxy} from '../performance_browser_proxy.js';
import {PerformanceBrowserProxyImpl} from '../performance_browser_proxy.js';
import type {PerformanceMetricsProxy} from '../performance_metrics_proxy.js';
import {MemorySaverModeExceptionListAction, PerformanceMetricsProxyImpl} from '../performance_metrics_proxy.js';

import {getTemplate} from './exception_current_sites_list.html.js';
import {TAB_DISCARD_EXCEPTIONS_PREF} from './exception_validation_mixin.js';

export interface ExceptionCurrentSitesListElement {
  $: {
    list: IronListElement,
  };
}

type Site = string;

type Constructor<T> = new (...args: any[]) => T;
const ExceptionCurrentSitesListElementBase =
    ListPropertyUpdateMixin(ScrollableMixin(PrefsMixin(PolymerElement))) as
    Constructor<ListPropertyUpdateMixinInterface&ScrollableMixinInterface&
                PrefsMixinInterface&PolymerElement>;

export class ExceptionCurrentSitesListElement extends
    ExceptionCurrentSitesListElementBase {
  static get is() {
    return 'tab-discard-exception-current-sites-list';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      currentSites_: {type: Array, value: []},

      selectedSites_: {
        type: Array,
        value() {
          return new Set();
        },
      },

      submitDisabled: {
        type: Boolean,
        notify: true,
      },

      updateIntervalMS_: {
        type: Number,
        value: 1000,
      },

      // whether the current sites list is visible according to its parent
      visible: {
        type: Boolean,
        value: true,
        observer: 'onVisibilityChanged_',
      },
    };
  }

  private browserProxy_: PerformanceBrowserProxy =
      PerformanceBrowserProxyImpl.getInstance();
  private metricsProxy_: PerformanceMetricsProxy =
      PerformanceMetricsProxyImpl.getInstance();

  private currentSites_: Site[];
  private selectedSites_: Set<Site>;
  private submitDisabled: boolean;
  private updateIntervalMS_: number;
  visible: boolean;

  private onVisibilityChangedListener_: () => void;
  private updateIntervalID_: number|undefined = undefined;

  override async connectedCallback() {
    super.connectedCallback();

    await this.updateCurrentSites_();
    this.dispatchEvent(new CustomEvent('sites-populated', {
      detail: {length: this.currentSites_.length},
    }));

    this.onVisibilityChanged_();
    this.onVisibilityChangedListener_ = this.onVisibilityChanged_.bind(this);
    document.addEventListener(
        'visibilitychange', this.onVisibilityChangedListener_);
  }

  override disconnectedCallback() {
    document.removeEventListener(
        'visibilitychange', this.onVisibilityChangedListener_);
    this.stopUpdatingCurrentSites_();
  }

  // Notifies the iron-list child that it should resize (generally because this
  // element's visibility has changed).
  notifyResize() {
    this.$.list.notifyResize();
  }

  private onVisibilityChanged_() {
    if (this.visible && document.visibilityState === 'visible') {
      this.startUpdatingCurrentSites_();
    } else {
      this.stopUpdatingCurrentSites_();
    }
  }

  private startUpdatingCurrentSites_() {
    this.updateCurrentSites_().then(() => {
      if (this.updateIntervalID_ === undefined) {
        this.updateIntervalID_ = setInterval(
            this.updateCurrentSites_.bind(this), this.updateIntervalMS_);
      }
    });
  }

  private stopUpdatingCurrentSites_() {
    if (this.updateIntervalID_ !== undefined) {
      clearInterval(this.updateIntervalID_);
      this.updateIntervalID_ = undefined;
    }
  }

  setUpdateIntervalForTesting(updateIntervalMS: number) {
    this.updateIntervalMS_ = updateIntervalMS;
    this.stopUpdatingCurrentSites_();
    this.startUpdatingCurrentSites_();
  }

  getIsUpdatingForTesting() {
    return this.updateIntervalID_ !== undefined;
  }

  private async updateCurrentSites_() {
    const existingSites =
        new Set(Object.keys(this.getPref(TAB_DISCARD_EXCEPTIONS_PREF).value));
    const currentSites = (await this.browserProxy_.getCurrentOpenSites())
                             .filter(rule => !existingSites.has(rule));

    // Remove sites from selected set that are no longer in the list.
    this.selectedSites_ =
        new Set(currentSites.filter(this.isSelectedSite_.bind(this)));
    this.computeSubmitDisabled_();

    this.updateList('currentSites_', x => x, currentSites);
    if (this.currentSites_.length) {
      this.updateScrollableContents();
    }
  }

  private computeSubmitDisabled_() {
    this.submitDisabled = !this.selectedSites_.size;
  }

  // Convert iron-list index (0-indexed) to aria-posinset (1-indexed).
  private getAriaPosinset_(index: number): number {
    return index + 1;
  }

  // Called to recalculate checked status of entries when the site changes due
  // to list updates.
  private isSelectedSite_(site: Site) {
    return this.selectedSites_.has(site);
  }

  private onToggleSelection_(e: {model: {item: Site}, detail: boolean}) {
    if (e.detail) {
      this.selectedSites_.add(e.model.item);
    } else {
      this.selectedSites_.delete(e.model.item);
    }
    this.computeSubmitDisabled_();
  }

  submit() {
    assert(!this.submitDisabled);
    this.selectedSites_.forEach(rule => {
      this.setPrefDictEntry(
          TAB_DISCARD_EXCEPTIONS_PREF, rule, convertDateToWindowsEpoch());
    });
    this.metricsProxy_.recordExceptionListAction(
        MemorySaverModeExceptionListAction.ADD_FROM_CURRENT);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'tab-discard-exception-current-sites-list':
        ExceptionCurrentSitesListElement;
  }
}

customElements.define(
    ExceptionCurrentSitesListElement.is,
    ExceptionCurrentSitesListElement);