MOBILE-3382 messages: Show badge to scroll bottom

main
Pau Ferrer Ocaña 2020-05-12 17:53:10 +02:00
parent a9aba7739b
commit db6f9dadf7
3 changed files with 130 additions and 10 deletions

View File

@ -41,7 +41,7 @@
</h6> </h6>
<ion-chip class="addon-messages-unreadfrom" *ngIf="unreadMessageFrom && message.id == unreadMessageFrom" color="light"> <ion-chip class="addon-messages-unreadfrom" *ngIf="unreadMessageFrom && message.id == unreadMessageFrom" color="light">
<ion-label>{{ 'addon.messages.newmessages' | translate:{$a: title} }}</ion-label> <ion-label>{{ 'addon.messages.newmessages' | translate }}</ion-label>
<ion-icon name="arrow-round-down"></ion-icon> <ion-icon name="arrow-round-down"></ion-icon>
</ion-chip> </ion-chip>
@ -68,6 +68,13 @@
</ion-item> </ion-item>
</ng-container> </ng-container>
</ion-list> </ion-list>
<!-- Scroll bottom. -->
<ion-fab core-fab bottom end *ngIf="newMessages > 0">
<button ion-fab mini (click)="scrollToFirstUnreadMessage(true)" color="light" [attr.aria-label]="'addon.messages.newmessages' | translate">
<ion-icon name="arrow-round-down"></ion-icon>
<span class="core-discussion-messages-badge">{{ newMessages }}</span>
</button>
</ion-fab>
<core-empty-box *ngIf="!messages || messages.length <= 0" icon="chatbubbles" [message]="'addon.messages.nomessagesfound' | translate"></core-empty-box> <core-empty-box *ngIf="!messages || messages.length <= 0" icon="chatbubbles" [message]="'addon.messages.nomessagesfound' | translate"></core-empty-box>
</core-loading> </core-loading>
</ion-content> </ion-content>

View File

@ -4,6 +4,9 @@ $item-message-note-text: $gray-dark !default;
$item-message-note-font-size: 75% !default; $item-message-note-font-size: 75% !default;
$item-message-mine-bg: $gray-light !default; $item-message-mine-bg: $gray-light !default;
$core-discussion-messages-badge: $core-color !default;
$core-discussion-messages-badge-text: $white !default;
@mixin message-page { @mixin message-page {
ion-content { ion-content {
background-color: $gray-lighter !important; background-color: $gray-lighter !important;
@ -194,6 +197,28 @@ $item-message-mine-bg: $gray-light !default;
border-top-right-radius: 0; border-top-right-radius: 0;
border-top-left-radius: 0; border-top-left-radius: 0;
} }
.has-fab .scroll-content {
padding-bottom: 0;
}
ion-fab button {
overflow: visible;
position: relative;
.core-discussion-messages-badge {
position: absolute;
border-radius: 50%;
color: $core-discussion-messages-badge-text;
background-color: $core-discussion-messages-badge;
display: block;
line-height: 20px;
height: 20px;
width: 20px;
right: -6px;
top: -6px;
}
}
} }

View File

