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, first we need to add a route and a state.
Let's call it chat
and we will load a template and a controller which we will add later.
1
2
3
4
5
6
7
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { Config } from 'angular-ecmascript/module-helpers';
import chatsTemplateUrl from '../templates/chats.html';
import chatTemplateUrl from '../templates/chat.html';
import tabsTemplateUrl from '../templates/tabs.html';
export default class RoutesConfig extends Config {
...some lines skipped...
controller: 'ChatsCtrl as chats'
}
}
})
.state('tab.chat', {
url: '/chats/:chatId',
views: {
'tab-chats': {
templateUrl: chatTemplateUrl,
controller: 'ChatCtrl as chat'
}
}
});
this.$urlRouterProvider.otherwise('tab/chats');
Let's add a very basic view with the chat's details. The file will be located in client/templates/chat.html
:
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 in client/scripts/controllers/chat.controller.js
and call it ChatCtrl
.
We will use the $stateParams
provider to get the chat id and then we will define a helper that will help us fetch the chat that we want.
So in order to do that we shall define a helper named chat
, and use findOne()
to fetch the wanted document.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Controller } from 'angular-ecmascript/module-helpers';
import { Chats } from '../../../lib/collections';
export default class ChatCtrl extends Controller {
constructor() {
super(...arguments);
this.chatId = this.$stateParams.chatId;
this.helpers({
data() {
return Chats.findOne(this.chatId);
}
});
}
}
ChatCtrl.$name = 'ChatCtrl';
ChatCtrl.$inject = ['$stateParams'];
10
11
12
13
14
15
16
24
25
26
27
28
29
30
// Modules
import ChatsCtrl from '../controllers/chats.controller';
import ChatCtrl from '../controllers/chat.controller';
import CalendarFilter from '../filters/calendar.filter';
import RoutesConfig from '../routes';
...some lines skipped...
new Loader(App)
.load(ChatsCtrl)
.load(ChatCtrl)
.load(CalendarFilter)
.load(RoutesConfig);
So now that we have the chat view and controller, 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 let's add some css
rules and let's add the messages view.
Let's create a new sass
file for our view at client/styles/chat.scss
, 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%;
}
Our next step is about getting the chat's messages in the controller, we will add another helper, but instead of using the whole collection we will fetch only the relevant messages:
1
2
3
4
5
8
9
10
11
12
13
14
15
16
import { Controller } from 'angular-ecmascript/module-helpers';
import { Chats, Messages } from '../../../lib/collections';
export default class ChatCtrl extends Controller {
constructor() {
...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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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-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>
As for now we do not have an identity for each user or message, so we will just use odd
/even
classes and this will be the indication for which message is mine and which isn't. In the next step we will add the authentication and each message will be related to a user.
Now we will add some css
to the messages list:
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(/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(/message-other.png)
}
&.message-mine::before {
right: -11px;
background-image: url(/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;
}
}
We also need to add some Whatsapp
assets so it would look more similar.
Note that the images are under public/
folder so we can use them in the client side from the root directory (in the css
file).
You can copy them from here.
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:
$ meteor npm install angular-moment --save
And since it's an AngularJS
extension, we need to add a dependency in our module definition:
1
2
3
4
5
6
7
20
21
22
23
24
25
26
// Libs
import 'angular-animate';
import 'angular-meteor';
import 'angular-moment';
import 'angular-sanitize';
import 'angular-ui-router';
import 'ionic-scripts';
...some lines skipped...
// App
Angular.module(App, [
'angular-meteor',
'angularMoment',
'ionic'
]);
And now we will use a filter from this package in our view:
7
8
9
10
11
12
13
<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>
Just like Whatsapp
...
Our next step is about adding the input for adding a new message to the chat, we need to add an input at the bottom of the view. ion-footer-bar
provides a perfect solution for that.
So we will add an input, a send button and some icons for sending images and sound recordings (For now we will live them just so our view would look reach without any logic implemented behind).
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
</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>
Let's add the data
object to our controller, and add a stub method for sendMessage()
, which will be implemented further in this tutorial.
16
17
18
19
20
21
22
23
24
}
});
}
sendMessage() {
}
}
ChatCtrl.$name = 'ChatCtrl';
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 the return
button (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
48
49
50
51
import { Directive } from 'angular-ecmascript/module-helpers';
export default class InputDirective extends Directive {
constructor() {
super(...arguments);
this.restrict = 'E';
this.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();
});
}
});
}
}
InputDirective.$name = 'input';
InputDirective.$inject = ['$timeout'];
12
13
14
15
16
17
18
28
29
30
31
32
33
34
// Modules
import ChatsCtrl from '../controllers/chats.controller';
import ChatCtrl from '../controllers/chat.controller';
import InputDirective from '../directives/input.directive';
import CalendarFilter from '../filters/calendar.filter';
import RoutesConfig from '../routes';
...some lines skipped...
new Loader(App)
.load(ChatsCtrl)
.load(ChatCtrl)
.load(InputDirective)
.load(CalendarFilter)
.load(RoutesConfig);
And now we can use those events in our view:
18
19
20
21
22
23
24
25
26
27
<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
8
9
10
11
12
13
14
15
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
import Ionic from 'ionic-scripts';
import { Meteor } from 'meteor/meteor';
import { Controller } from 'angular-ecmascript/module-helpers';
import { Chats, Messages } from '../../../lib/collections';
...some lines skipped...
super(...arguments);
this.chatId = this.$stateParams.chatId;
this.isIOS = Ionic.Platform.isWebView() && Ionic.Platform.isIOS();
this.isCordova = Meteor.isCordova;
this.helpers({
messages() {
...some lines skipped...
sendMessage() {
}
inputUp () {
if (this.isIOS) {
this.keyboardHeight = 216;
}
this.scrollBottom(true);
}
inputDown () {
if (this.isIOS) {
this.keyboardHeight = 0;
}
this.$ionicScrollDelegate.$getByHandle('chatScroll').resize();
}
closeKeyboard () {
if (this.isCordova) {
cordova.plugins.Keyboard.close();
}
}
scrollBottom(animate) {
this.$timeout(() => {
this.$ionicScrollDelegate.$getByHandle('chatScroll').scrollBottom(animate);
}, 300);
}
}
ChatCtrl.$name = 'ChatCtrl';
ChatCtrl.$inject = ['$stateParams', '$timeout', '$ionicScrollDelegate'];
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 goes up.
So now it's time to implement the sendMessage()
in our controller.
We will use callMethod()
in order to call that method on the server side.
1
2
3
4
5
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import Ionic from 'ionic-scripts';
import { _ } from 'meteor/underscore';
import { Meteor } from 'meteor/meteor';
import { Controller } from 'angular-ecmascript/module-helpers';
import { Chats, Messages } from '../../../lib/collections';
...some lines skipped...
}
sendMessage() {
if (_.isEmpty(this.message)) return;
this.callMethod('newMessage', {
text: this.message,
type: 'text',
chatId: this.chatId
});
delete this.message;
}
inputUp () {
Now let's create our method in lib/methods.js
:
1
2
3
4
5
6
7
8
9
10
11
12
13
import { Meteor } from 'meteor/meteor';
import { Chats, Messages } from '../lib/collections';
Meteor.methods({
newMessage(message) {
message.timestamp = new Date();
const messageId = Messages.insert(message);
Chats.update(message.chatId, { $set: { lastMessage: message } });
return messageId;
}
});
Let's add validation to our method.
Meteor
provides us with a useful package named check
that validates data types and scheme.
Add it by running:
$ meteor add check
And now let's use it in the newMessage()
method:
3
4
5
6
7
8
9
10
11
12
13
14
Meteor.methods({
newMessage(message) {
check(message, {
type: String,
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.