chromium/ash/webui/personalization_app/resources/js/user/user_selectors.ts

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert} from 'chrome://resources/js/assert.js';
import {BigBuffer} from 'chrome://resources/mojo/mojo/public/mojom/base/big_buffer.mojom-webui.js';
import {Url} from 'chrome://resources/mojo/url/mojom/url.mojom-webui.js';

import {UserImage} from '../../personalization_app.mojom-webui.js';
import {PersonalizationState} from '../personalization_state.js';

import {AVATAR_PLACEHOLDER_URL} from './utils.js';

/**
 * @fileoverview Utility functions to derive a user image URL to display from
 * |PersonalizationState|. Results are cached when necessary to avoid expensive
 * re-computation.
 */

/**
 * Transforming a |BigBuffer| to a blob url is expensive. Cache results by
 * reference to a |BigBuffer|. Use |WeakMap| so that old |BigBuffer| objects
 * that are no longer stored in |PersonalizationState| can be garbage collected.
 */
const objectUrlCache = new WeakMap<BigBuffer, Url>();

function bufferToPngObjectUrl(value: BigBuffer): Url|null {
  if (value.invalidBuffer) {
    console.error('Invalid buffer received');
    return null;
  }

  if (objectUrlCache.has(value)) {
    return objectUrlCache.get(value)!;
  }

  try {
    let bytes: Uint8Array;
    if (Array.isArray(value.bytes)) {
      bytes = new Uint8Array(value.bytes);
    } else {
      assert(!!value.sharedMemory, 'sharedMemory must be defined here');
      const sharedMemory = value.sharedMemory!;
      const {buffer, result} =
          sharedMemory.bufferHandle.mapBuffer(0, sharedMemory.size);
      assert(result === Mojo.RESULT_OK, 'Could not map buffer');
      bytes = new Uint8Array(buffer);
    }

    const result = {
      url: URL.createObjectURL(new Blob([bytes], {type: 'image/png'})),
    };
    objectUrlCache.set(value, result);
    return result;

  } catch (e) {
    console.error('Unable to create blob from image data', e);
    return null;
  }
}

/**
 * The placeholder url is used as the user image url for invalid or unknown
 * urls.
 */
const placeHolderUrl = {
  url: AVATAR_PLACEHOLDER_URL,
};

/**
 * Derive a user image |Url| from |PersonalizationState|. Return a |Url| rather
 * than |string| to avoid copies on potentially large data URLs.
 */
export function selectUserImageUrl(state: PersonalizationState): Url|null {
  const userImage = state.user.image;

  if (!userImage) {
    return null;
  }

  // Generated types for |userImage| are incorrect for mojom unions. Only one
  // key should be present at runtime. This weird construction allows typescript
  // to do exhaustiveness checking in the switch/case.
  assert(Object.keys(userImage).length === 1, 'only one key is set');
  const key = Object.keys(userImage)[0] as keyof UserImage;
  switch (key) {
    case 'invalidImage':
      return placeHolderUrl;
    case 'defaultImage':
      return userImage.defaultImage!.url;
    case 'profileImage':
      return state.user.profileImage;
    case 'externalImage':
      return bufferToPngObjectUrl(userImage.externalImage!);
    default:
      console.error('Unknown image type received', key);
      return placeHolderUrl;
  }
}

/**
 * Derive the |Url| to display of the last external user image. This is an image
 * from camera or file.
 */
export function selectLastExternalUserImageUrl(state: PersonalizationState):
    Url|null {
  const lastExternalUserImage = state.user.lastExternalUserImage;

  if (!lastExternalUserImage) {
    return null;
  }

  const buffer = lastExternalUserImage.externalImage;
  assert(!!buffer, 'externalImage must be set');
  return bufferToPngObjectUrl(buffer);
}