Skip to content

Commit

Permalink
personalization: Show placeholders while loading Google Photos album.
Browse files Browse the repository at this point in the history
Previously the user saw an empty white space while waiting for a Google
Photos album to load. Now, the user will see the same placeholder
animation as is used for the collections grid.

Follow up CLs will apply the same treatment to the Google Photos photos
and albums pages.

Demo: http://shortn/_XnnbEqbZau [1]

[1] Demo video taken with artificially long response time from server.

Bug: b:228213634
Change-Id: I7a368cbefb466af74d281f4aa5ace57b3bfc90ba
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3572167
Reviewed-by: Jeffrey Young <cowmoo@chromium.org>
Commit-Queue: David Black <dmblack@google.com>
Cr-Commit-Position: refs/heads/main@{#989487}
  • Loading branch information
David Black authored and Chromium LUCI CQ committed Apr 6, 2022
1 parent 39283c8 commit 0980ec6
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 19 deletions.
12 changes: 11 additions & 1 deletion ash/webui/personalization_app/resources/common/utils.ts
Expand Up @@ -98,12 +98,22 @@ export function getLoadingPlaceholderAnimationDelay(index: number): string {
return `--animation-delay: ${index * 83}ms;`;
}

/**
* Returns loading placeholders to render given the current inner width of the
* |window|. Placeholders are constructed using the specified |factory|.
*/
export function getLoadingPlaceholders<T>(factory: () => T): T[] {
const x = getNumberOfGridItemsPerRow();
const y = Math.floor(window.innerHeight / /*tileHeightPx=*/ 136);
return Array.from({length: x * y}, factory);
}

/**
* Returns the number of grid items to render per row given the current inner
* width of the |window|.
*/
export function getNumberOfGridItemsPerRow(): number {
return window.innerWidth > 688 ? 4 : 3;
return window.innerWidth > 720 ? 4 : 3;
}

/**
Expand Down
Expand Up @@ -17,8 +17,10 @@
<wallpaper-grid-item
class="photo"
image-src="[[photo.url.url]]"
index="[[index]]"
on-click="onPhotoSelected_"
on-keypress="onPhotoSelected_"
placeholder$="[[isPhotoPlaceholder_(photo)]]"
selected="[[isPhotoSelected_(
photo, currentSelected_, pendingSelected_)]]"
tabindex$="[[tabIndex]]">
Expand Down
Expand Up @@ -18,7 +18,7 @@ import {IronListElement} from 'chrome://resources/polymer/v3_0/iron-list/iron-li
import {IronScrollThresholdElement} from 'chrome://resources/polymer/v3_0/iron-scroll-threshold/iron-scroll-threshold.js';
import {afterNextRender} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {isSelectionEvent} from '../../common/utils.js';
import {getLoadingPlaceholders, isSelectionEvent} from '../../common/utils.js';
import {CurrentWallpaper, GooglePhotosAlbum, GooglePhotosPhoto, WallpaperImage, WallpaperProviderInterface, WallpaperType} from '../personalization_app.mojom-webui.js';
import {WithPersonalizationStore} from '../personalization_store.js';
import {isGooglePhotosPhoto} from '../utils.js';
Expand All @@ -27,6 +27,17 @@ import {getTemplate} from './google_photos_photos_by_album_id_element.html.js';
import {fetchGooglePhotosAlbum, selectWallpaper} from './wallpaper_controller.js';
import {getWallpaperProvider} from './wallpaper_interface_provider.js';

const PLACEHOLDER_ID = 'placeholder';

/** Returns placeholders to show while Google Photos are loading. */
function getPlaceholders(): GooglePhotosPhoto[] {
return getLoadingPlaceholders(() => {
const photo = new GooglePhotosPhoto();
photo.id = PLACEHOLDER_ID;
return photo;
});
}

export interface GooglePhotosPhotosByAlbumId {
$: {grid: IronListElement, gridScrollThreshold: IronScrollThresholdElement};
}
Expand All @@ -53,7 +64,7 @@ export class GooglePhotosPhotosByAlbumId extends WithPersonalizationStore {

album_: {
type: Array,
value: [],
value: getPlaceholders,
},

albums: Array,
Expand Down Expand Up @@ -177,19 +188,20 @@ export class GooglePhotosPhotosByAlbumId extends WithPersonalizationStore {
}

// If the album associated with |albumId| or |photosByAlbumId| have not yet
// been set, there is nothing to display. This occurs if the user refreshes
// the wallpaper app while its navigated to a Google Photos album.
// been set, there is nothing to display except placeholders. This occurs
// if the user refreshes the wallpaper app while its navigated to a Google
// Photos album.
if (!Array.isArray(albums) || !albums.some(album => album.id === albumId) ||
!photosByAlbumId) {
this.album_ = [];
this.album_ = getPlaceholders();
return;
}

// If the currently selected album has not already been fetched, do so
// though there is still nothing to display.
// though there is still nothing to display except placeholders.
if (!photosByAlbumId.hasOwnProperty(albumId)) {
fetchGooglePhotosAlbum(this.wallpaperProvider_, this.getStore(), albumId);
this.album_ = [];
this.album_ = getPlaceholders();
return;
}

Expand All @@ -216,11 +228,16 @@ export class GooglePhotosPhotosByAlbumId extends WithPersonalizationStore {
/** Invoked on selection of a photo. */
private onPhotoSelected_(e: Event&{model: {photo: GooglePhotosPhoto}}) {
assert(e.model.photo);
if (isSelectionEvent(e)) {
if (!this.isPhotoPlaceholder_(e.model.photo) && isSelectionEvent(e)) {
selectWallpaper(e.model.photo, this.wallpaperProvider_, this.getStore());
}
}

/** Returns whether the specified |photo| is a placeholder. */
private isPhotoPlaceholder_(photo: GooglePhotosPhoto|null): boolean {
return !!photo && photo.id === PLACEHOLDER_ID;
}

// Returns whether the specified |photo| is currently selected.
private isPhotoSelected_(
photo: GooglePhotosPhoto|null,
Expand Down
Expand Up @@ -44,6 +44,10 @@
outline: 2px solid var(--cros-focus-ring-color);
}

:host([placeholder]) .item {
animation: 2210ms linear var(--animation-delay, 1s) infinite ripple;
}

.item[aria-selected] {
background-color: rgba(
var(--cros-color-prominent-rgb),
Expand Down Expand Up @@ -134,7 +138,8 @@
}
}
</style>
<div class="item" aria-selected$="[[selected]]">
<div class="item" aria-selected$="[[selected]]"
style$="[[getItemPlaceholderAnimationDelay_(index)]]">
<template is="dom-if" if="[[isImageVisible_(imageSrc)]]">
<img is="cr-auto-img" auto-src="[[imageSrc]]" clear-src with-cookies></img>
</template>
Expand Down
Expand Up @@ -10,6 +10,7 @@ import 'chrome://resources/cr_elements/cr_auto_img/cr_auto_img.js';
import '../../common/styles.js';

import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getLoadingPlaceholderAnimationDelay} from '../../common/utils.js';
import {getTemplate} from './wallpaper_grid_item_element.html.js';

export class WallpaperGridItem extends PolymerElement {
Expand All @@ -24,6 +25,7 @@ export class WallpaperGridItem extends PolymerElement {
static get properties() {
return {
imageSrc: String,
index: Number,
primaryText: String,
secondaryText: String,

Expand All @@ -37,6 +39,9 @@ export class WallpaperGridItem extends PolymerElement {
/** The source for the image to render for the grid item. */
imageSrc: string|undefined;

/** The index of the grid item within its parent grid. */
index: number;

/** The primary text to render for the grid item. */
primaryText: string|undefined;

Expand All @@ -46,6 +51,12 @@ export class WallpaperGridItem extends PolymerElement {
/** Whether the grid item is currently selected. */
selected: boolean;

/** Returns the delay to use for the grid item's placeholder animation. */
private getItemPlaceholderAnimationDelay_(index: WallpaperGridItem['index']):
string {
return getLoadingPlaceholderAnimationDelay(index);
}

/** Whether the image is currently visible. */
private isImageVisible_() {
return !!this.imageSrc && !!this.imageSrc.length;
Expand Down
Expand Up @@ -12,7 +12,7 @@ import {FilePath} from 'chrome://resources/mojo/mojo/public/mojom/base/file_path
import {Url} from 'chrome://resources/mojo/url/mojom/url.mojom-webui.js';

import {Events, EventType, kMaximumGooglePhotosPreviews, kMaximumLocalImagePreviews} from '../common/constants.js';
import {getCountText, getLoadingPlaceholderAnimationDelay, getNumberOfGridItemsPerRow, isNonEmptyArray, isNullOrArray, isNullOrNumber, isSelectionEvent} from '../common/utils.js';
import {getCountText, getLoadingPlaceholderAnimationDelay, getLoadingPlaceholders, isNonEmptyArray, isNullOrArray, isNullOrNumber, isSelectionEvent} from '../common/utils.js';
import {GooglePhotosEnablementState, WallpaperCollection} from '../trusted/personalization_app.mojom-webui.js';
import {selectCollection, selectGooglePhotosCollection, selectLocalCollection, validateReceivedData} from '../untrusted/iframe_api.js';

Expand All @@ -26,9 +26,6 @@ import {getTemplate} from './collections_grid.html.js';
const kGooglePhotosCollectionId = 'google_photos_';
const kLocalCollectionId = 'local_';

/** Height in pixels of a tile. */
const kTileHeightPx = 136;

enum TileType {
LOADING = 'loading',
IMAGE_GOOGLE_PHOTOS = 'image_google_photos',
Expand Down Expand Up @@ -188,9 +185,7 @@ export class CollectionsGrid extends PolymerElement {
value() {
// Fill the view with loading tiles. Will be adjusted to the correct
// number of tiles when collections are received.
const x = getNumberOfGridItemsPerRow();
const y = Math.floor(window.innerHeight / kTileHeightPx);
return Array.from({length: x * y}, () => ({type: TileType.LOADING}));
return getLoadingPlaceholders(() => ({type: TileType.LOADING}));
}
},
};
Expand Down
Expand Up @@ -7,7 +7,7 @@ import 'chrome://webui-test/mojo_webui_test_support.js';

import {fetchGooglePhotosAlbum, GooglePhotosAlbum, GooglePhotosEnablementState, GooglePhotosPhoto, GooglePhotosPhotosByAlbumId, initializeGooglePhotosData, WallpaperGridItem, WallpaperLayout, WallpaperType} from 'chrome://personalization/trusted/personalization_app.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {assertDeepEquals, assertEquals} from 'chrome://webui-test/chai_assert.js';
import {assertDeepEquals, assertEquals, assertNotEquals} from 'chrome://webui-test/chai_assert.js';
import {waitAfterNextRender} from 'chrome://webui-test/test_util.js';

import {baseSetup, initElement, teardownElement} from './personalization_app_test_utils.js';
Expand All @@ -19,6 +19,15 @@ suite('GooglePhotosPhotosByAlbumIdTest', function() {
let personalizationStore: TestPersonalizationStore;
let wallpaperProvider: TestWallpaperProvider;

/**
* Returns the match for |selector| in |googlePhotosPhotosByAlbumIdElement|'s
* shadow DOM.
*/
function querySelector(selector: string): Element|null {
return googlePhotosPhotosByAlbumIdElement!.shadowRoot!.querySelector(
selector);
}

/**
* Returns all matches for |selector| in
* |googlePhotosPhotosByAlbumIdElement|'s shadow DOM.
Expand Down Expand Up @@ -99,7 +108,8 @@ suite('GooglePhotosPhotosByAlbumIdTest', function() {
await waitAfterNextRender(googlePhotosPhotosByAlbumIdElement);

// Initially no album id selected. Photos should be absent.
const photoSelector = 'wallpaper-grid-item:not([hidden]).photo';
const photoSelector =
'wallpaper-grid-item:not([hidden]):not([placeholder]).photo';
assertEquals(querySelectorAll(photoSelector)!.length, 0);

// Select an album id. Photos should be absent since albums have not loaded.
Expand Down Expand Up @@ -305,6 +315,62 @@ suite('GooglePhotosPhotosByAlbumIdTest', function() {
assertEquals(photoEls[1]!.selected, false);
});

test('displays placeholders until photos are present', async () => {
// Prepare Google Photos data.
const photosCount = 5;
const album: GooglePhotosAlbum =
{id: '1', title: '', photoCount: photosCount, preview: {url: ''}};
const photos: GooglePhotosPhoto[] =
Array.from({length: photosCount}, (_, i) => ({
id: `id-${i}`,
name: `name-${i}`,
date: {data: []},
url: {url: `url-${i}`},
}));

// Initialize |googlePhotosPhotosByAlbumIdElement|.
googlePhotosPhotosByAlbumIdElement =
initElement(GooglePhotosPhotosByAlbumId, {hidden: false});
await waitAfterNextRender(googlePhotosPhotosByAlbumIdElement);

// Initially no album id selected. Photos and placeholders should be absent.
const selector = 'wallpaper-grid-item:not([hidden]).photo';
const photoSelector = `${selector}:not([placeholder])`;
const placeholderSelector = `${selector}[placeholder]`;
assertEquals(querySelectorAll(photoSelector)!.length, 0);
assertEquals(querySelectorAll(placeholderSelector)!.length, 0);

// Select album id. Only placeholders should be present.
googlePhotosPhotosByAlbumIdElement.setAttribute('album-id', album.id);
await waitAfterNextRender(googlePhotosPhotosByAlbumIdElement);
assertEquals(querySelectorAll(photoSelector)!.length, 0);
assertNotEquals(querySelectorAll(placeholderSelector)!.length, 0);

// Clicking a placeholder should do nothing.
const clickHandler = 'selectGooglePhotosPhoto';
(querySelector(placeholderSelector) as HTMLElement).click();
await new Promise<void>(resolve => setTimeout(resolve));
assertEquals(wallpaperProvider.getCallCount(clickHandler), 0);

// Provide Google Photos data.
personalizationStore.data.wallpaper.googlePhotos.count = photosCount;
personalizationStore.data.wallpaper.googlePhotos.albums = [album];
personalizationStore.data.wallpaper.googlePhotos.photos = photos;
personalizationStore.data.wallpaper.googlePhotos
.photosByAlbumId = {[album.id]: photos};
personalizationStore.notifyObservers();

// Only photos should be present.
await waitAfterNextRender(googlePhotosPhotosByAlbumIdElement);
assertNotEquals(querySelectorAll(photoSelector)!.length, 0);
assertEquals(querySelectorAll(placeholderSelector)!.length, 0);

// Clicking a photo should do something.
(querySelector(photoSelector) as HTMLElement).click();
assertEquals(
await wallpaperProvider.whenCalled(clickHandler), photos[0]!.id);
});

test('incrementally loads photos', async () => {
personalizationStore.setReducersEnabled(true);

Expand Down

0 comments on commit 0980ec6

Please sign in to comment.