123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462 |
- /*
- * vim: ts=4:sw=4:expandtab
- */
- (function () {
- 'use strict';
- window.Whisper = window.Whisper || {};
- // TODO: Factor out private and group subclasses of Conversation
- var COLORS = [
- '#EF5350', // red
- '#EC407A', // pink
- '#AB47BC', // purple
- '#7E57C2', // deep purple
- '#5C6BC0', // indigo
- '#2196F3', // blue
- '#03A9F4', // light blue
- '#00BCD4', // cyan
- '#009688', // teal
- '#4CAF50', // green
- '#7CB342', // light green
- '#FF9800', // orange
- '#FF5722', // deep orange
- '#FFB300', // amber
- '#607D8B', // blue grey
- ];
- Whisper.Conversation = Backbone.Model.extend({
- database: Whisper.Database,
- storeName: 'conversations',
- defaults: function() {
- var timestamp = new Date().getTime();
- return {
- unreadCount : 0,
- timestamp : timestamp,
- };
- },
- initialize: function() {
- this.contactCollection = new Backbone.Collection();
- this.messageCollection = new Whisper.MessageCollection([], {
- conversation: this
- });
- this.on('change:avatar', this.updateAvatarUrl);
- this.on('destroy', this.revokeAvatarUrl);
- },
- validate: function(attributes, options) {
- var required = ['id', 'type'];
- var missing = _.filter(required, function(attr) { return !attributes[attr]; });
- if (missing.length) { return "Conversation must have " + missing; }
- if (attributes.type !== 'private' && attributes.type !== 'group') {
- return "Invalid conversation type: " + attributes.type;
- }
- var error = this.validateNumber();
- if (error) { return error; }
- this.updateTokens();
- },
- validateNumber: function() {
- if (this.isPrivate()) {
- var regionCode = storage.get('regionCode');
- var number = libphonenumber.util.parseNumber(this.id, regionCode);
- if (number.isValidNumber) {
- this.set({ id: number.e164 });
- } else {
- return number.error || "Invalid phone number";
- }
- }
- },
- updateTokens: function() {
- var tokens = [];
- var name = this.get('name');
- if (typeof name === 'string') {
- tokens.push(name.toLowerCase());
- tokens = tokens.concat(name.trim().toLowerCase().split(/[\s\-_\(\)\+]+/));
- }
- if (this.isPrivate()) {
- var regionCode = storage.get('regionCode');
- var number = libphonenumber.util.parseNumber(this.id, regionCode);
- tokens.push(
- number.nationalNumber,
- number.countryCode + number.nationalNumber
- );
- }
- this.set({tokens: tokens});
- },
- sendMessage: function(body, attachments) {
- var now = Date.now();
- var message = this.messageCollection.add({
- body : body,
- conversationId : this.id,
- type : 'outgoing',
- attachments : attachments,
- sent_at : now,
- received_at : now
- });
- if (this.isPrivate()) {
- message.set({destination: this.id});
- }
- message.save();
- this.save({
- unreadCount : 0,
- active_at : now,
- timestamp : now,
- lastMessage : message.getNotificationText()
- });
- var sendFunc;
- if (this.get('type') == 'private') {
- sendFunc = textsecure.messaging.sendMessageToNumber;
- }
- else {
- sendFunc = textsecure.messaging.sendMessageToGroup;
- }
- message.send(sendFunc(this.get('id'), body, attachments, now));
- },
- endSession: function() {
- if (this.isPrivate()) {
- var now = Date.now();
- var message = this.messageCollection.create({
- conversationId : this.id,
- type : 'outgoing',
- sent_at : now,
- received_at : now,
- flags : textsecure.protobuf.DataMessage.Flags.END_SESSION
- });
- message.send(textsecure.messaging.closeSession(this.id, now));
- }
- },
- updateGroup: function(group_update) {
- if (this.isPrivate()) {
- throw new Error("Called update group on private conversation");
- }
- if (group_update === undefined) {
- group_update = this.pick(['name', 'avatar', 'members']);
- }
- var now = Date.now();
- var message = this.messageCollection.create({
- conversationId : this.id,
- type : 'outgoing',
- sent_at : now,
- received_at : now,
- group_update : group_update
- });
- message.send(textsecure.messaging.updateGroup(
- this.id,
- this.get('name'),
- this.get('avatar'),
- this.get('members')
- ));
- },
- leaveGroup: function() {
- var now = Date.now();
- if (this.get('type') === 'group') {
- var message = this.messageCollection.create({
- group_update: { left: 'You' },
- conversationId : this.id,
- type : 'outgoing',
- sent_at : now,
- received_at : now
- });
- message.send(textsecure.messaging.leaveGroup(this.id));
- }
- },
- markRead: function() {
- if (this.get('unreadCount') > 0) {
- this.save({unreadCount: 0});
- var conversationId = this.id;
- Whisper.Notifications.remove(
- Whisper.Notifications.models.filter(
- function(model) {
- return model.attributes.conversationId===conversationId;
- }));
- }
- },
- fetchMessages: function() {
- if (!this.id) { return false; }
- return this.messageCollection.fetchConversation(this.id);
- },
- fetchContacts: function(options) {
- return new Promise(function(resolve) {
- if (this.isPrivate()) {
- this.contactCollection.reset([this]);
- resolve();
- } else {
- var promises = [];
- var members = this.get('members') || [];
- this.contactCollection.reset(
- members.map(function(number) {
- var c = ConversationController.create({
- id : number,
- type : 'private'
- });
- promises.push(new Promise(function(resolve) {
- c.fetch().always(resolve);
- }));
- return c;
- }.bind(this))
- );
- resolve(Promise.all(promises));
- }
- }.bind(this));
- },
- destroyMessages: function() {
- this.messageCollection.fetch({
- index: {
- // 'conversation' index on [conversationId, received_at]
- name : 'conversation',
- lower : [this.id],
- upper : [this.id, Number.MAX_VALUE],
- }
- }).then(function() {
- var models = this.messageCollection.models;
- this.messageCollection.reset([]);
- _.each(models, function(message) { message.destroy(); });
- this.save({active_at: null, lastMessage: ''}); // archive
- }.bind(this));
- },
- getTitle: function() {
- if (this.isPrivate()) {
- return this.get('name') || this.getNumber();
- } else {
- return this.get('name') || 'Unknown group';
- }
- },
- getNumber: function() {
- if (!this.isPrivate()) {
- return '';
- }
- var number = this.id;
- try {
- var parsedNumber = libphonenumber.parse(number);
- var regionCode = libphonenumber.getRegionCodeForNumber(parsedNumber);
- if (regionCode === storage.get('regionCode')) {
- return libphonenumber.format(parsedNumber, libphonenumber.PhoneNumberFormat.NATIONAL);
- } else {
- return libphonenumber.format(parsedNumber, libphonenumber.PhoneNumberFormat.INTERNATIONAL);
- }
- } catch (e) {
- return number;
- }
- },
- isPrivate: function() {
- return this.get('type') === 'private';
- },
- revokeAvatarUrl: function() {
- if (this.avatarUrl) {
- URL.revokeObjectURL(this.avatarUrl);
- this.avatarUrl = null;
- }
- },
- updateAvatarUrl: function(silent) {
- this.revokeAvatarUrl();
- var avatar = this.get('avatar');
- if (avatar) {
- this.avatarUrl = URL.createObjectURL(
- new Blob([avatar.data], {type: avatar.contentType})
- );
- } else {
- this.avatarUrl = null;
- }
- if (!silent) {
- this.trigger('change');
- }
- },
- getAvatar: function() {
- if (this.avatarUrl === undefined) {
- this.updateAvatarUrl(true);
- }
- if (this.avatarUrl) {
- return { url: this.avatarUrl };
- } else if (this.isPrivate()) {
- var title = this.get('name');
- if (!title) {
- return { content: '#', color: '#999999' };
- }
- var initials = title.trim()[0];
- return {
- color: COLORS[Math.abs(this.hashCode()) % 15],
- content: initials
- };
- } else {
- return { url: '/images/group_default.png', color: 'gray' };
- }
- },
- getNotificationIcon: function() {
- return new Promise(function(resolve) {
- var avatar = this.getAvatar();
- if (avatar.url) {
- resolve(avatar.url);
- } else {
- resolve(new Whisper.IdenticonSVGView(avatar).getDataUrl());
- }
- }.bind(this));
- },
- resolveConflicts: function(conflict) {
- var number = conflict.number;
- var identityKey = conflict.identityKey;
- if (this.isPrivate()) {
- number = this.id;
- } else if (!_.include(this.get('members'), number)) {
- throw 'Tried to resolve conflicts for a unknown group member';
- }
- if (!this.messageCollection.hasKeyConflicts()) {
- throw 'No conflicts to resolve';
- }
- return textsecure.storage.axolotl.removeIdentityKey(number).then(function() {
- return textsecure.storage.axolotl.putIdentityKey(number, identityKey).then(function() {
- var promise = Promise.resolve();
- this.messageCollection.each(function(message) {
- if (message.hasKeyConflict(number)) {
- var resolveConflict = function() {
- return message.resolveConflict(number);
- };
- promise = promise.then(resolveConflict, resolveConflict);
- }
- });
- return promise;
- }.bind(this));
- }.bind(this));
- },
- notify: function(message) {
- if (!message.isIncoming()) {
- this.markRead();
- return;
- }
- if (window.isOpen() && window.isFocused()) {
- return;
- }
- window.drawAttention();
- var sender = ConversationController.create({
- id: message.get('source'), type: 'private'
- });
- var conversationId = this.id;
- sender.fetch().then(function() {
- sender.getNotificationIcon().then(function(iconUrl) {
- Whisper.Notifications.add({
- title : sender.getTitle(),
- message : message.getNotificationText(),
- iconUrl : iconUrl,
- imageUrl : message.getImageUrl(),
- conversationId : conversationId
- });
- });
- });
- },
- hashCode: function() {
- if (this.hash === undefined) {
- var string = this.getTitle() || '';
- if (string.length === 0) {
- return 0;
- }
- var hash = 0;
- for (var i = 0; i < string.length; i++) {
- hash = ((hash<<5)-hash) + string.charCodeAt(i);
- hash = hash & hash; // Convert to 32bit integer
- }
- this.hash = hash;
- }
- return this.hash;
- }
- });
- Whisper.ConversationCollection = Backbone.Collection.extend({
- database: Whisper.Database,
- storeName: 'conversations',
- model: Whisper.Conversation,
- comparator: function(m) {
- return -m.get('timestamp');
- },
- destroyAll: function () {
- return Promise.all(this.models.map(function(m) {
- return new Promise(function(resolve, reject) {
- m.destroy().then(resolve).fail(reject);
- });
- }));
- },
- search: function(query) {
- query = query.trim().toLowerCase();
- if (query.length > 0) {
- query = query.replace(/[-.\(\)]*/g,'').replace(/^\+(\d*)$/, '$1');
- var lastCharCode = query.charCodeAt(query.length - 1);
- var nextChar = String.fromCharCode(lastCharCode + 1);
- var upper = query.slice(0, -1) + nextChar;
- return new Promise(function(resolve) {
- this.fetch({
- index: {
- name: 'search', // 'search' index on tokens array
- lower: query,
- upper: upper,
- excludeUpper: true
- }
- }).always(resolve);
- }.bind(this));
- }
- },
- fetchAlphabetical: function() {
- return new Promise(function(resolve) {
- this.fetch({
- index: {
- name: 'search', // 'search' index on tokens array
- },
- limit: 100
- }).always(resolve);
- }.bind(this));
- },
- fetchGroups: function(number) {
- return this.fetch({
- index: {
- name: 'group',
- only: number
- }
- });
- },
- fetchActive: function() {
- // Ensures all active conversations are included in this collection,
- // and updates their attributes, but removes nothing.
- return this.fetch({
- index: {
- name: 'inbox', // 'inbox' index on active_at
- order: 'desc' // ORDER timestamp DESC
- // TODO pagination/infinite scroll
- // limit: 10, offset: page*10,
- },
- remove: false
- });
- }
- });
- })();
|