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.
*