Skip to content

Commit

Permalink
personalization: Support incremental load of Google Photos album.
Browse files Browse the repository at this point in the history
Previously, all Google Photos photos for an album were loaded at once.
Now, photos for an album will be loaded on an incremental basis as the
user scrolls down the page.

Note that this CL makes use of a mixin to update list properties in
place to improve performance by minimizing property change notifications.

Bug: b:216882690
Change-Id: I712295dbfd02d3b8b1fcee9c889b90bf40f72350
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3543508
Reviewed-by: Jeffrey Young <cowmoo@chromium.org>
Commit-Queue: David Black <dmblack@google.com>
Cr-Commit-Position: refs/heads/main@{#984600}
  • Loading branch information
David Black authored and Chromium LUCI CQ committed Mar 23, 2022
1 parent f912750 commit 6b6641b
Show file tree
Hide file tree
Showing 15 changed files with 402 additions and 243 deletions.
Expand Up @@ -5,6 +5,7 @@
import {Action, Store} from 'chrome://resources/js/cr/ui/store.js';
import {StoreClient, StoreClientInterface} from 'chrome://resources/js/cr/ui/store_client.js';
import {I18nMixin, I18nMixinInterface} from 'chrome://resources/js/i18n_mixin.js';
import {ListPropertyUpdateMixin, ListPropertyUpdateMixinInterface} from 'chrome://resources/js/list_property_update_mixin.js';
import {IronResizableBehavior} from 'chrome://resources/polymer/v3_0/iron-resizable-behavior/iron-resizable-behavior.js';
import {mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

Expand Down Expand Up @@ -107,12 +108,13 @@ const PersonalizationStoreClientImpl: PersonalizationStoreClient&

export const WithPersonalizationStore: {
new (): PolymerElement&I18nMixinInterface&IronResizableBehavior&
PersonalizationStoreClient&StoreClientInterface
ListPropertyUpdateMixinInterface&PersonalizationStoreClient&
StoreClientInterface
} =
mixinBehaviors(
[
StoreClient,
PersonalizationStoreClientImpl,
IronResizableBehavior,
],
I18nMixin(PolymerElement));
I18nMixin(ListPropertyUpdateMixin(PolymerElement)));
Expand Up @@ -11,12 +11,12 @@ import 'chrome://resources/polymer/v3_0/iron-scroll-threshold/iron-scroll-thresh
import './styles.js';
import '../../common/styles.js';

import {assert} from 'chrome://resources/js/assert.m.js';
import {assert} from 'chrome://resources/js/assert_ts.js';
import {IronListElement} from 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import {IronScrollThresholdElement} from 'chrome://resources/polymer/v3_0/iron-scroll-threshold/iron-scroll-threshold.js';
import {afterNextRender, html} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getCountText, isNonEmptyArray, isSelectionEvent} from '../../common/utils.js';
import {getCountText, isSelectionEvent} from '../../common/utils.js';
import {GooglePhotosAlbum, WallpaperProviderInterface} from '../personalization_app.mojom-webui.js';
import {PersonalizationRouter} from '../personalization_router_element.js';
import {WithPersonalizationStore} from '../personalization_store.js';
Expand Down Expand Up @@ -51,7 +51,11 @@ export class GooglePhotosAlbums extends WithPersonalizationStore {
observer: 'onAlbumsChanged_',
},

albumsForDisplay_: Array,
albumsForDisplay_: {
type: Array,
value: [],
},

albumsLoading_: Boolean,

