In this step we will add the chat view and the ability to send messages.
We still don’t have an identity for each user, we will add it later, but we can still send messages to existing chats.
So just like any other page, let’s begin by adding a very basic view with the chat's details:
1
2
3
4
5
<ion-view title="{{ chat.data.name }}">
<ion-nav-buttons side="right">
<button class="button button-clear"><img class="header-picture" ng-src="{{ chat.data.picture }}"></button>
</ion-nav-buttons>
</ion-view>
Now we need to implement the logic in the controller, so let’s create it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Chats } from 'api/collections';
import { Controller } from 'angular-ecmascript/module-helpers';
export default class ChatCtrl extends Controller {
static $inject = ['$stateParams']
constructor() {
super(...arguments);
this.chatId = this.$stateParams.chatId;
this.helpers({
data() {
return Chats.findOne(this.chatId);
}
});
}
}
ChatCtrl.$name = 'ChatCtrl';
4
5
6
7
8
9
10
17
18
19
20
21
22
23
import StatusBar from 'cordova/status-bar';
import Loader from 'angular-ecmascript/module-loader';
import ChatCtrl from './controllers/chat.controller';
import ChatsCtrl from './controllers/chats.controller';
import CalendarFilter from './filters/calendar.filter';
import RoutesConfig from './routes';
...some lines skipped...
]);
new Loader(App)
.load(ChatCtrl)
.load(ChatsCtrl)
.load(CalendarFilter)
.load(RoutesConfig);
We used the $statePrams
provider to get the id of the chat, and then we used the Chats
collection to find the data related to the it. The function findOne()
takes a query as a parameter and returns a single document. Just like collections in MongoDB
.
Now that we have the view and the controller let's connect them by adding the appropriate route state:
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
controller: 'ChatsCtrl as chats'
}
}
})
.state('tab.chat', {
url: '/chats/:chatId',
views: {
'tab-chats': {
templateUrl: 'templates/chat.html',
controller: 'ChatCtrl as chat'
}
}
});
this.$urlRouterProvider.otherwise('tab/chats');
And all is left to do is to link these two:
3
4
5
6
7
8
9
10
<ion-list>
<ion-item ng-repeat="chat in chats.data | orderBy:'-lastMessage.timestamp'"
class="item-chat item-remove-animate item-avatar item-icon-right"
type="item-text-wrap"
href="#/tab/chats/{{ chat._id }}">
<img ng-src="{{ chat.picture }}">
<h2>{{ chat.name }}</h2>
<p>{{ chat.lastMessage.text }}</p>
Now each time we will click on a chat item from the menu, we should be navigating to it.
Let’s create a new scss
file to our Chat
and fix the image style so it won't look silly:
1
2
3
4
5
6
7
.header-picture {
max-width: 33px;
max-height: 33px;
width: 100%;
height: 100%;
border-radius: 50%;
}
21
22
23
24
25
// Include all of Ionic
@import "www/lib/ionic/scss/ionic";
@import "chat";
@import "chats";
Our next step is about getting the chat messages on the controller, we will add another helper, but instead of using the whole collection we will fetch only the relevant messages for the current chat:
1
2
3
4
10
11
12
13
14
15
16
17
18
import { Chats, Messages } from 'api/collections';
import { Controller } from 'angular-ecmascript/module-helpers';
export default class ChatCtrl extends Controller {
...some lines skipped...
this.chatId = this.$stateParams.chatId;
this.helpers({
messages() {
return Messages.find({ chatId: this.chatId });
},
data() {
return Chats.findOne(this.chatId);
}
And now to add it to the view, we use ng-repeat
to iterate the messages:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<ion-nav-buttons side="right">
<button class="button button-clear"><img class="header-picture" ng-src="{{ chat.data.picture }}"></button>
</ion-nav-buttons>
<ion-content class="chat" delegate-handle="chatScroll">
<div class="message-list">
<div ng-repeat="message in chat.messages" class="message-wrapper">
<div class="message" ng-class-even="'message-mine'" ng-class-odd="'message-other'">
<div class="message-text">{{ message.text }}</div>
<span class="message-timestamp">{{ message.timestamp }}</span>
</div>
</div>
</div>
</ion-content>
</ion-view>
Now that it is well functioning, let's polish our Chats
's looking by adding some style to our newly created messages:
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
width: 100%;
height: 100%;
border-radius: 50%;
}
.chat {
background-image: url(/img/chat-background.jpg);
background-color: #E0DAD6;
background-repeat: no-repeat;
background-size: 100%;
}
.message-list {
margin-top: 12px;
padding: 0 5%;
}
.message-wrapper {
margin-bottom: 9px;
&::after {
content: "";
display: table;
clear: both;
}
}
.message {
display: inline-block;
position: relative;
max-width: 236px;
border-radius: 7px;
box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
&.message-mine {
float: right;
background-color: #DCF8C6;
}
&.message-other {
float: left;
background-color: #FFF;
}
&.message-other::before, &.message-mine::before, {
content: "";
position: absolute;
bottom: 3px;
width: 12px;
height: 19px;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: contain;
}
&.message-other::before {
left: -11px;
background-image: url(/img/message-other.png)
}
&.message-mine::before {
right: -11px;
background-image: url(/img/message-mine.png)
}
.message-text {
padding: 5px 7px;
word-wrap: break-word;
&::after {
content: " \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0";
display: inline;
}
}
.message-timestamp {
position: absolute;
bottom: 2px;
right: 7px;
color: gray;
font-size: 12px;
}
}
Also, this stylesheet uses some assets located in the www/img
dir, so inorder for the stylesheet to work properly you'll need to copy the files located here.
After doing so, our app should look like this:
{{tutorialImage 'ionic' '4.png' 500}}
Now we just need to take care of the message timestamp and format it.
We will use Moment
like before, but now let's add another package called angular-moment that provides us the UI filters.
So adding the package is just like any other package we added so far. First, we will install it:
$ npm install angular-moment --save
5
6
7
8
9
10
11
"dependencies": {
"angular-ecmascript": "0.0.3",
"angular-meteor": "^1.3.11",
"angular-moment": "^1.0.0",
"babel": "^6.5.2",
"babel-core": "^6.7.6",
"babel-loader": "^6.2.4",
And then we will load it:
9
10
11
12
13
14
import 'script!lib/ionic/js/ionic-angular';
import 'script!meteor-client-side/dist/meteor-client-side.bundle';
import 'script!angular-meteor/dist/angular-meteor.bundle';
import 'script!angular-moment/angular-moment';
// app
import './app';
13
14
15
16
17
18
19
Angular.module(App, [
'angular-meteor',
'angularMoment',
'ionic'
]);
NOTE: Because it’s an
Angular
extension, we loaded its dependency in our module definition.
Now that we have angular-moment
ready to use, we will use a filter provided by it in our view:
8
9
10
11
12
13
14
<div ng-repeat="message in chat.messages" class="message-wrapper">
<div class="message" ng-class-even="'message-mine'" ng-class-odd="'message-other'">
<div class="message-text">{{ message.text }}</div>
<span class="message-timestamp">{{ message.timestamp | amDateFormat: 'HH:MM' }}</span>
</div>
</div>
</div>
Our messages are set, but there is one really important feature missing and that's sending messages. Let's implement our message editor.
We will start with the view itself. We will add an input for editing our messages, a send
button and some icons for sending images and sound recordings, whom logic won't be implemented in this tutorial since we only wanna focus on the messaging system.
The ionic-footer-bar
directive provides a perfect solution for placing stuff under our content, let's use it:
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
</div>
</div>
</ion-content>
<ion-footer-bar keyboard-attach class="bar-stable footer-chat item-input-inset">
<button class="button button-clear button-icon button-positive icon ion-ios-upload-outline"></button>
<label class="item-input-wrapper">
<input ng-model="chat.message"
dir="auto"
type="text"/>
</label>
<span ng-if="chat.message.length > 0">
<button ng-click="chat.sendMessage()" class="button button-clear button-positive">Send</button>
</span>
<span ng-if="!chat.message || chat.message.length === 0">
<button class="button button-clear button-icon button-positive icon ion-ios-camera-outline"></button>
<i class="buttons-seperator icon ion-android-more-vertical"></i>
<button class="button button-clear button-icon button-positive icon ion-ios-mic-outline"></button>
</span>
</ion-footer-bar>
</ion-view>
To improve the user experience in our app, we want some extra events to our input because we want to move it up when the keyboard comes from the bottom of the screen and we want to know if return
(aka Enter
) was pressed.
We will implement a new directive that extends the regular input
tag and add those events to the directive:
1
2
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { Directive } from 'angular-ecmascript/module-helpers';
export default class InputDirective extends Directive {
static $inject = ['$timeout']
static $name = 'input'
restrict = 'E'
scope = {
'returnClose': '=',
'onReturn': '&',
'onFocus': '&',
'onBlur': '&'
}
link(scope, element) {
element.bind('focus', (e) => {
if (!scope.onFocus) return;
this.$timeout(() => {
scope.onFocus();
});
});
element.bind('blur', (e) => {
if (!scope.onBlur) return;
this.$timeout(() => {
scope.onBlur();
});
});
element.bind('keydown', (e) => {
if (e.which != 13) return;
if (scope.returnClose) {
element[0].blur();
}
if (scope.onReturn) {
this.$timeout(() => {
scope.onReturn();
});
}
});
}
}
6
7
8
9
10
11
12
21
22
23
24
25
26
27
import ChatCtrl from './controllers/chat.controller';
import ChatsCtrl from './controllers/chats.controller';
import InputDirective from './directives/input.directive';
import CalendarFilter from './filters/calendar.filter';
import RoutesConfig from './routes';
...some lines skipped...
new Loader(App)
.load(ChatCtrl)
.load(ChatsCtrl)
.load(InputDirective)
.load(CalendarFilter)
.load(RoutesConfig);
And now we can use those events in our view:
20
21
22
23
24
25
26
27
28
29
<label class="item-input-wrapper">
<input ng-model="chat.message"
dir="auto"
type="text"
on-return="chat.sendMessage(); chat.closeKeyboard()"
on-focus="chat.inputUp()"
on-blur="chat.inputDown()"/>
</label>
<span ng-if="chat.message.length > 0">
And implement the controller methods which handle those events:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
49
50
51
52
53
54
import Ionic from 'ionic';
import Keyboard from 'cordova/keyboard';
import { Chats, Messages } from 'api/collections';
import { Controller } from 'angular-ecmascript/module-helpers';
export default class ChatCtrl extends Controller {
static $inject = ['$stateParams', '$timeout', '$ionicScrollDelegate']
constructor() {
super(...arguments);
this.chatId = this.$stateParams.chatId;
this.isIOS = Ionic.Platform.isWebView() && Ionic.Platform.isIOS();
this.helpers({
messages() {
...some lines skipped...
}
});
}
inputUp () {
if (this.isIOS) {
this.keyboardHeight = 216;
}
this.scrollBottom(true);
}
inputDown () {
if (this.isIOS) {
this.keyboardHeight = 0;
}
this.$ionicScrollDelegate.$getByHandle('chatScroll').resize();
}
closeKeyboard() {
if (Keyboard) {
Keyboard.close();
}
}
scrollBottom(animate) {
this.$timeout(() => {
this.$ionicScrollDelegate.$getByHandle('chatScroll').scrollBottom(animate);
}, 300);
}
}
ChatCtrl.$name = 'ChatCtrl';
We will also add some css
to this view:
45
46
47
48
49
50
51
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
background-color: #FFF;
}
&.message-other::before, &.message-mine::before {
content: "";
position: absolute;
bottom: 3px;
...some lines skipped...
color: gray;
font-size: 12px;
}
}
.footer-chat {
.item-input-wrapper {
background-color: #FFF;
}
.button.button-icon {
margin: 0 10px;
}
.buttons-seperator {
color: gray;
font-size: 18px;
line-height: 32px;
}
}
So now when the user focuses on the input, it should pop up.
This is what we got so far:
{{tutorialImage 'ionic' '5.png' 500}}
So now it’s time to implement the sendMessage()
in our controller, which is responsible for the logic of sending a message.
We will use Scope.callMethod()
in order to call that method on the server side:
1
2
3
4
5
6
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import Ionic from 'ionic';
import Keyboard from 'cordova/keyboard';
import { _ } from 'meteor/underscore';
import { Chats, Messages } from 'api/collections';
import { Controller } from 'angular-ecmascript/module-helpers';
...some lines skipped...
});
}
sendMessage() {
if (_.isEmpty(this.message)) return;
this.callMethod('newMessage', {
text: this.message,
chatId: this.chatId
});
delete this.message;
}
inputUp () {
if (this.isIOS) {
this.keyboardHeight = 216;
Now let’s create our api
method in a file called methods.js
:
1
2
3
4
5
6
7
8
9
10
11
12
13
import { Meteor } from 'meteor/meteor';
import { Chats, Messages } from './collections';
Meteor.methods({
newMessage(message) {
message.timestamp = new Date();
const messageId = Messages.insert(message);
Chats.update(message.chatId, { $set: { lastMessage: message } });
return messageId;
}
});
And we also need to load them in our client, since they are called twice, once in our client (As a validation and smoother experience without refreshing) and once in our server (For security and data handling):
10
11
12
13
14
15
16
import 'script!meteor-client-side/dist/meteor-client-side.bundle';
import 'script!angular-meteor/dist/angular-meteor.bundle';
import 'script!angular-moment/angular-moment';
// api
import 'api/methods';
// app
import './app';
We would also like to validate some data sent to methods we define. Meteor
provides us with a useful package named check
that validates data types and scheme.
We will add it to our server using the following commands:
$ cd api
$ meteor add check
NOTE:
meteor-client-side
is already provided with thecheck
package so no need to require it again.
Now let’s use it in the newMessage()
method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { Chats, Messages } from './collections';
Meteor.methods({
newMessage(message) {
check(message, {
text: String,
chatId: String
});
message.timestamp = new Date();
const messageId = Messages.insert(message);
Now that it's ready you can go ahead and send a message and view it on the screen. It should look like this: