2013-10-16 13:48:00 -04:00
|
|
|
/*global app, me, client, URL*/
|
2013-09-13 03:53:13 -04:00
|
|
|
"use strict";
|
2013-08-29 23:38:28 -04:00
|
|
|
|
2013-09-13 03:06:27 -04:00
|
|
|
var _ = require('underscore');
|
2013-10-15 03:02:45 -04:00
|
|
|
var crypto = require('crypto');
|
2013-08-29 23:38:28 -04:00
|
|
|
var async = require('async');
|
2013-09-12 16:24:52 -04:00
|
|
|
var uuid = require('node-uuid');
|
2013-09-03 22:18:31 -04:00
|
|
|
var HumanModel = require('human-model');
|
2013-08-29 23:38:28 -04:00
|
|
|
var Resources = require('./resources');
|
|
|
|
var Messages = require('./messages');
|
|
|
|
var Message = require('./message');
|
2013-10-15 15:15:25 -04:00
|
|
|
var logger = require('andlog');
|
2013-09-19 19:57:37 -04:00
|
|
|
var fetchAvatar = require('../helpers/fetchAvatar');
|
2013-08-29 23:38:28 -04:00
|
|
|
|
|
|
|
|
2013-09-05 19:53:23 -04:00
|
|
|
module.exports = HumanModel.define({
|
2013-08-29 23:38:28 -04:00
|
|
|
initialize: function (attrs) {
|
|
|
|
if (attrs.jid) {
|
2013-09-12 16:24:52 -04:00
|
|
|
this.id = attrs.jid;
|
2013-08-29 23:38:28 -04:00
|
|
|
}
|
|
|
|
this.setAvatar(attrs.avatarID);
|
|
|
|
|
2013-09-19 12:27:52 -04:00
|
|
|
this.resources.bind('add remove reset', this.onResourceListChange, this);
|
|
|
|
this.resources.bind('change', this.onResourceChange, this);
|
|
|
|
|
|
|
|
this.bind('change:topResource change:lockedResource change:_forceUpdate', this.summarizeResources, this);
|
2013-08-29 23:38:28 -04:00
|
|
|
},
|
|
|
|
type: 'contact',
|
|
|
|
props: {
|
2013-09-12 16:24:52 -04:00
|
|
|
id: ['string', true, false],
|
2013-09-13 03:53:13 -04:00
|
|
|
avatarID: ['string', true, ''],
|
|
|
|
groups: ['array', true, []],
|
2013-09-09 19:00:13 -04:00
|
|
|
inRoster: ['bool', true, false],
|
2013-08-29 23:38:28 -04:00
|
|
|
jid: ['string', true],
|
|
|
|
name: ['string', true, ''],
|
2013-09-13 03:53:13 -04:00
|
|
|
owner: ['string', true, ''],
|
|
|
|
storageId: ['string', true, ''],
|
|
|
|
subscription: ['string', true, 'none']
|
2013-08-29 23:38:28 -04:00
|
|
|
},
|
2013-09-13 03:08:48 -04:00
|
|
|
session: {
|
2013-09-13 03:53:13 -04:00
|
|
|
activeContact: ['bool', true, false],
|
2013-09-13 03:08:48 -04:00
|
|
|
avatar: 'string',
|
2013-09-13 03:53:13 -04:00
|
|
|
lastInteraction: 'date',
|
2013-09-13 03:08:48 -04:00
|
|
|
lastSentMessage: 'object',
|
2013-09-13 16:55:46 -04:00
|
|
|
lockedResource: 'string',
|
2013-09-13 03:53:13 -04:00
|
|
|
offlineStatus: ['string', true, ''],
|
2013-09-13 16:55:46 -04:00
|
|
|
topResource: 'string',
|
2013-09-19 12:27:52 -04:00
|
|
|
unreadCount: ['number', true, 0],
|
2013-10-15 03:02:45 -04:00
|
|
|
_forceUpdate: ['number', true, 0],
|
2013-10-15 23:06:18 -04:00
|
|
|
onCall: ['boolean', true, false],
|
2013-10-15 03:02:45 -04:00
|
|
|
stream: 'object'
|
2013-09-13 03:08:48 -04:00
|
|
|
},
|
2013-08-29 23:38:28 -04:00
|
|
|
derived: {
|
2013-10-16 13:48:00 -04:00
|
|
|
streamUrl: {
|
|
|
|
deps: ['stream'],
|
|
|
|
cache: true,
|
|
|
|
fn: function () {
|
|
|
|
if (!this.stream) return '';
|
|
|
|
return URL.createObjectURL(this.stream);
|
|
|
|
}
|
|
|
|
},
|
2013-08-29 23:38:28 -04:00
|
|
|
displayName: {
|
|
|
|
deps: ['name', 'jid'],
|
|
|
|
fn: function () {
|
2013-09-13 03:08:48 -04:00
|
|
|
return this.name || this.jid;
|
2013-08-29 23:38:28 -04:00
|
|
|
}
|
|
|
|
},
|
2013-09-25 04:47:11 -04:00
|
|
|
displayUnreadCount: {
|
|
|
|
deps: ['unreadCount'],
|
|
|
|
fn: function () {
|
|
|
|
if (this.unreadCount > 0) {
|
|
|
|
return this.unreadCount.toString();
|
|
|
|
}
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
},
|
2013-09-03 18:25:14 -04:00
|
|
|
formattedTZO: {
|
|
|
|
deps: ['timezoneOffset', 'displayName'],
|
2013-08-29 23:38:28 -04:00
|
|
|
fn: function () {
|
2013-09-03 18:25:14 -04:00
|
|
|
if (this.timezoneOffset !== undefined) {
|
|
|
|
var localTZO = (new Date()).getTimezoneOffset();
|
2013-09-13 16:55:46 -04:00
|
|
|
var diff = Math.abs(localTZO % (24 * 60) - this.timezoneOffset % (24 * 60)) / 60;
|
2013-09-03 18:25:14 -04:00
|
|
|
if (diff === 0) {
|
|
|
|
return this.displayName + ' is in the same timezone as you';
|
|
|
|
}
|
2013-09-17 14:16:08 -04:00
|
|
|
var dir = (localTZO > this.timezoneOffset) ? 'ahead of' : 'behind';
|
2013-09-03 18:25:14 -04:00
|
|
|
return this.displayName + ' is ' + diff + 'hrs ' + dir + ' you';
|
|
|
|
} else {
|
|
|
|
return '';
|
2013-08-29 23:38:28 -04:00
|
|
|
}
|
|
|
|
}
|
2013-09-11 23:59:50 -04:00
|
|
|
},
|
2013-09-19 12:27:52 -04:00
|
|
|
status: {
|
|
|
|
deps: ['topResource', 'lockedResource', '_forceUpdate'],
|
|
|
|
fn: function () {
|
|
|
|
var resource = this.resources.get(this.lockedResource) || this.resources.get(this.topResource) || {};
|
|
|
|
return resource.status || '';
|
|
|
|
}
|
|
|
|
},
|
|
|
|
show: {
|
|
|
|
deps: ['topResource', 'lockedResource', '_forceUpdate'],
|
|
|
|
fn: function () {
|
|
|
|
if (this.resources.length === 0) {
|
|
|
|
return 'offline';
|
|
|
|
}
|
|
|
|
var resource = this.resources.get(this.lockedResource) || this.resources.get(this.topResource) || {};
|
|
|
|
return resource.show || 'online';
|
|
|
|
}
|
|
|
|
},
|
|
|
|
timezoneOffset: {
|
|
|
|
deps: ['topResource', 'lockedResource', '_forceUpdate'],
|
|
|
|
fn: function () {
|
|
|
|
var resource = this.resources.get(this.lockedResource) || this.resources.get(this.topResource) || {};
|
|
|
|
return resource.timezoneOffset || undefined;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
idleSince: {
|
|
|
|
deps: ['topResource', 'lockedResource', '_forceUpdate'],
|
|
|
|
fn: function () {
|
|
|
|
var resource = this.resources.get(this.lockedResource) || this.resources.get(this.topResource) || {};
|
|
|
|
return resource.idleSince || undefined;
|
|
|
|
}
|
|
|
|
},
|
2013-09-27 00:29:45 -04:00
|
|
|
idle: {
|
|
|
|
deps: ['idleSince'],
|
|
|
|
fn: function () {
|
|
|
|
return !!this.idleSince;
|
|
|
|
}
|
|
|
|
},
|
2013-09-19 12:27:52 -04:00
|
|
|
chatState: {
|
|
|
|
deps: ['topResource', 'lockedResource', '_forceUpdate'],
|
|
|
|
fn: function () {
|
|
|
|
var states = {};
|
|
|
|
this.resources.models.forEach(function (resource) {
|
|
|
|
states[resource.chatState] = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (states.composing) return 'composing';
|
|
|
|
if (states.paused) return 'paused';
|
|
|
|
if (states.active) return 'active';
|
|
|
|
if (states.inactive) return 'inactive';
|
|
|
|
return 'gone';
|
|
|
|
}
|
|
|
|
},
|
2013-09-11 23:59:50 -04:00
|
|
|
hasUnread: {
|
|
|
|
deps: ['unreadCount'],
|
|
|
|
fn: function () {
|
|
|
|
return this.unreadCount > 0;
|
|
|
|
}
|
2013-10-14 17:11:32 -04:00
|
|
|
},
|
|
|
|
jingleResources: {
|
2013-10-15 03:02:45 -04:00
|
|
|
deps: ['_forceUpdate'],
|
2013-10-14 17:11:32 -04:00
|
|
|
fn: function () {
|
|
|
|
return this.resources.filter(function (res) {
|
|
|
|
return res.supportsJingleMedia;
|
|
|
|
});
|
|
|
|
}
|
2013-10-15 17:15:07 -04:00
|
|
|
},
|
|
|
|
callable: {
|
|
|
|
deps: ['jingleResources'],
|
|
|
|
fn: function () {
|
|
|
|
return !!this.jingleResources.length;
|
|
|
|
}
|
2013-10-16 13:48:00 -04:00
|
|
|
},
|
|
|
|
callObject: {
|
|
|
|
fn: function () {
|
|
|
|
return app.calls.where('contact', this);
|
|
|
|
}
|
2013-08-29 23:38:28 -04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
collections: {
|
|
|
|
resources: Resources,
|
|
|
|
messages: Messages
|
|
|
|
},
|
2013-10-15 03:02:45 -04:00
|
|
|
call: function () {
|
2013-10-15 15:15:25 -04:00
|
|
|
if (this.jingleResources.length) {
|
2013-10-15 03:02:45 -04:00
|
|
|
var peer = this.jingleResources[0];
|
|
|
|
this.callState = 'starting';
|
2013-10-16 16:02:03 -04:00
|
|
|
app.api.call(peer.id);
|
2013-10-15 15:15:25 -04:00
|
|
|
} else {
|
|
|
|
logger.error('no jingle resources for this user');
|
2013-10-15 03:02:45 -04:00
|
|
|
}
|
|
|
|
},
|
2013-08-29 23:38:28 -04:00
|
|
|
setAvatar: function (id, type) {
|
|
|
|
var self = this;
|
2013-09-19 19:57:37 -04:00
|
|
|
fetchAvatar(this.jid, id, type, function (avatar) {
|
|
|
|
self.avatarID = avatar.id;
|
|
|
|
self.avatar = avatar.uri;
|
|
|
|
self.save();
|
2013-08-29 23:38:28 -04:00
|
|
|
});
|
|
|
|
},
|
2013-09-13 03:06:27 -04:00
|
|
|
onResourceChange: function () {
|
2013-09-19 12:27:52 -04:00
|
|
|
this.resources.sort();
|
|
|
|
this.topResource = (this.resources.first() || {}).id;
|
|
|
|
this._forceUpdate++;
|
|
|
|
},
|
|
|
|
onResourceListChange: function () {
|
2013-08-29 23:38:28 -04:00
|
|
|
// Manually propagate change events for properties that
|
|
|
|
// depend on the resources collection.
|
|
|
|
this.resources.sort();
|
|
|
|
|
|
|
|
var res = this.resources.first();
|
|
|
|
if (res) {
|
|
|
|
this.offlineStatus = '';
|
2013-09-13 16:55:46 -04:00
|
|
|
this.topResource = res.id;
|
2013-08-29 23:38:28 -04:00
|
|
|
} else {
|
2013-09-13 03:53:13 -04:00
|
|
|
this.topResource = undefined;
|
2013-08-29 23:38:28 -04:00
|
|
|
}
|
2013-09-13 03:08:48 -04:00
|
|
|
|
|
|
|
this.lockedResource = undefined;
|
|
|
|
},
|
|
|
|
addMessage: function (message, notify) {
|
2013-09-13 17:36:15 -04:00
|
|
|
message.owner = me.jid.bare;
|
|
|
|
|
2013-09-27 14:54:51 -04:00
|
|
|
if (notify && (!this.activeContact || (this.activeContact && !app.state.focused)) && message.from.bare === this.jid) {
|
2013-09-13 03:08:48 -04:00
|
|
|
this.unreadCount++;
|
2013-10-11 18:40:42 -04:00
|
|
|
app.notifications.create(this.displayName, {
|
|
|
|
body: message.body,
|
2013-09-13 03:08:48 -04:00
|
|
|
icon: this.avatar,
|
2013-10-11 18:40:42 -04:00
|
|
|
tag: this.jid,
|
2013-09-13 03:08:48 -04:00
|
|
|
onclick: _.bind(app.navigate, app, '/chat/' + this.jid)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2013-12-18 16:31:22 -05:00
|
|
|
var existing = Message.idLookup(message.from[message.type == 'groupchat' ? 'full' : 'bare'], message.mid);
|
|
|
|
if (existing) {
|
|
|
|
existing.set(message);
|
|
|
|
existing.save();
|
|
|
|
} else {
|
|
|
|
this.messages.add(message);
|
|
|
|
message.save();
|
|
|
|
}
|
2013-09-13 17:36:15 -04:00
|
|
|
|
2013-09-13 03:08:48 -04:00
|
|
|
var newInteraction = new Date(message.created);
|
|
|
|
if (!this.lastInteraction || this.lastInteraction < newInteraction) {
|
|
|
|
this.lastInteraction = newInteraction;
|
|
|
|
}
|
2013-08-29 23:38:28 -04:00
|
|
|
},
|
|
|
|
fetchHistory: function () {
|
|
|
|
var self = this;
|
|
|
|
app.whenConnected(function () {
|
2013-09-12 16:44:02 -04:00
|
|
|
var filter = {
|
|
|
|
count: 20,
|
|
|
|
before: true,
|
|
|
|
};
|
|
|
|
|
|
|
|
var lastMessage = self.messages.last();
|
|
|
|
if (lastMessage && lastMessage.archivedId) {
|
|
|
|
filter.after = lastMessage.archivedId;
|
|
|
|
}
|
|
|
|
|
2013-08-29 23:38:28 -04:00
|
|
|
client.getHistory({
|
|
|
|
with: self.jid,
|
2013-09-12 16:44:02 -04:00
|
|
|
start: self.lastInteraction,
|
|
|
|
rsm: filter
|
2013-08-29 23:38:28 -04:00
|
|
|
}, function (err, res) {
|
|
|
|
if (err) return;
|
|
|
|
|
|
|
|
var results = res.mamQuery.results || [];
|
|
|
|
results.reverse();
|
|
|
|
results.forEach(function (result) {
|
|
|
|
result = result.toJSON();
|
|
|
|
var msg = result.mam.forwarded.message;
|
2013-09-24 05:05:10 -04:00
|
|
|
|
2013-12-18 16:31:22 -05:00
|
|
|
msg.mid = msg.id;
|
|
|
|
delete msg.id;
|
2013-08-29 23:38:28 -04:00
|
|
|
|
|
|
|
if (!msg.delay) {
|
|
|
|
msg.delay = result.mam.forwarded.delay;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (msg.replace) {
|
2013-12-18 16:31:22 -05:00
|
|
|
var original = Message.idLookup(msg.from[msg.type == 'groupchat' ? 'full' : 'bare'], msg.replace);
|
2013-09-12 16:24:52 -04:00
|
|
|
// Drop the message if editing a previous, but
|
|
|
|
// keep it if it didn't actually change an
|
|
|
|
// existing message.
|
|
|
|
if (original && original.correct(msg)) return;
|
2013-08-29 23:38:28 -04:00
|
|
|
}
|
|
|
|
|
2013-09-12 16:24:52 -04:00
|
|
|
var message = new Message(msg);
|
|
|
|
message.archivedId = result.mam.id;
|
2013-09-24 05:05:10 -04:00
|
|
|
message.acked = true;
|
2013-09-12 16:44:02 -04:00
|
|
|
|
2013-09-13 03:08:48 -04:00
|
|
|
self.addMessage(message, false);
|
2013-08-29 23:38:28 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
save: function () {
|
2013-09-09 19:00:13 -04:00
|
|
|
if (!this.inRoster) return;
|
|
|
|
|
2013-10-15 03:02:45 -04:00
|
|
|
var storageId = crypto.createHash('sha1').update(this.owner + '/' + this.id).digest('hex');
|
2013-08-29 23:38:28 -04:00
|
|
|
var data = {
|
2013-10-15 03:02:45 -04:00
|
|
|
storageId: storageId,
|
2013-09-11 17:58:39 -04:00
|
|
|
owner: this.owner,
|
2013-08-29 23:38:28 -04:00
|
|
|
jid: this.jid,
|
|
|
|
name: this.name,
|
|
|
|
groups: this.groups,
|
|
|
|
subscription: this.subscription,
|
|
|
|
avatarID: this.avatarID
|
|
|
|
};
|
|
|
|
app.storage.roster.add(data);
|
|
|
|
}
|
|
|
|
});
|