albumsResumeToken_: {
Expand All @@ -68,7 +72,7 @@ export class GooglePhotosAlbums extends WithPersonalizationStore {
private albums_: GooglePhotosAlbum[]|null|undefined;

/** The list of |albums_| which is updated in place for display. */
private albumsForDisplay_: GooglePhotosAlbum[]|null|undefined;
private albumsForDisplay_: GooglePhotosAlbum[];

/** Whether the list of albums is currently loading. */
private albumsLoading_: boolean;
Expand Down Expand Up @@ -104,39 +108,20 @@ export class GooglePhotosAlbums extends WithPersonalizationStore {

/** Invoked on changes to |albums_|. */
private onAlbumsChanged_(albums: GooglePhotosAlbums['albums_']) {
if (!isNonEmptyArray(albums)) {
this.albumsForDisplay_ = null;
return;
}

// Case: First batch of albums.
if (this.albumsForDisplay_ === null ||
this.albumsForDisplay_ === undefined) {
this.albumsForDisplay_ = albums;
return;
}

// Case: Subsequent batches of albums.
// NOTE: |albumsForDisplay_| is updated in place to avoid resetting the
// scroll position of the grid but it will be deeply equal to |albums_|
// after being updated.
albums.forEach((album, i) => {
if (i < this.albumsForDisplay_!.length) {
this.set(`albumsForDisplay_.${i}`, album);
} else {
this.push('albumsForDisplay_', album);
}
});

while (this.albumsForDisplay_.length > albums.length) {
this.pop(`albumsForDisplay_`);
}
// scroll position of the grid which would otherwise occur during
// reassignment but it will be deeply equal to |albums_| after updating.
this.updateList(
/*propertyPath=*/ 'albumsForDisplay_',
/*identityGetter=*/ (album: GooglePhotosAlbum) => album.id,
/*newList=*/ albums ?? [],
/*identityBasedUpdate=*/ true);
}

/** Invoked on changes to |albumsResumeToken_|. */
private onAlbumsResumeTokenChanged_(
albumsResumeToken: GooglePhotosAlbums['albumsResumeToken_']) {
if (albumsResumeToken?.length) {
if (albumsResumeToken) {
this.$.gridScrollThreshold.clearTriggers();
}
}
Expand All @@ -151,7 +136,7 @@ export class GooglePhotosAlbums extends WithPersonalizationStore {

// Ignore this event if albums are already being loading or if there is no
// resume token (indicating there are no additional albums to load).
if (this.albumsLoading_ === true || this.albumsResumeToken_ === null) {
if (this.albumsLoading_ === true || !this.albumsResumeToken_) {
return;
}

Expand Down
Expand Up @@ -11,7 +11,7 @@ import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import './styles.js';
import '/common/styles.js';

import {assertNotReached} from 'chrome://resources/js/assert.m.js';
import {assertNotReached} from 'chrome://resources/js/assert_ts.js';
import {html} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {isNonEmptyArray} from '../../common/utils.js';
Expand Down
Expand Up @@ -3,21 +3,26 @@
overflow: hidden;
}

iron-scroll-threshold,
iron-list {
height: 100%;
width: 100%;
}
</style>
<iron-list id="grid" items="[[album_]]" as="photo" grid>
<template>
<wallpaper-grid-item
class="photo"
image-src="[[photo.url.url]]"
on-click="onPhotoSelected_"
on-keypress="onPhotoSelected_"
selected="[[isPhotoSelected_(
photo, currentSelected_, pendingSelected_)]]"
tabindex$="[[tabIndex]]">
</wallpaper-grid-item>
</template>
</iron-list>
<iron-scroll-threshold id="gridScrollThreshold"
on-lower-threshold="onGridScrollThresholdReached_">
<iron-list id="grid" items="[[album_]]" as="photo" grid
scroll-target="gridScrollThreshold">
<template>
<wallpaper-grid-item
class="photo"
image-src="[[photo.url.url]]"
on-click="onPhotoSelected_"
on-keypress="onPhotoSelected_"
selected="[[isPhotoSelected_(
photo, currentSelected_, pendingSelected_)]]"
tabindex$="[[tabIndex]]">
</wallpaper-grid-item>
</template>
</iron-list>
</iron-scroll-threshold>
Expand Up @@ -7,12 +7,15 @@
* for the currently selected album id.
*/

import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import 'chrome://resources/polymer/v3_0/iron-scroll-threshold/iron-scroll-threshold.js';
import './styles.js';
import '/common/styles.js';

import {assert} from 'chrome://resources/js/assert.m.js';
import {assert} from 'chrome://resources/js/assert_ts.js';
import {FilePath} from 'chrome://resources/mojo/mojo/public/mojom/base/file_path.mojom-webui.js';
import {IronListElement} from 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import {IronScrollThresholdElement} from 'chrome://resources/polymer/v3_0/iron-scroll-threshold/iron-scroll-threshold.js';
import {afterNextRender, html} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {isSelectionEvent} from '../../common/utils.js';
Expand All @@ -24,7 +27,7 @@ import {fetchGooglePhotosAlbum, selectWallpaper} from './wallpaper_controller.js
import {getWallpaperProvider} from './wallpaper_interface_provider.js';

export interface GooglePhotosPhotosByAlbumId {
$: {grid: IronListElement;};
$: {grid: IronListElement; gridScrollThreshold: IronScrollThresholdElement};
}

