diff --git a/src/addons/messages/pages/discussion/discussion.html b/src/addons/messages/pages/discussion/discussion.html index 6c5affb89..346fe8573 100644 --- a/src/addons/messages/pages/discussion/discussion.html +++ b/src/addons/messages/pages/discussion/discussion.html @@ -174,6 +174,6 @@

+ [placeholder]="'addon.messages.newmessage' | translate"> diff --git a/src/addons/messages/pages/discussion/discussion.page.ts b/src/addons/messages/pages/discussion/discussion.page.ts index 89e954d6a..cb683d327 100644 --- a/src/addons/messages/pages/discussion/discussion.page.ts +++ b/src/addons/messages/pages/discussion/discussion.page.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // 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 { AlertOptions } from '@ionic/core'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -76,6 +76,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView protected viewDestroyed = false; protected memberInfoObserver: CoreEventObserver; 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. conversation?: AddonMessagesConversationFormatted; // The conversation object (if it exists). @@ -109,10 +110,13 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView newMessages = 0; scrollElement?: HTMLElement; unreadMessageFrom = 0; + initialized = false; constructor( protected route: ActivatedRoute, + protected elementRef: ElementRef, ) { + this.hostElement = elementRef.nativeElement; this.siteId = CoreSites.getCurrentSiteId(); this.currentUserId = CoreSites.getCurrentSiteUserId(); this.groupMessagingEnabled = AddonMessages.isGroupMessagingEnabled(); @@ -162,6 +166,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView this.route.queryParams.subscribe(async (params) => { const oldConversationId = this.conversationId; const oldUserId = this.userId; + let forceScrollToBottom = false; this.conversationId = CoreNavigator.getRouteNumberParam('conversationId', { params }) || undefined; this.userId = CoreNavigator.getRouteNumberParam('userId', { params }) || undefined; this.showInfo = !params.hideInfo; @@ -169,13 +174,15 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView if (oldConversationId != this.conversationId || oldUserId != this.userId) { // Showing reload again can break animations. this.loaded = false; + this.initialized = false; + forceScrollToBottom = true; } this.showKeyboard = CoreNavigator.getRouteBooleanParam('showKeyboard', { params }) || false; await this.fetchData(); - this.scrollToBottom(); + this.scrollToBottom(forceScrollToBottom); }); } @@ -353,7 +360,6 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView CoreDomUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); } finally { this.checkCanDelete(); - this.resizeContent(); this.loaded = true; this.setPolling(); // Make sure we're polling messages. this.setContactRequestInfo(); @@ -481,10 +487,6 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView message.showTail = this.showTail(message, this.messages[index + 1]); }); - // Call resize to recalculate the dimensions. - // @todo probably not needed. - // this.content!.resize(); - // If we received a new message while using group messaging, force mark messages as read. const last = this.messages[this.messages.length - 1]; const forceMark = this.groupMessagingEnabled && last && last.useridfrom != this.currentUserId && this.lastMessage.text != '' @@ -537,7 +539,9 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView 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 elementRect = message.getBoundingClientRect(); @@ -1036,7 +1040,16 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView * @return Resolved when done. */ async loadPrevious(infiniteComplete?: () => void): Promise { - let infiniteHeight = this.infinite?.infiniteEl?.nativeElement.getBoundingClientRect().height || 0; + if (!this.initialized) { + // Don't load previous if the view isn't fully initialized. + // Don't put the initialized condition in the "enabled" input because then the load more is hidden and + // the scroll height changes when it appears. + infiniteComplete && infiniteComplete(); + + return; + } + + let infiniteHeight = this.infinite?.hostElement.getBoundingClientRect().height || 0; const scrollHeight = (this.scrollElement?.scrollHeight || 0); // If there is an ongoing fetch, wait for it to finish. @@ -1051,7 +1064,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView // Try to keep the scroll position. 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) { // The height of the infinite is different while spinner is shown. Add that difference. infiniteHeight = infiniteHeight - height; @@ -1073,10 +1086,8 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView /** * 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 { - setTimeout(() => { const newScrollHeight = (this.scrollElement?.scrollHeight || 0); @@ -1089,53 +1100,39 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView 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); } - /** - * 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. + * + * @param force Whether to force scroll to bottom. */ - scrollToBottom(): void { + async scrollToBottom(force = false): Promise { // Check if scroll is at bottom. If so, scroll bottom after rendering since there might be something new. - if (this.scrollBottom) { - // Need a timeout to leave time to the view to be rendered. - setTimeout(() => { - if (!this.viewDestroyed) { - this.content!.scrollToBottom(0); - } - }); + if (this.scrollBottom || force) { this.scrollBottom = false; // Reset the badge. this.setNewMessagesBadge(0); + + // Leave time for the view to be rendered. + await CoreUtils.nextTicks(5); + + if (!this.viewDestroyed) { + this.content!.scrollToBottom(0); + } + + if (force) { + this.initialized = true; + } } } @@ -1144,7 +1141,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView */ scrollToFirstUnreadMessage(): void { 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!, messages[messages.length - this.newMessages]); } diff --git a/src/addons/qtype/ddimageortext/classes/ddimageortext.ts b/src/addons/qtype/ddimageortext/classes/ddimageortext.ts index 506d44b3b..8a9f75b64 100644 --- a/src/addons/qtype/ddimageortext/classes/ddimageortext.ts +++ b/src/addons/qtype/ddimageortext/classes/ddimageortext.ts @@ -360,7 +360,7 @@ export class AddonQtypeDdImageOrTextQuestion { this.pollForImageLoad(); }); - this.resizeFunction = this.repositionDragsForQuestion.bind(this); + this.resizeFunction = this.windowResized.bind(this); window.addEventListener('resize', this.resizeFunction!); } @@ -679,6 +679,15 @@ export class AddonQtypeDdImageOrTextQuestion { } } + /** + * Window resized. + */ + async windowResized(): Promise { + await CoreDomUtils.waitForResizeDone(); + + this.repositionDragsForQuestion(); + } + } /** diff --git a/src/addons/qtype/ddmarker/classes/ddmarker.ts b/src/addons/qtype/ddmarker/classes/ddmarker.ts index 60a414db0..a8fca0b21 100644 --- a/src/addons/qtype/ddmarker/classes/ddmarker.ts +++ b/src/addons/qtype/ddmarker/classes/ddmarker.ts @@ -601,7 +601,7 @@ export class AddonQtypeDdMarkerQuestion { this.pollForImageLoad(); }); - this.resizeFunction = this.redrawDragsAndDrops.bind(this); + this.resizeFunction = this.windowResized.bind(this); window.addEventListener('resize', this.resizeFunction!); } @@ -869,6 +869,15 @@ export class AddonQtypeDdMarkerQuestion { } } + /** + * Window resized. + */ + async windowResized(): Promise { + await CoreDomUtils.waitForResizeDone(); + + this.redrawDragsAndDrops(); + } + } /** diff --git a/src/addons/qtype/ddwtos/classes/ddwtos.ts b/src/addons/qtype/ddwtos/classes/ddwtos.ts index ccc06cff2..7808cf602 100644 --- a/src/addons/qtype/ddwtos/classes/ddwtos.ts +++ b/src/addons/qtype/ddwtos/classes/ddwtos.ts @@ -214,7 +214,7 @@ export class AddonQtypeDdwtosQuestion { this.positionDragItems(); - this.resizeFunction = this.positionDragItems.bind(this); + this.resizeFunction = this.windowResized.bind(this); window.addEventListener('resize', this.resizeFunction!); } @@ -515,6 +515,15 @@ export class AddonQtypeDdwtosQuestion { }); } + /** + * Window resized. + */ + async windowResized(): Promise { + await CoreDomUtils.waitForResizeDone(); + + this.positionDragItems(); + } + } /** diff --git a/src/core/components/infinite-loading/infinite-loading.ts b/src/core/components/infinite-loading/infinite-loading.ts index 2a088dcf5..994f6ea43 100644 --- a/src/core/components/infinite-loading/infinite-loading.ts +++ b/src/core/components/infinite-loading/infinite-loading.ts @@ -13,7 +13,7 @@ // limitations under the License. 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 { CoreUtils } from '@services/utils/utils'; @@ -37,15 +37,16 @@ export class CoreInfiniteLoadingComponent implements OnChanges { @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; @ViewChild(IonInfiniteScroll) infiniteScroll?: IonInfiniteScroll; loadingMore = false; // Hide button and avoid loading more. + hostElement: HTMLElement; - constructor(protected element: ElementRef) { + constructor(protected element: ElementRef) { this.action = new EventEmitter(); + this.hostElement = element.nativeElement; } /** @@ -76,14 +77,14 @@ export class CoreInfiniteLoadingComponent implements OnChanges { await CoreUtils.nextTick(); // Calculate distance from edge. - const content = this.element.nativeElement.closest('ion-content') as IonContent; + const content = this.hostElement.closest('ion-content'); if (!content) { return; } const scrollElement = await content.getScrollElement(); - const infiniteHeight = this.element.nativeElement.getBoundingClientRect().height; + const infiniteHeight = this.hostElement.getBoundingClientRect().height; const scrollTop = scrollElement.scrollTop; const height = scrollElement.offsetHeight; const threshold = height * THRESHOLD; @@ -141,11 +142,20 @@ export class CoreInfiniteLoadingComponent implements OnChanges { * @deprecated since 3.9.5 */ 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) + - this.getElementHeight(this.spinnerContainer); + /** + * Get the infinite scroll element. + * + * @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. * @return Height. */ - protected getElementHeight(element?: ElementRef): number { - if (element && element.nativeElement) { - return CoreDomUtils.getElementHeight(element.nativeElement, true, true, true); + protected getElementHeight(element?: HTMLElement | null): number { + if (element) { + return CoreDomUtils.getElementHeight(element, true, true, true); } return 0; diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 71cf7fddc..19a864a24 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -1861,6 +1861,32 @@ export class CoreDomUtilsProvider { CoreForms.triggerFormSubmittedEvent(formRef, online, siteId); } + /** + * In iOS the resize event is triggered before the window size changes. Wait for the size to change. + * + * @param windowWidth Initial window width. + * @param windowHeight Initial window height. + * @param retries Number of retries done. + */ + async waitForResizeDone(windowWidth?: number, windowHeight?: number, retries = 0): Promise { + if (!CoreApp.isIOS()) { + return; // Only wait in iOS. + } + + windowWidth = windowWidth || window.innerWidth; + windowHeight = windowHeight || window.innerHeight; + + if (windowWidth != window.innerWidth || windowHeight != window.innerHeight || retries >= 10) { + // Window size changed or max number of retries reached, stop. + return; + } + + // Wait a bit and try again. + await CoreUtils.wait(50); + + return this.waitForResizeDone(windowWidth, windowHeight, retries+1); + } + } /** diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index ddf711a7e..6c5ca7826 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -1666,6 +1666,15 @@ export class CoreUtilsProvider { return this.wait(0); } + /** + * Wait until several next ticks. + */ + async nextTicks(numTicks = 0): Promise { + for (let i = 0; i < numTicks; i++) { + await this.wait(0); + } + } + /** * Given some options, check if a file should be opened with showOpenWithDialog. *