MOBILE-3320 messages: Improve scroll position after loading previous

main
Dani Palou 2021-07-01 12:49:19 +02:00
parent 8681b91a0e
commit a81d1c7161
3 changed files with 39 additions and 48 deletions

View File

@ -174,6 +174,6 @@
</p> </p>
</div> </div>
<core-send-message-form *ngIf="footerType == 'message'" (onSubmit)="sendMessage($event)" [showKeyboard]="showKeyboard" <core-send-message-form *ngIf="footerType == 'message'" (onSubmit)="sendMessage($event)" [showKeyboard]="showKeyboard"
[placeholder]="'addon.messages.newmessage' | translate" (onResize)="resizeContent()"></core-send-message-form> [placeholder]="'addon.messages.newmessage' | translate"></core-send-message-form>
</ion-toolbar> </ion-toolbar>
</ion-footer> </ion-footer>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { IonContent } from '@ionic/angular'; import { IonContent } from '@ionic/angular';
import { AlertOptions } from '@ionic/core'; import { AlertOptions } from '@ionic/core';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
@ -76,6 +76,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
protected viewDestroyed = false; protected viewDestroyed = false;
protected memberInfoObserver: CoreEventObserver; protected memberInfoObserver: CoreEventObserver;
protected showLoadingModal = false; // Whether to show a loading modal while fetching data. protected showLoadingModal = false; // Whether to show a loading modal while fetching data.
protected hostElement: HTMLElement;
conversationId?: number; // Conversation ID. Undefined if it's a new individual conversation. conversationId?: number; // Conversation ID. Undefined if it's a new individual conversation.
conversation?: AddonMessagesConversationFormatted; // The conversation object (if it exists). conversation?: AddonMessagesConversationFormatted; // The conversation object (if it exists).
@ -113,7 +114,9 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
constructor( constructor(
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected elementRef: ElementRef<HTMLElement>,
) { ) {
this.hostElement = elementRef.nativeElement;
this.siteId = CoreSites.getCurrentSiteId(); this.siteId = CoreSites.getCurrentSiteId();
this.currentUserId = CoreSites.getCurrentSiteUserId(); this.currentUserId = CoreSites.getCurrentSiteUserId();
this.groupMessagingEnabled = AddonMessages.isGroupMessagingEnabled(); this.groupMessagingEnabled = AddonMessages.isGroupMessagingEnabled();
@ -357,7 +360,6 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
CoreDomUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); CoreDomUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true);
} finally { } finally {
this.checkCanDelete(); this.checkCanDelete();
this.resizeContent();
this.loaded = true; this.loaded = true;
this.setPolling(); // Make sure we're polling messages. this.setPolling(); // Make sure we're polling messages.
this.setContactRequestInfo(); this.setContactRequestInfo();
@ -537,7 +539,9 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
return; return;
} }
const messages = Array.from(document.querySelectorAll('.addon-message-not-mine')).slice(-this.newMessages).reverse(); const messages = Array.from(this.hostElement.querySelectorAll('.addon-message-not-mine'))
.slice(-this.newMessages)
.reverse();
const newMessagesUnread = messages.findIndex((message) => { const newMessagesUnread = messages.findIndex((message) => {
const elementRect = message.getBoundingClientRect(); const elementRect = message.getBoundingClientRect();
@ -1045,7 +1049,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
return; return;
} }
let infiniteHeight = this.infinite?.infiniteEl?.nativeElement.getBoundingClientRect().height || 0; let infiniteHeight = this.infinite?.hostElement.getBoundingClientRect().height || 0;
const scrollHeight = (this.scrollElement?.scrollHeight || 0); const scrollHeight = (this.scrollElement?.scrollHeight || 0);
// If there is an ongoing fetch, wait for it to finish. // If there is an ongoing fetch, wait for it to finish.
@ -1060,7 +1064,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
// Try to keep the scroll position. // Try to keep the scroll position.
const scrollBottom = scrollHeight - (this.scrollElement?.scrollTop || 0); const scrollBottom = scrollHeight - (this.scrollElement?.scrollTop || 0);
const height = this.infinite?.infiniteEl?.nativeElement.getBoundingClientRect().height || 0; const height = this.infinite?.hostElement.getBoundingClientRect().height || 0;
if (this.canLoadMore && infiniteHeight && this.infinite) { if (this.canLoadMore && infiniteHeight && this.infinite) {
// The height of the infinite is different while spinner is shown. Add that difference. // The height of the infinite is different while spinner is shown. Add that difference.
infiniteHeight = infiniteHeight - height; infiniteHeight = infiniteHeight - height;
@ -1082,10 +1086,8 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
/** /**
* Keep scroll position after loading previous messages. * 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 = 0): void { protected keepScroll(oldScrollHeight: number, oldScrollBottom: number, infiniteHeight: number, retries = 0): void {
setTimeout(() => { setTimeout(() => {
const newScrollHeight = (this.scrollElement?.scrollHeight || 0); const newScrollHeight = (this.scrollElement?.scrollHeight || 0);
@ -1098,37 +1100,16 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
return; return;
} }
const scrollTo = newScrollHeight - oldScrollBottom + infiniteHeight; // Scroll has changed, but maybe it hasn't reached the full height yet.
setTimeout(() => {
const newScrollHeight = (this.scrollElement?.scrollHeight || 0);
const scrollTo = newScrollHeight - oldScrollBottom + infiniteHeight;
this.content!.scrollToPoint(0, scrollTo, 0); this.content!.scrollToPoint(0, scrollTo, 0);
}, 30);
}, 30); }, 30);
} }
/**
* Content or scroll has been resized. For content, only call it if it's been added on top.
*/
resizeContent(): void {
/* @todo probably not needed.
let top = this.content!.getContentDimensions().scrollTop;
// @todo this.content.resize();
// Wait for new content height to be calculated.
setTimeout(() => {
// Visible content size changed, maintain the bottom position.
if (!this.viewDestroyed && (this.scrollElement?.clientHeight || 0) != this.oldContentHeight) {
if (!top) {
top = this.content!.getContentDimensions().scrollTop;
}
top += this.oldContentHeight - (this.scrollElement?.clientHeight || 0);
this.oldContentHeight = (this.scrollElement?.clientHeight || 0);
this.content!.scrollToPoint(0, top, 0);
}
});
*/
}
/** /**
* Scroll bottom when render has finished. * Scroll bottom when render has finished.
* *
@ -1160,7 +1141,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
*/ */
scrollToFirstUnreadMessage(): void { scrollToFirstUnreadMessage(): void {
if (this.newMessages > 0) { if (this.newMessages > 0) {
const messages = Array.from(document.querySelectorAll('.addon-message-not-mine')); const messages = Array.from(this.hostElement.querySelectorAll('.addon-message-not-mine'));
CoreDomUtils.scrollToElement(this.content!, <HTMLElement> messages[messages.length - this.newMessages]); CoreDomUtils.scrollToElement(this.content!, <HTMLElement> messages[messages.length - this.newMessages]);
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange, ViewChild, ElementRef } from '@angular/core'; import { Component, Input, Output, EventEmitter, OnChanges, SimpleChange, ViewChild, ElementRef } from '@angular/core';
import { IonContent, IonInfiniteScroll } from '@ionic/angular'; import { IonInfiniteScroll } from '@ionic/angular';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
@ -37,15 +37,16 @@ export class CoreInfiniteLoadingComponent implements OnChanges {
@Output() action: EventEmitter<() => void>; // Will emit an event when triggered. @Output() action: EventEmitter<() => void>; // Will emit an event when triggered.
@ViewChild('topbutton') topButton?: ElementRef; @ViewChild('topbutton') topButton?: ElementRef;
@ViewChild('infinitescroll') infiniteEl?: ElementRef;
@ViewChild('bottombutton') bottomButton?: ElementRef; @ViewChild('bottombutton') bottomButton?: ElementRef;
@ViewChild('spinnercontainer') spinnerContainer?: ElementRef; @ViewChild('spinnercontainer') spinnerContainer?: ElementRef;
@ViewChild(IonInfiniteScroll) infiniteScroll?: IonInfiniteScroll; @ViewChild(IonInfiniteScroll) infiniteScroll?: IonInfiniteScroll;
loadingMore = false; // Hide button and avoid loading more. loadingMore = false; // Hide button and avoid loading more.
hostElement: HTMLElement;
constructor(protected element: ElementRef) { constructor(protected element: ElementRef<HTMLElement>) {
this.action = new EventEmitter(); this.action = new EventEmitter();
this.hostElement = element.nativeElement;
} }
/** /**
@ -76,14 +77,14 @@ export class CoreInfiniteLoadingComponent implements OnChanges {
await CoreUtils.nextTick(); await CoreUtils.nextTick();
// Calculate distance from edge. // Calculate distance from edge.
const content = this.element.nativeElement.closest('ion-content') as IonContent; const content = this.hostElement.closest('ion-content');
if (!content) { if (!content) {
return; return;
} }
const scrollElement = await content.getScrollElement(); const scrollElement = await content.getScrollElement();
const infiniteHeight = this.element.nativeElement.getBoundingClientRect().height; const infiniteHeight = this.hostElement.getBoundingClientRect().height;
const scrollTop = scrollElement.scrollTop; const scrollTop = scrollElement.scrollTop;
const height = scrollElement.offsetHeight; const height = scrollElement.offsetHeight;
const threshold = height * THRESHOLD; const threshold = height * THRESHOLD;
@ -141,11 +142,20 @@ export class CoreInfiniteLoadingComponent implements OnChanges {
* @deprecated since 3.9.5 * @deprecated since 3.9.5
*/ */
getHeight(): number { getHeight(): number {
// return this.element.nativeElement.getBoundingClientRect().height; return (this.position == 'top' ?
this.getElementHeight(this.topButton?.nativeElement) :
this.getElementHeight(this.bottomButton?.nativeElement)) +
this.getElementHeight(this.infiniteScrollElement) +
this.getElementHeight(this.spinnerContainer?.nativeElement);
}
return (this.position == 'top' ? this.getElementHeight(this.topButton): this.getElementHeight(this.bottomButton)) + /**
this.getElementHeight(this.infiniteEl) + * Get the infinite scroll element.
this.getElementHeight(this.spinnerContainer); *
* @return Element or null.
*/
get infiniteScrollElement(): HTMLIonInfiniteScrollElement | null {
return this.hostElement.querySelector('ion-infinite-scroll');
} }
/** /**
@ -154,9 +164,9 @@ export class CoreInfiniteLoadingComponent implements OnChanges {
* @param element Element ref. * @param element Element ref.
* @return Height. * @return Height.
*/ */
protected getElementHeight(element?: ElementRef): number { protected getElementHeight(element?: HTMLElement | null): number {
if (element && element.nativeElement) { if (element) {
return CoreDomUtils.getElementHeight(element.nativeElement, true, true, true); return CoreDomUtils.getElementHeight(element, true, true, true);
} }
return 0; return 0;