export class GooglePhotosPhotosByAlbumId extends WithPersonalizationStore {
Expand All @@ -38,9 +41,7 @@ export class GooglePhotosPhotosByAlbumId extends WithPersonalizationStore {

static get properties() {
return {
albumId: {
type: String,
},
albumId: String,

hidden: {
type: Boolean,
Expand All @@ -51,25 +52,32 @@ export class GooglePhotosPhotosByAlbumId extends WithPersonalizationStore {

album_: {
type: Array,
computed:
'computeAlbum_(albumId, photosByAlbumId_, photosByAlbumIdLoading_)',
value: [],
},

currentSelected_: Object,
pendingSelected_: Object,
photosByAlbumId_: Object,
photosByAlbumIdLoading_: Object,
photosByAlbumIdResumeTokens_: Object,
};
}

static get observers() {
return [
'onAlbumIdOrPhotosByAlbumIdChanged_(albumId, photosByAlbumId_)',
'onAlbumIdOrPhotosByAlbumIdResumeTokensChanged_(albumId, photosByAlbumIdResumeTokens_)',
];
}

/** The currently selected album id. */
albumId: string|undefined;

/** Whether or not this element is currently hidden. */
override hidden: boolean;

/** The list of photos for the currently selected album id. */
private album_: GooglePhotosPhoto[]|null|undefined;
private album_: GooglePhotosPhoto[];

/** The currently selected wallpaper. */
private currentSelected_: CurrentWallpaper|null;
Expand All @@ -83,6 +91,9 @@ export class GooglePhotosPhotosByAlbumId extends WithPersonalizationStore {
/** Whether the list of photos by album id is currently loading. */
private photosByAlbumIdLoading_: Record<string, boolean>;

/** The resume tokens needed to fetch the next page of photos by album id. */
private photosByAlbumIdResumeTokens_: Record<string, string|null>;

/** The singleton wallpaper provider interface. */
private wallpaperProvider_: WallpaperProviderInterface =
getWallpaperProvider();
Expand All @@ -100,10 +111,33 @@ export class GooglePhotosPhotosByAlbumId extends WithPersonalizationStore {
this.watch<GooglePhotosPhotosByAlbumId['photosByAlbumIdLoading_']>(
'photosByAlbumIdLoading_',
state => state.wallpaper.loading.googlePhotos.photosByAlbumId);
this.watch<GooglePhotosPhotosByAlbumId['photosByAlbumIdResumeTokens_']>(
'photosByAlbumIdResumeTokens_',
state => state.wallpaper.googlePhotos.resumeTokens.photosByAlbumId);

this.updateFromStore();
}

/** Invoked on grid scroll threshold reached. */
private onGridScrollThresholdReached_() {
// Ignore this event if fired during initialization.
if (!this.$.gridScrollThreshold.scrollHeight || !this.albumId) {
this.$.gridScrollThreshold.clearTriggers();
return;
}

// Ignore this event if photos are already being loaded or if there is no
// resume token (indicating there are no additional photos to load).
if (this.photosByAlbumIdLoading_[this.albumId] === true ||
!this.photosByAlbumIdResumeTokens_[this.albumId]) {
return;
}

// Fetch the next page of photos.
fetchGooglePhotosAlbum(
this.wallpaperProvider_, this.getStore(), this.albumId);
}

/** Invoked on changes to this element's |hidden| state. */
private onHiddenChanged_(hidden: GooglePhotosPhotosByAlbumId['hidden']) {
if (hidden) {
Expand All @@ -116,36 +150,49 @@ export class GooglePhotosPhotosByAlbumId extends WithPersonalizationStore {
afterNextRender(this, () => this.$.grid.fire('iron-resize'));
}

/** Invoked on selection of a photo. */
private onPhotoSelected_(e: Event&{model: {photo: GooglePhotosPhoto}}) {
assert(e.model.photo);
if (isSelectionEvent(e)) {
selectWallpaper(e.model.photo, this.wallpaperProvider_, this.getStore());
}
}

/** Invoked to compute |album_|. */
private computeAlbum_(
/** Invoked on changes to |albumId| or |photosByAlbumId_|. */
private onAlbumIdOrPhotosByAlbumIdChanged_(
albumId: GooglePhotosPhotosByAlbumId['albumId'],
photosByAlbumId: GooglePhotosPhotosByAlbumId['photosByAlbumId_'],
photosByAlbumIdLoading:
GooglePhotosPhotosByAlbumId['photosByAlbumIdLoading_']):
GooglePhotosPhoto[]|null {
// If no album is currently selected or if the currently selected album is
// still loading then there is nothing to display.
if (!albumId || photosByAlbumIdLoading[albumId]) {
return null;
photosByAlbumId: GooglePhotosPhotosByAlbumId['photosByAlbumId_']) {
// If no album is currently selected there is nothing to display.
if (!albumId) {
this.album_ = [];
return;
}

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

// Once the currently selected album has been fetched it can be displayed.
return photosByAlbumId[albumId]!;
// NOTE: |album_| is updated in place to avoid resetting the scroll
// position of the grid which would otherwise occur during reassignment.
this.updateList(
/*propertyPath=*/ 'album_',
/*identityGetter=*/ (photo: GooglePhotosPhoto) => photo.id,
/*newList=*/ photosByAlbumId[albumId] ?? [],
/*identityBasedUpdate=*/ true);
}

/** Invoked on changes to |albumId| or |photosByAlbumIdResumeTokens_|. */
private onAlbumIdOrPhotosByAlbumIdResumeTokensChanged_(
albumId: GooglePhotosPhotosByAlbumId['albumId'],
photosByAlbumIdResumeTokens:
GooglePhotosPhotosByAlbumId['photosByAlbumIdResumeTokens_']) {
if (albumId && photosByAlbumIdResumeTokens[albumId]) {
this.$.gridScrollThreshold.clearTriggers();
}
}

/** Invoked on selection of a photo. */
private onPhotoSelected_(e: Event&{model: {photo: GooglePhotosPhoto}}) {
assert(e.model.photo);
if (isSelectionEvent(e)) {
selectWallpaper(e.model.photo, this.wallpaperProvider_, this.getStore());
}
}

// Returns whether the specified |photo| is currently selected.
Expand Down

0 comments on commit 6b6641b

Please sign in to comment.