MOBILE-2962 messages: Keep scroll position when loading previous

main
Dani Palou 2019-04-10 12:17:45 +02:00
parent 546667d11a
commit bbfe831846
3 changed files with 96 additions and 7 deletions

View File

@ -27,6 +27,7 @@ import { CoreLoggerProvider } from '@providers/logger';
import { CoreAppProvider } from '@providers/app';
import { coreSlideInOut } from '@classes/animations';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreInfiniteLoadingComponent } from '@components/infinite-loading/infinite-loading';
import { Md5 } from 'ts-md5/dist/md5';
import * as moment from 'moment';
@ -41,6 +42,7 @@ import * as moment from 'moment';
})
export class AddonMessagesDiscussionPage implements OnDestroy {
@ViewChild(Content) content: Content;
@ViewChild(CoreInfiniteLoadingComponent) infinite: CoreInfiniteLoadingComponent;
siteId: string;
protected fetching: boolean;
@ -381,6 +383,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
message.showUserData = this.showUserData(message, this.messages[index - 1]);
});
// Call resize to recalculate the dimensions.
this.content && this.content.resize();
// Notify that there can be a new message.
this.notifyNewMessage();
@ -825,11 +830,28 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
* @return {Promise<any>} Resolved when done.
*/
loadPrevious(infiniteComplete?: any): Promise<any> {
let infiniteHeight = this.infinite ? this.infinite.getHeight() : 0;
const scrollHeight = this.domUtils.getScrollHeight(this.content);
// If there is an ongoing fetch, wait for it to finish.
return this.waitForFetch().finally(() => {
this.pagesLoaded++;
this.fetchMessages().catch((error) => {
this.fetchMessages().then(() => {
// Try to keep the scroll position.
const scrollBottom = scrollHeight - this.domUtils.getScrollTop(this.content);
if (this.canLoadMore && infiniteHeight && this.infinite) {
// The height of the infinite is different while spinner is shown. Add that difference.
infiniteHeight = infiniteHeight - this.infinite.getHeight();
} else if (!this.canLoadMore) {
// Can't load more, take into account the full height of the infinite loading since it will disappear now.
infiniteHeight = infiniteHeight || (this.infinite ? this.infinite.getHeight() : 0);
}
this.keepScroll(scrollHeight, scrollBottom, infiniteHeight);
}).catch((error) => {
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
this.pagesLoaded--;
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true);
@ -839,6 +861,31 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
});
}
/**
* Keep scroll position after loading previous messages.
* We don't use resizeContent because the approach used is different and it isn't easy to calculate these positions.
*/
protected keepScroll(oldScrollHeight: number, oldScrollBottom: number, infiniteHeight: number, retries?: number): void {
retries = retries || 0;
setTimeout(() => {
const newScrollHeight = this.domUtils.getScrollHeight(this.content);
if (newScrollHeight == oldScrollHeight) {
// Height hasn't changed yet. Retry if max retries haven't been reached.
if (retries <= 10) {
this.keepScroll(oldScrollHeight, oldScrollBottom, infiniteHeight, retries + 1);
}
return;
}
const scrollTo = newScrollHeight - oldScrollBottom + infiniteHeight;
this.domUtils.scrollTo(this.content, 0, scrollTo, 0);
}, 30);
}
/**
* Content or scroll has been resized. For content, only call it if it's been added on top.
*/

View File

@ -1,5 +1,5 @@
<ng-container *ngIf="!loadingMore && position != 'top'">
<div *ngIf="enabled || error" padding-horizontal>
<div *ngIf="enabled || error" padding-horizontal #bottombutton>
<button *ngIf="!error" ion-button block (click)="loadMore()" color="light">
{{ 'core.loadmore' | translate }}
</button>
@ -9,12 +9,12 @@
</div>
</ng-container>
<ion-infinite-scroll [enabled]="enabled && !error && !loadingMore" (ionInfinite)="loadMore($event)" [position]="position">
<ion-infinite-scroll [enabled]="enabled && !error && !loadingMore" (ionInfinite)="loadMore($event)" [position]="position" #infinitescroll>
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
<ng-container *ngIf="!loadingMore && position == 'top'">
<div *ngIf="enabled || error" padding-horizontal>
<div *ngIf="enabled || error" padding-horizontal #topbutton>
<button *ngIf="!error" ion-button block (click)="loadMore()" color="light">
{{ 'core.loadmore' | translate }}
</button>
@ -24,6 +24,6 @@
</div>
</ng-container>
<div *ngIf="loadingMore" padding text-center>
<div *ngIf="loadingMore" padding text-center #spinnercontainer>
<ion-spinner></ion-spinner>
</div>

View File

@ -12,8 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional } from '@angular/core';
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional, ViewChild, ElementRef } from '@angular/core';
import { InfiniteScroll, Content } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
/**
* Component to show a infinite loading trigger and spinner while more data is being loaded.
@ -31,11 +32,16 @@ export class CoreInfiniteLoadingComponent implements OnChanges {
@Input() position = 'bottom';
@Output() action: EventEmitter<() => void>; // Will emit an event when triggered.
@ViewChild('topbutton') topButton: ElementRef;
@ViewChild('infinitescroll') infiniteEl: ElementRef;
@ViewChild('bottombutton') bottomButton: ElementRef;
@ViewChild('spinnercontainer') spinnerContainer: ElementRef;
loadingMore = false; // Hide button and avoid loading more.
protected infiniteScroll: InfiniteScroll;
constructor(@Optional() private content: Content) {
constructor(@Optional() private content: Content, private domUtils: CoreDomUtilsProvider) {
this.action = new EventEmitter();
}
@ -77,6 +83,18 @@ export class CoreInfiniteLoadingComponent implements OnChanges {
* Complete loading.
*/
complete(): void {
if (this.position == 'top') {
// Wait a bit before allowing loading more, otherwise it could be re-triggered automatically when it shouldn't.
setTimeout(this.completeLoadMore.bind(this), 400);
} else {
this.completeLoadMore();
}
}
/**
* Complete loading.
*/
protected completeLoadMore(): void {
this.loadingMore = false;
this.infiniteScroll && this.infiniteScroll.complete();
this.infiniteScroll = undefined;
@ -89,4 +107,28 @@ export class CoreInfiniteLoadingComponent implements OnChanges {
});
}
/**
* Get the height of the element.
*
* @return {number} Height.
*/
getHeight(): number {
return this.getElementHeight(this.topButton) + this.getElementHeight(this.infiniteEl) +
this.getElementHeight(this.bottomButton) + this.getElementHeight(this.spinnerContainer);
}
/**
* Get the height of an element.
*
* @param {ElementRef} element Element ref.
* @return {number} Height.
*/
protected getElementHeight(element: ElementRef): number {
if (element && element.nativeElement) {
return this.domUtils.getElementHeight(element.nativeElement, true, true, true);
}
return 0;
}
}