-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathchat.js
345 lines (266 loc) · 8.06 KB
/
chat.js
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
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
/* global log */
var moment = require('alloy/moment');
/**
* Self-executing function containing all code that is executed when an instance
* of this controller is created, apart from dependencies and variables declared
* above. Just for readability, much like a class constructor.
*/
(function constructor(args) {
// Listen to collection changes
Alloy.Collections.message.on('fetch destroy change add remove reset', onChange);
// Fetch existing messages from the SQLite store
Alloy.Collections.message.fetch();
// Fired by notifications.js when a notifications is received or responded to
Alloy.Events.on('action', onAction);
// Resize the container when the keyboards shows/hides
Ti.App.addEventListener('keyboardframechanged', onKeyboardframechanged);
// When the app resumes see if this has focus and if so, mark all messages as read
Ti.App.addEventListener('resume', onResume);
})(arguments[0] || {});
/**
* Event listener set in constructor to be called when the message collection changes
*
* @param {Object} model Collection or model that was changed
*/
function onChange(model) {
// Collection is empty
if (Alloy.Collections.message.length === 0) {
// Create first message
Alloy.Collections.message.create({
id: Ti.Platform.createUUID(),
message: 'Hey, how are you? Send me a message, then move the app to the background or lock your phone to get a reply.',
mine: 0,
sent: Date.now()
});
} else {
// Scroll down each time something changes
scrollDown();
}
}
/**
* Event listener set in constructor to be called when a notification is received
* or responsed to so it can be handled.
*
* @param {Object} e Event
*/
function onAction(e) {
// Get the related message model
var model = Alloy.Collections.message.get(e.id);
// Delete action was selected
if (e.action === 'DELETE') {
model.destroy();
log('Chat: Deleted message ' + e.id + '.');
} else {
// Mark the message as read
model.save({
read: Date.now()
});
// Update badges
updateBadges();
log('Chat: Marked message ' + e.id + ' as read.');
switch (e.action) {
// Not OK (thumbs down) action
case 'NOK':
// Create Not OK message
Alloy.Collections.message.create({
id: Ti.Platform.createUUID(),
message: '👎',
mine: 1,
sent: Date.now()
});
log('Chat: Replied 👎 to message ' + e.id + '.');
// Schedule fake response
scheduleFakeResponse();
break;
// REPLY action
case 'REPLY':
// Only in Titanium 5.1 or later and when the user typed something do we have input
if (Ti.App.iOS.USER_NOTIFICATION_BEHAVIOR_TEXTINPUT && e.typedText) {
Alloy.Collections.message.create({
id: Ti.Platform.createUUID(),
message: e.typedText,
mine: 1,
sent: Date.now()
});
log('Chat: Replied \'' + e.typedText + '\' to message ' + e.id + '.');
// Schedule fake response
scheduleFakeResponse();
break;
}
// No action was selected or REPLY was empty
default:
// Make this tab active if it's not already so user can handle it
$.tab.active = true;
log('Chat: Opened chat with message ' + e.id + ' to reply.');
break;
}
}
}
/**
* Called by onChange() and as event listener for the ListView's postlayout event
* to scroll all the way down to the latest message.
*/
function scrollDown() {
$.listView.scrollToItem(0, $.listView.sections[0].items.length - 1);
}
/**
* Event listener set in constructor to be called when the keyboard shows or
* hides so we can resize the container of both the ListView and TextField.
*/
function onKeyboardframechanged(e) {
// FIXME: Kind of tricky since this might change in future iOS but did not
// find a way to get the Tab Bar height from some proxy.
var tabsHeight = 50;
// Full screen height minus keyboard start (from top) minus tabs height
// If the keyboard is down this will be -50, so at least do 0
$.container.bottom = Math.max(0, Ti.Platform.displayCaps.platformHeight - e.keyboardFrame.y - tabsHeight);
}
/**
* Event listener set in constructor to be called when the app resumes.
*
* @param {Object} e Event
*/
function onResume(e) {
// If the current tab is active
if ($.tab.active) {
// Mark all other messages as read
markAllRead(false);
}
}
/**
* Called by onResume() and set as event listener in the view to mark all
* messages of us or (true) the other (false) as read.
*/
function markAllRead(mine) {
var mine = _.isObject(mine) ? false : mine;
// Get all unread messages
_.each(getUnread(mine), function(model) {
// Set as read without triggering the data-binding for each
model.save({
read: Date.now()
}, {
silent: true
});
});
// Trigger the change event to update the UI via data-binding
Alloy.Collections.message.trigger('fetch');
// Reset app and tab badge number
updateBadges();
}
/**
* Called by markAllRead() and updateBadges() to get all unread messages
* of either me (true) or the other (false).
*
* @return {Array} Array of message models
*/
function getUnread(mine) {
return Alloy.Collections.message.where({
read: 0,
mine: mine ? 1 : 0
});
}
/**
* Event listener set in the view to be called when the user taps on a ListView
* item to blur the TextField and by doing so hide the keyboard.
*
* @param {Object} e Event
*/
function hideKeyboard(e) {
$.textField.blur();
}
/**
* Event listener set in the view to be called when a ListView item is swiped
* from right to left to delete it.
*
* @param {Object} e Event
*/
function deleteMessage(e) {
Alloy.Collections.message.get(e.itemId).destroy();
}
/**
* Event listener set in view to be called when user hits send on the keyboard
* after typing a message in the TextField. It should send the message.
*
* @param {Object} e Event
*/
function sendMessage(e) {
// Don't send empty messages
if (!e.value) {
return;
}
// Empty the TextField for the next message
$.textField.value = '';
// Create the message model
Alloy.Collections.message.create({
id: Ti.Platform.createUUID(),
message: e.value,
mine: 1,
sent: Date.now()
});
// Schedule a fake response from out bot
scheduleFakeResponse();
}
/**
* Function set in the view to be called on each model before rendering it in the ListView
*
* @method transformMessage
* @param {Object} The original model
* @return {Object} New or changed attributes
*/
function transformMessage(model) {
// Convert 1|0 to bool
var mine = !!model.get('mine');
// Create the meta string
var meta = (mine ? 'Sent' : 'Received') + ' ' + moment(model.get('sent')).format('HH:mm:ss');
// Get the read-date
var read = model.get('read');
if (read) {
// Add the read-date to the meta
meta += ', read ' + moment(read).format('HH:mm:ss');
}
return {
template: mine ? 'mine' : 'theirs',
meta: meta
};
}
/**
* Function called in different places in this controller to schedule a fake response
* from our bot.
*/
function scheduleFakeResponse() {
// After delay set in config.json
setTimeout(function() {
// Create the response
var response = Alloy.Collections.message.create({
id: Ti.Platform.createUUID(),
// Pick a random response from the options in config.json
message: Alloy.CFG.messages[_.random(Alloy.CFG.messages.length - 1)],
mine: 0,
sent: Date.now()
});
// Mark all messages that are mine as read (by the other)
markAllRead(true);
// Notifiy the user right away (no date)
Ti.App.iOS.scheduleLocalNotification({
alertAction: 'Reply',
alertBody: response.get('message'),
category: 'CHAT_CATEGORY',
sound: 'notification.caf',
userInfo: {
id: response.id
}
});
log('Chat: Notifying about a fake response with message ID: ' + response.id);
}, Alloy.CFG.delay);
}
/**
* Function called in different places in this controller to update the app
* and tab badge with te number of badges we haven't read.
*/
function updateBadges() {
// Get the number of unread messages
var unreads = getUnread(false).length;
// Set the tab and app badge
$.tab.badge = unreads || null;
Ti.UI.iPhone.appBadge = unreads;
}