In this step, we will implement a lazy-loading mechanism in the MessagesPage
. Lazy loading means that only the necessary data will be loaded once we're promoted to the corresponding view, and it will keep loading, but gradually. In the MessagesPage
case, we will only be provided with several messages once we enter the view, enough messages to fill all of it, and as we scroll up, we will provided with more messages. This way we can have a smooth experience, without the cost of fetching the entire messages collection. We will start by limiting our messages
subscription into 30 documents:
15
16
17
18
19
20
21
22
23
25
26
27
28
29
30
31
32
});
});
Meteor.publish('messages', function(
chatId: string,
messagesBatchCounter: number): Mongo.Cursor<Message> {
if (!this.userId || !chatId) {
return;
}
...some lines skipped...
return Messages.collection.find({
chatId
}, {
sort: { createdAt: -1 },
limit: 30 * messagesBatchCounter
});
});
As we said, we will be fetching more and more messages gradually, so we will need to have a counter in the component which will tell us the number of the batch we would like to fetch in our next scroll:
23
24
25
26
27
28
29
66
67
68
69
70
71
72
73
senderId: string;
loadingMessages: boolean;
messagesComputation: Subscription;
messagesBatchCounter: number = 0;
constructor(
navParams: NavParams,
...some lines skipped...
this.scrollOffset = this.scroller.scrollHeight;
MeteorObservable.subscribe('messages',
this.selectedChat._id,
++this.messagesBatchCounter
).subscribe(() => {
// Keep tracking changes in the dataset and re-render the view
if (!this.messagesComputation) {
By now, whether you noticed or not, we have some sort of a limitation which we have to solve. Let's say we've fetched all the messages available for the current chat, and we keep scrolling up, the component will keep attempting to fetch more messages, but it doesn't know that it reached the limit. Because of that, we will need to know the total number of messages so we will know when to stop the lazy-loading mechanism. To solve this issue, we will begin with implementing a method which will retrieve the number of total messages for a provided chat:
90
91
92
93
94
95
96
97
type: type
})
};
},
countMessages(): number {
return Messages.collection.find().count();
}
});
Now, whenever we fetch a new messages-batch we will check if we reached the total messages limit, and if so, we will stop listening to the scroll event:
6
7
8
9
10
11
12
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import * as moment from 'moment';
import { _ } from 'meteor/underscore';
import { MessagesOptionsComponent } from './messages-options';
import { Subscription, Observable, Subscriber } from 'rxjs';
@Component({
selector: 'messages-page',
...some lines skipped...
ngOnInit() {
this.autoScroller = this.autoScroll();
this.subscribeMessages();
// Get total messages count in database so we can have an indication of when to
// stop the auto-subscriber
MeteorObservable.call('countMessages').subscribe((messagesCount: number) => {
Observable
// Chain every scroll event
.fromEvent(this.scroller, 'scroll')
// Remove the scroll listener once all messages have been fetched
.takeUntil(this.autoRemoveScrollListener(messagesCount))
// Filter event handling unless we're at the top of the page
.filter(() => !this.scroller.scrollTop)
// Prohibit parallel subscriptions
.filter(() => !this.loadingMessages)
// Invoke the messages subscription once all the requirements have been met
.forEach(() => this.subscribeMessages());
});
}
ngOnDestroy() {
...some lines skipped...
});
}
// Removes the scroll listener once all messages from the past were fetched
autoRemoveScrollListener<T>(messagesCount: number): Observable<T> {
return Observable.create((observer: Subscriber<T>) => {
Messages.find().subscribe({
next: (messages) => {
// Once all messages have been fetched
if (messagesCount !== messages.length) {
return;
}
// Signal to stop listening to the scroll event
observer.next();
// Finish the observation to prevent unnecessary calculations
observer.complete();
},
error: (e) => {
observer.error(e);
}
});
});
}
showOptions(): void {
const popover = this.popoverCtrl.create(MessagesOptionsComponent, {
chat: this.selectedChat
Now we're gonna implement a search-bar, in the NewChatComponent
.
Let's start by implementing the logic using RxJS
. We will use a BehaviorSubject
which will store the search pattern entered in the search bar, and we will be able to detect changes in its value using the Observable
API; So whenever the search pattern is being changed, we will update the users list by re-subscribing to the users
subscription:
4
5
6
7
8
9
10
11
12
13
14
15
16
17
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
58
59
60
61
62
63
64
65
66
67
68
69
import { AlertController, ViewController } from 'ionic-angular';
import { MeteorObservable } from 'meteor-rxjs';
import { _ } from 'meteor/underscore';
import { Observable, Subscription, BehaviorSubject } from 'rxjs';
@Component({
selector: 'new-chat',
templateUrl: 'new-chat.html'
})
export class NewChatComponent implements OnInit {
searchPattern: BehaviorSubject<any>;
senderId: string;
users: Observable<User[]>;
usersSubscription: Subscription;
...some lines skipped...
private viewCtrl: ViewController
) {
this.senderId = Meteor.userId();
this.searchPattern = new BehaviorSubject(undefined);
}
ngOnInit() {
this.observeSearchBar();
}
updateSubscription(newValue) {
this.searchPattern.next(newValue);
}
observeSearchBar(): void {
this.searchPattern.asObservable()
// Prevents the search bar from being spammed
.debounce(() => Observable.timer(1000))
.forEach(() => {
if (this.usersSubscription) {
this.usersSubscription.unsubscribe();
}
this.usersSubscription = this.subscribeUsers();
});
}
addChat(user): void {
...some lines skipped...
});
}
subscribeUsers(): Subscription {
// Fetch all users matching search pattern
const subscription = MeteorObservable.subscribe('users', this.searchPattern.getValue());
const autorun = MeteorObservable.autorun();
return Observable.merge(subscription, autorun).subscribe(() => {
this.users = this.findUsers();
});
}
Note how we used the debounce
method to prevent subscription spamming. Let's add the template for the search-bar in the NewChat
view, and bind it to the corresponding data-models and methods in the component:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
18
19
20
21
22
23
24
<ion-header>
<ion-toolbar *ngIf="searching" color="whatsapp">
<ion-searchbar
autofocus
class="seach-bar"
color="whatsapp"
[showCancelButton]="true"
(ionInput)="updateSubscription($event.target.value); searching = true;"
(ionClear)="updateSubscription(undefined); searching = false;">
</ion-searchbar>
</ion-toolbar>
<ion-toolbar *ngIf="!searching" color="whatsapp">
<ion-title>New Chat</ion-title>
<ion-buttons left>
...some lines skipped...
</ion-buttons>
<ion-buttons end>
<button ion-button class="search-button" (click)="searching = true"><ion-icon name="search"></ion-icon></button>
</ion-buttons>
</ion-toolbar>
</ion-header>
Now we will modify the users
subscription to accept the search-pattern, which will be used as a filter for the result-set;
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Messages } from './collections/messages';
import { Chats } from './collections/chats';
Meteor.publishComposite('users', function(
pattern: string
): PublishCompositeConfig<User> {
if (!this.userId) {
return;
}
let selector = {};
if (pattern) {
selector = {
'profile.name': { $regex: pattern, $options: 'i' }
};
}
return {
find: () => {
return Users.collection.find(selector, {
fields: { profile: 1 },
limit: 15
});
}
};
});
Meteor.publish('messages', function(