@ -65,6 +65,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
protected viewDestroyed = false; protected viewDestroyed = false;
protected memberInfoObserver: any; protected memberInfoObserver: any;
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 scrollListener;
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).
@ -95,6 +96,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
isSelf = false; isSelf = false;
muteEnabled = false; muteEnabled = false;
muteIcon = 'volume-off'; muteIcon = 'volume-off';
newMessages = 0;
constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, navParams: NavParams, constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, navParams: NavParams,
private userProvider: CoreUserProvider, private navCtrl: NavController, private messagesSync: AddonMessagesSyncProvider, private userProvider: CoreUserProvider, private navCtrl: NavController, private messagesSync: AddonMessagesSyncProvider,
@ -134,6 +136,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
this.fetchData(); this.fetchData();
} }
}, this.siteId); }, this.siteId);
this.scrollListener = this.scrollListenerFunction.bind(this);
} }
/** /**
@ -141,21 +145,26 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
* *
* @param message Message to be added. * @param message Message to be added.
* @param keep If set the keep flag or not. * @param keep If set the keep flag or not.
* @return If message is not mine and was recently added.
*/ */
protected addMessage(message: AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted, protected addMessage(message: AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted,
keep: boolean = true): void { keep: boolean = true): boolean {
/* Create a hash to identify the message. The text of online messages isn't reliable because it can have random data /* Create a hash to identify the message. The text of online messages isn't reliable because it can have random data
like VideoJS ID. Try to use id and fallback to text for offline messages. */ like VideoJS ID. Try to use id and fallback to text for offline messages. */
message.hash = Md5.hashAsciiStr(String(message.id || message.text || '')) + '#' + message.timecreated + '#' + message.hash = Md5.hashAsciiStr(String(message.id || message.text || '')) + '#' + message.timecreated + '#' +
message.useridfrom; message.useridfrom;
let added = false;
if (typeof this.keepMessageMap[message.hash] === 'undefined') { if (typeof this.keepMessageMap[message.hash] === 'undefined') {
// Message not added to the list. Add it now. // Message not added to the list. Add it now.
this.messages.push(message); this.messages.push(message);
added = message.useridfrom != this.currentUserId;
} }
// Message needs to be kept in the list. // Message needs to be kept in the list.
this.keepMessageMap[message.hash] = keep; this.keepMessageMap[message.hash] = keep;
return added;
} }
/** /**
@ -306,9 +315,10 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
/** /**
* Convenience function to fetch messages. * Convenience function to fetch messages.
* *
* @param messagesAreNew If messages loaded are new messages.
* @return Resolved when done. * @return Resolved when done.
*/ */
protected fetchMessages(): Promise<void> { protected fetchMessages(messagesAreNew: boolean = true): Promise<void> {
this.loadMoreError = false; this.loadMoreError = false;
if (this.messagesBeingSent > 0) { if (this.messagesBeingSent > 0) {
@ -348,7 +358,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
}); });
} }
}).then((messages: (AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted)[]) => { }).then((messages: (AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted)[]) => {
this.loadMessages(messages); this.loadMessages(messages, messagesAreNew);
}).finally(() => { }).finally(() => {
this.fetching = false; this.fetching = false;
}); });
@ -357,10 +367,11 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
/** /**
* Format and load a list of messages into the view. * Format and load a list of messages into the view.
* *
* @param messagesAreNew If messages loaded are new messages.
* @param messages Messages to load. * @param messages Messages to load.
*/ */
protected loadMessages(messages: (AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted)[]) protected loadMessages(messages: (AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted)[],
: void { messagesAreNew: boolean = true): void {
if (this.viewDestroyed) { if (this.viewDestroyed) {
return; return;
@ -380,9 +391,14 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
} }
// Add new messages to the list and mark the messages that should still be displayed. // Add new messages to the list and mark the messages that should still be displayed.
messages.forEach((message) => { const newMessages = messages.reduce((val, message) => {
this.addMessage(message); return val + (this.addMessage(message) ? 1 : 0);
}); }, 0);
// Set the new badges message if we're loading new messages.
if (messagesAreNew) {
this.setNewMessagesBadge(this.newMessages + newMessages);
}
// Remove messages that shouldn't be in the list anymore. // Remove messages that shouldn't be in the list anymore.
for (const hash in this.keepMessageMap) { for (const hash in this.keepMessageMap) {
@ -414,6 +430,63 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
this.markMessagesAsRead(forceMark); this.markMessagesAsRead(forceMark);
} }
/**
* Set the new message badge number and set scroll listener if needed.
*
* @param addMessages NUmber of messages still to be read.
*/
protected setNewMessagesBadge(addMessages: number): void {
if (this.newMessages == 0 && addMessages > 0) {
// Setup scrolling.
this.content.getScrollElement().addEventListener('scroll', this.scrollListener);
this.scrollListenerFunction();
} else if (this.newMessages > 0 && addMessages == 0) {
// Remove scrolling.
this.content.getScrollElement().removeEventListener('scroll', this.scrollListener);
}
this.newMessages = addMessages;
}
/**
* The scroll was moved. Update new messages count.
*/
protected scrollListenerFunction(): void {
if (this.newMessages > 0) {
const scrollBottom = this.domUtils.getScrollTop(this.content) + this.domUtils.getContentHeight(this.content);
const scrollHeight = this.domUtils.getScrollHeight(this.content);
if (scrollBottom > scrollHeight - 40) {
// At the bottom, reset.
this.setNewMessagesBadge(0);
return;
}
const scrollElRect = this.content.getScrollElement().getBoundingClientRect();
const scrollBottomPos = (scrollElRect && scrollElRect.bottom) || 0;
if (scrollBottomPos == 0) {
return;
}
const messages = Array.from(document.querySelectorAll('.addon-message-not-mine')).slice(-this.newMessages).reverse();
const newMessagesUnread = messages.findIndex((message, index) => {
const elementRect = message.getBoundingClientRect();
if (!elementRect) {
return false;
}
return elementRect.bottom <= scrollBottomPos;
});
if (newMessagesUnread > 0 && newMessagesUnread < this.newMessages) {
this.setNewMessagesBadge(newMessagesUnread);
}
}
}
/** /**
* Get the conversation. * Get the conversation.
* *
@ -887,7 +960,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
return this.waitForFetch().finally(() => { return this.waitForFetch().finally(() => {
this.pagesLoaded++; this.pagesLoaded++;
this.fetchMessages().then(() => { this.fetchMessages(false).then(() => {
// Try to keep the scroll position. // Try to keep the scroll position.
const scrollBottom = scrollHeight - this.domUtils.getScrollTop(this.content); const scrollBottom = scrollHeight - this.domUtils.getScrollTop(this.content);
@ -972,6 +1045,20 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
} }
}); });
this.scrollBottom = false; this.scrollBottom = false;
// Reset the badge.
this.setNewMessagesBadge(0);
}
}
/**
* Scroll to the first new unread message.
*/
scrollToFirstUnreadMessage(): void {
if (this.newMessages > 0) {
const messages = Array.from(document.querySelectorAll('.addon-message-not-mine'));
this.domUtils.scrollToElement(this.content, <HTMLElement> messages[messages.length - this.newMessages]);
} }
} }
@ -987,6 +1074,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
this.showDelete = false; this.showDelete = false;
this.scrollBottom = true; this.scrollBottom = true;
this.setNewMessagesBadge(0);
message = { message = {
id: null, id: null,