diff --git a/app/sql.js b/app/sql.js
index b49cb0bea0..ac26078fff 100644
--- a/app/sql.js
+++ b/app/sql.js
@@ -16,14 +16,6 @@ module.exports = {
removeDB,
removeIndexedDBFiles,
- createOrUpdateGroup,
- getGroupById,
- getAllGroupIds,
- getAllGroups,
- bulkAddGroups,
- removeGroupById,
- removeAllGroups,
-
createOrUpdateIdentityKey,
getIdentityKeyById,
bulkAddIdentityKeys,
@@ -625,6 +617,20 @@ async function updateToSchemaVersion10(currentVersion, instance) {
console.log('updateToSchemaVersion10: success!');
}
+async function updateToSchemaVersion11(currentVersion, instance) {
+ if (currentVersion >= 11) {
+ return;
+ }
+ console.log('updateToSchemaVersion11: starting...');
+ await instance.run('BEGIN TRANSACTION;');
+
+ await instance.run('DROP TABLE groups;');
+
+ await instance.run('PRAGMA schema_version = 11;');
+ await instance.run('COMMIT TRANSACTION;');
+ console.log('updateToSchemaVersion11: success!');
+}
+
const SCHEMA_VERSIONS = [
updateToSchemaVersion1,
updateToSchemaVersion2,
@@ -636,6 +642,7 @@ const SCHEMA_VERSIONS = [
updateToSchemaVersion8,
updateToSchemaVersion9,
updateToSchemaVersion10,
+ updateToSchemaVersion11,
];
async function updateSchema(instance) {
@@ -726,31 +733,6 @@ async function removeIndexedDBFiles() {
indexedDBPath = null;
}
-const GROUPS_TABLE = 'groups';
-async function createOrUpdateGroup(data) {
- return createOrUpdate(GROUPS_TABLE, data);
-}
-async function getGroupById(id) {
- return getById(GROUPS_TABLE, id);
-}
-async function getAllGroupIds() {
- const rows = await db.all('SELECT id FROM groups ORDER BY id ASC;');
- return map(rows, row => row.id);
-}
-async function getAllGroups() {
- const rows = await db.all('SELECT json FROM groups ORDER BY id ASC;');
- return map(rows, row => jsonToObject(row.json));
-}
-async function bulkAddGroups(array) {
- return bulkAdd(GROUPS_TABLE, array);
-}
-async function removeGroupById(id) {
- return removeById(GROUPS_TABLE, id);
-}
-async function removeAllGroups() {
- return removeAllFromTable(GROUPS_TABLE);
-}
-
const IDENTITY_KEYS_TABLE = 'identityKeys';
async function createOrUpdateIdentityKey(data) {
return createOrUpdate(IDENTITY_KEYS_TABLE, data);
@@ -1701,7 +1683,6 @@ async function removeAll() {
promise = Promise.all([
db.run('BEGIN TRANSACTION;'),
db.run('DELETE FROM conversations;'),
- db.run('DELETE FROM groups;'),
db.run('DELETE FROM identityKeys;'),
db.run('DELETE FROM items;'),
db.run('DELETE FROM messages;'),
diff --git a/background.html b/background.html
index c4df972c28..9d780a0f8e 100644
--- a/background.html
+++ b/background.html
@@ -615,7 +615,6 @@
-
diff --git a/js/models/conversations.js b/js/models/conversations.js
index 160be22a2d..048f304e26 100644
--- a/js/models/conversations.js
+++ b/js/models/conversations.js
@@ -210,14 +210,16 @@
sendTypingMessage(isTyping) {
const groupId = !this.isPrivate() ? this.id : null;
const recipientId = this.isPrivate() ? this.id : null;
+ const groupNumbers = this.getRecipients();
const sendOptions = this.getSendOptions();
this.wrapSend(
textsecure.messaging.sendTypingMessage(
{
- groupId,
isTyping,
recipientId,
+ groupId,
+ groupNumbers,
},
sendOptions
)
@@ -929,12 +931,36 @@
}
const conversationType = this.get('type');
- const sendFunction = (() => {
+ const options = this.getSendOptions();
+ const groupNumbers = this.getRecipients();
+
+ const promise = (() => {
switch (conversationType) {
case Message.PRIVATE:
- return textsecure.messaging.sendMessageToNumber;
+ return textsecure.messaging.sendMessageToNumber(
+ destination,
+ body,
+ attachmentsWithData,
+ quote,
+ preview,
+ now,
+ expireTimer,
+ profileKey,
+ options
+ );
case Message.GROUP:
- return textsecure.messaging.sendMessageToGroup;
+ return textsecure.messaging.sendMessageToGroup(
+ destination,
+ groupNumbers,
+ body,
+ attachmentsWithData,
+ quote,
+ preview,
+ now,
+ expireTimer,
+ profileKey,
+ options
+ );
default:
throw new TypeError(
`Invalid conversation type: '${conversationType}'`
@@ -942,22 +968,7 @@
}
})();
- const options = this.getSendOptions();
- return message.send(
- this.wrapSend(
- sendFunction(
- destination,
- body,
- attachmentsWithData,
- quote,
- preview,
- now,
- expireTimer,
- profileKey,
- options
- )
- )
- );
+ return message.send(this.wrapSend(promise));
});
},
@@ -1239,25 +1250,31 @@
return message;
}
- let sendFunc;
- if (this.get('type') === 'private') {
- sendFunc = textsecure.messaging.sendExpirationTimerUpdateToNumber;
- } else {
- sendFunc = textsecure.messaging.sendExpirationTimerUpdateToGroup;
- }
let profileKey;
if (this.get('profileSharing')) {
profileKey = storage.get('profileKey');
}
-
const sendOptions = this.getSendOptions();
- const promise = sendFunc(
- this.get('id'),
- this.get('expireTimer'),
- message.get('sent_at'),
- profileKey,
- sendOptions
- );
+ let promise;
+
+ if (this.get('type') === 'private') {
+ promise = textsecure.messaging.sendExpirationTimerUpdateToNumber(
+ this.get('id'),
+ this.get('expireTimer'),
+ message.get('sent_at'),
+ profileKey,
+ sendOptions
+ );
+ } else {
+ promise = textsecure.messaging.sendExpirationTimerUpdateToGroup(
+ this.get('id'),
+ this.getRecipients(),
+ this.get('expireTimer'),
+ message.get('sent_at'),
+ profileKey,
+ sendOptions
+ );
+ }
await message.send(this.wrapSend(promise));
@@ -1335,6 +1352,7 @@
async leaveGroup() {
const now = Date.now();
if (this.get('type') === 'group') {
+ const groupNumbers = this.getRecipients();
this.set({ left: true });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
@@ -1355,7 +1373,9 @@
const options = this.getSendOptions();
message.send(
- this.wrapSend(textsecure.messaging.leaveGroup(this.id, options))
+ this.wrapSend(
+ textsecure.messaging.leaveGroup(this.id, groupNumbers, options)
+ )
);
}
},
diff --git a/js/models/messages.js b/js/models/messages.js
index 1da77e20a4..b674aa8e7a 100644
--- a/js/models/messages.js
+++ b/js/models/messages.js
@@ -706,21 +706,35 @@
this.isReplayableError.bind(this)
);
- // Remove the errors that aren't replayable
+ // Put the errors back which aren't replayable
this.set({ errors });
- const profileKey = null;
- let numbers = retries
+ const conversation = this.getConversation();
+ const intendedRecipients = this.get('recipients') || [];
+ const currentRecipients = conversation.getRecipients();
+
+ const profileKey = conversation.get('profileSharing')
+ ? storage.get('profileKey')
+ : null;
+
+ const errorNumbers = retries
.map(retry => retry.number)
.filter(item => Boolean(item));
+ let numbers = _.intersection(
+ errorNumbers,
+ intendedRecipients,
+ currentRecipients
+ );
if (!numbers.length) {
window.log.warn(
'retrySend: No numbers in error set, using all recipients'
);
- const conversation = this.getConversation();
+
if (conversation) {
- numbers = conversation.getRecipients();
+ numbers = _.intersection(currentRecipients, intendedRecipients);
+ // We clear all errors here to start with a fresh slate, since we are starting
+ // from scratch on this message with a fresh set of recipients
this.set({ errors: null });
} else {
throw new Error(
@@ -752,7 +766,6 @@
}
let promise;
- const conversation = this.getConversation();
const options = conversation.getSendOptions();
if (conversation.isPrivate()) {
diff --git a/js/modules/backup.js b/js/modules/backup.js
index b486a6e10d..f527a142ee 100644
--- a/js/modules/backup.js
+++ b/js/modules/backup.js
@@ -109,9 +109,9 @@ function createOutputStream(writer) {
};
}
-async function exportContactAndGroupsToFile(parent) {
+async function exportConversationListToFile(parent) {
const writer = await createFileAndWriter(parent, 'db.json');
- return exportContactsAndGroups(writer);
+ return exportConversationList(writer);
}
function writeArray(stream, array) {
@@ -137,7 +137,7 @@ function getPlainJS(collection) {
return collection.map(model => model.attributes);
}
-async function exportContactsAndGroups(fileWriter) {
+async function exportConversationList(fileWriter) {
const stream = createOutputStream(fileWriter);
stream.write('{');
@@ -149,13 +149,6 @@ async function exportContactsAndGroups(fileWriter) {
window.log.info(`Exporting ${conversations.length} conversations`);
writeArray(stream, getPlainJS(conversations));
- stream.write(',');
-
- stream.write('"groups": ');
- const groups = await window.Signal.Data.getAllGroups();
- window.log.info(`Exporting ${groups.length} groups`);
- writeArray(stream, groups);
-
stream.write('}');
await stream.close();
}
@@ -167,7 +160,7 @@ async function importNonMessages(parent, options) {
}
function eliminateClientConfigInBackup(data, targetPath) {
- const cleaned = _.pick(data, 'conversations', 'groups');
+ const cleaned = _.pick(data, 'conversations');
window.log.info('Writing configuration-free backup file back to disk');
try {
fs.writeFileSync(targetPath, JSON.stringify(cleaned));
@@ -223,10 +216,8 @@ async function importFromJsonString(jsonString, targetPath, options) {
_.defaults(options, {
forceLightImport: false,
conversationLookup: {},
- groupLookup: {},
});
- const { groupLookup } = options;
const result = {
fullImport: true,
};
@@ -251,7 +242,7 @@ async function importFromJsonString(jsonString, targetPath, options) {
// We mutate the on-disk backup to prevent the user from importing client
// configuration more than once - that causes lots of encryption errors.
- // This of course preserves the true data: conversations and groups.
+ // This of course preserves the true data: conversations.
eliminateClientConfigInBackup(importObject, targetPath);
const storeNames = _.keys(importObject);
@@ -262,12 +253,12 @@ async function importFromJsonString(jsonString, targetPath, options) {
const remainingStoreNames = _.without(
storeNames,
'conversations',
- 'unprocessed'
+ 'unprocessed',
+ 'groups' // in old data sets, but no longer included in database schema
);
await importConversationsFromJSON(conversations, options);
const SAVE_FUNCTIONS = {
- groups: window.Signal.Data.createOrUpdateGroup,
identityKeys: window.Signal.Data.createOrUpdateIdentityKey,
items: window.Signal.Data.createOrUpdateItem,
preKeys: window.Signal.Data.createOrUpdatePreKey,
@@ -292,29 +283,17 @@ async function importFromJsonString(jsonString, targetPath, options) {
return;
}
- let skipCount = 0;
-
for (let i = 0, max = toImport.length; i < max; i += 1) {
const toAdd = unstringify(toImport[i]);
-
- const haveGroupAlready =
- storeName === 'groups' && groupLookup[getGroupKey(toAdd)];
-
- if (haveGroupAlready) {
- skipCount += 1;
- } else {
- // eslint-disable-next-line no-await-in-loop
- await save(toAdd);
- }
+ // eslint-disable-next-line no-await-in-loop
+ await save(toAdd);
}
window.log.info(
'Done importing to store',
storeName,
'Total count:',
- toImport.length,
- 'Skipped:',
- skipCount
+ toImport.length
);
})
);
@@ -1160,14 +1139,6 @@ async function loadConversationLookup() {
return fromPairs(map(array, item => [getConversationKey(item), true]));
}
-function getGroupKey(group) {
- return group.id;
-}
-async function loadGroupsLookup() {
- const array = await window.Signal.Data.getAllGroupIds();
- return fromPairs(map(array, item => [getGroupKey(item), true]));
-}
-
function getDirectoryForExport() {
return getDirectory();
}
@@ -1254,7 +1225,7 @@ async function exportToDirectory(directory, options) {
const attachmentsDir = await createDirectory(directory, 'attachments');
- await exportContactAndGroupsToFile(stagingDir);
+ await exportConversationListToFile(stagingDir);
await exportConversations(
Object.assign({}, options, {
messagesDir: stagingDir,
@@ -1298,13 +1269,11 @@ async function importFromDirectory(directory, options) {
const lookups = await Promise.all([
loadMessagesLookup(),
loadConversationLookup(),
- loadGroupsLookup(),
]);
- const [messageLookup, conversationLookup, groupLookup] = lookups;
+ const [messageLookup, conversationLookup] = lookups;
options = Object.assign({}, options, {
messageLookup,
conversationLookup,
- groupLookup,
});
const archivePath = path.join(directory, ARCHIVE_NAME);
diff --git a/js/modules/data.js b/js/modules/data.js
index 2b1c207fc7..8eb9250458 100644
--- a/js/modules/data.js
+++ b/js/modules/data.js
@@ -47,14 +47,6 @@ module.exports = {
removeDB,
removeIndexedDBFiles,
- createOrUpdateGroup,
- getGroupById,
- getAllGroupIds,
- getAllGroups,
- bulkAddGroups,
- removeGroupById,
- removeAllGroups,
-
createOrUpdateIdentityKey,
getIdentityKeyById,
bulkAddIdentityKeys,
@@ -395,33 +387,6 @@ async function removeIndexedDBFiles() {
await channels.removeIndexedDBFiles();
}
-// Groups
-
-async function createOrUpdateGroup(data) {
- await channels.createOrUpdateGroup(data);
-}
-async function getGroupById(id) {
- const group = await channels.getGroupById(id);
- return group;
-}
-async function getAllGroupIds() {
- const ids = await channels.getAllGroupIds();
- return ids;
-}
-async function getAllGroups() {
- const groups = await channels.getAllGroups();
- return groups;
-}
-async function bulkAddGroups(array) {
- await channels.bulkAddGroups(array);
-}
-async function removeGroupById(id) {
- await channels.removeGroupById(id);
-}
-async function removeAllGroups() {
- await channels.removeAllGroups();
-}
-
// Identity Keys
const IDENTITY_KEY_KEYS = ['publicKey'];
diff --git a/js/modules/migrate_to_sql.js b/js/modules/migrate_to_sql.js
index 00dba4df49..4278ad0398 100644
--- a/js/modules/migrate_to_sql.js
+++ b/js/modules/migrate_to_sql.js
@@ -2,14 +2,12 @@
const { includes, isFunction, isString, last, map } = require('lodash');
const {
- bulkAddGroups,
bulkAddSessions,
bulkAddIdentityKeys,
bulkAddPreKeys,
bulkAddSignedPreKeys,
bulkAddItems,
- removeGroupById,
removeSessionById,
removeIdentityKeyById,
removePreKeyById,
@@ -184,31 +182,6 @@ async function migrateToSQL({
complete = false;
lastIndex = null;
- while (!complete) {
- // eslint-disable-next-line no-await-in-loop
- const status = await migrateStoreToSQLite({
- db,
- // eslint-disable-next-line no-loop-func
- save: bulkAddGroups,
- remove: removeGroupById,
- storeName: 'groups',
- handleDOMException,
- lastIndex,
- batchSize: 10,
- });
-
- ({ complete, lastIndex } = status);
- }
- window.log.info('migrateToSQL: migrate of groups complete');
- try {
- await clearStores(['groups']);
- } catch (error) {
- window.log.warn('Failed to clear groups store');
- }
-
- complete = false;
- lastIndex = null;
-
while (!complete) {
// eslint-disable-next-line no-await-in-loop
const status = await migrateStoreToSQLite({
diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js
index bf27c2850a..0a14b1a7cf 100644
--- a/js/signal_protocol_store.js
+++ b/js/signal_protocol_store.js
@@ -828,41 +828,6 @@
await textsecure.storage.protocol.removeAllSessions(number);
},
- // Groups
-
- async getGroup(groupId) {
- if (groupId === null || groupId === undefined) {
- throw new Error('Tried to get group for undefined/null id');
- }
-
- const group = await window.Signal.Data.getGroupById(groupId);
- if (group) {
- return group.data;
- }
-
- return undefined;
- },
- async putGroup(groupId, group) {
- if (groupId === null || groupId === undefined) {
- throw new Error('Tried to put group key for undefined/null id');
- }
- if (group === null || group === undefined) {
- throw new Error('Tried to put undefined/null group object');
- }
- const data = {
- id: groupId,
- data: group,
- };
- await window.Signal.Data.createOrUpdateGroup(data);
- },
- async removeGroup(groupId) {
- if (groupId === null || groupId === undefined) {
- throw new Error('Tried to remove group key for undefined/null id');
- }
-
- await window.Signal.Data.removeGroupById(groupId);
- },
-
// Not yet processed messages - for resiliency
getUnprocessedCount() {
return window.Signal.Data.getUnprocessedCount();
diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js
index cfa769aeb8..b509785cca 100644
--- a/js/views/conversation_view.js
+++ b/js/views/conversation_view.js
@@ -1135,16 +1135,7 @@
async showMembers(e, providedMembers, options = {}) {
_.defaults(options, { needVerify: false });
- const fromConversation = this.model.isPrivate()
- ? [this.model.id]
- : await textsecure.storage.groups.getNumbers(this.model.id);
- const members =
- providedMembers ||
- fromConversation.map(id => ConversationController.get(id));
-
- const model = this.model.getContactCollection();
- model.reset(members);
-
+ const model = providedMembers || this.model.contactCollection;
const view = new Whisper.GroupMemberList({
model,
// we pass this in to allow nested panels
diff --git a/js/views/new_group_update_view.js b/js/views/new_group_update_view.js
deleted file mode 100644
index c15494e996..0000000000
--- a/js/views/new_group_update_view.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/* global Whisper, _ */
-
-/* eslint-disable more/no-then */
-
-// eslint-disable-next-line func-names
-(function() {
- 'use strict';
-
- window.Whisper = window.Whisper || {};
-
- Whisper.NewGroupUpdateView = Whisper.View.extend({
- tagName: 'div',
- className: 'new-group-update',
- templateName: 'new-group-update',
- initialize(options) {
- this.render();
- this.avatarInput = new Whisper.FileInputView({
- el: this.$('.group-avatar'),
- window: options.window,
- });
-
- this.recipients_view = new Whisper.RecipientsInputView();
- this.listenTo(this.recipients_view.typeahead, 'sync', () =>
- this.model.contactCollection.models.forEach(model => {
- if (this.recipients_view.typeahead.get(model)) {
- this.recipients_view.typeahead.remove(model);
- }
- })
- );
- this.recipients_view.$el.insertBefore(this.$('.container'));
-
- this.member_list_view = new Whisper.ContactListView({
- collection: this.model.contactCollection,
- className: 'members',
- });
- this.member_list_view.render();
- this.$('.scrollable').append(this.member_list_view.el);
- },
- events: {
- 'click .back': 'goBack',
- 'click .send': 'send',
- 'focusin input.search': 'showResults',
- 'focusout input.search': 'hideResults',
- },
- hideResults() {
- this.$('.results').hide();
- },
- showResults() {
- this.$('.results').show();
- },
- goBack() {
- this.trigger('back');
- },
- render_attributes() {
- return {
- name: this.model.getTitle(),
- avatar: this.model.getAvatar(),
- };
- },
- async send() {
- // When we turn this view on again, need to handle avatars in the new way
-
- // const avatarFile = await this.avatarInput.getThumbnail();
- const now = Date.now();
- const attrs = {
- timestamp: now,
- active_at: now,
- name: this.$('.name').val(),
- members: _.union(
- this.model.get('members'),
- this.recipients_view.recipients.pluck('id')
- ),
- };
-
- // if (avatarFile) {
- // attrs.avatar = avatarFile;
- // }
-
- // Because we're no longer using Backbone-integrated saves, we need to manually
- // clear the changed fields here so model.changed is accurate.
- this.model.changed = {};
- this.model.set(attrs);
- const groupUpdate = this.model.changed;
-
- await window.Signal.Data.updateConversation(
- this.model.id,
- this.model.attributes,
- { Conversation: Whisper.Conversation }
- );
-
- if (groupUpdate.avatar) {
- this.model.trigger('change:avatar');
- }
-
- this.model.updateGroup(groupUpdate);
- this.goBack();
- },
- });
-})();
diff --git a/js/views/recipients_input_view.js b/js/views/recipients_input_view.js
deleted file mode 100644
index af5ea95578..0000000000
--- a/js/views/recipients_input_view.js
+++ /dev/null
@@ -1,183 +0,0 @@
-/* global Whisper, Backbone, ConversationController */
-
-// eslint-disable-next-line func-names
-(function() {
- 'use strict';
-
- window.Whisper = window.Whisper || {};
-
- const ContactsTypeahead = Backbone.TypeaheadCollection.extend({
- typeaheadAttributes: [
- 'name',
- 'e164_number',
- 'national_number',
- 'international_number',
- ],
- model: Whisper.Conversation,
- async fetchContacts() {
- const models = window.Signal.Data.getAllPrivateConversations({
- ConversationCollection: Whisper.ConversationCollection,
- });
-
- this.reset(models);
- },
- });
-
- Whisper.ContactPillView = Whisper.View.extend({
- tagName: 'span',
- className: 'recipient',
- events: {
- 'click .remove': 'removeModel',
- },
- templateName: 'contact_pill',
- initialize() {
- const error = this.model.validate(this.model.attributes);
- if (error) {
- this.$el.addClass('error');
- }
- },
- removeModel() {
- this.$el.trigger('remove', { modelId: this.model.id });
- this.remove();
- },
- render_attributes() {
- return { name: this.model.getTitle() };
- },
- });
-
- Whisper.RecipientListView = Whisper.ListView.extend({
- itemView: Whisper.ContactPillView,
- });
-
- Whisper.SuggestionView = Whisper.ConversationListItemView.extend({
- className: 'contact-details contact',
- templateName: 'contact_name_and_number',
- });
-
- Whisper.SuggestionListView = Whisper.ConversationListView.extend({
- itemView: Whisper.SuggestionView,
- });
-
- Whisper.RecipientsInputView = Whisper.View.extend({
- className: 'recipients-input',
- templateName: 'recipients-input',
- initialize(options) {
- if (options) {
- this.placeholder = options.placeholder;
- }
- this.render();
- this.$input = this.$('input.search');
- this.$new_contact = this.$('.new-contact');
-
- // Collection of recipients selected for the new message
- this.recipients = new Whisper.ConversationCollection([], {
- comparator: false,
- });
-
- // View to display the selected recipients
- this.recipients_view = new Whisper.RecipientListView({
- collection: this.recipients,
- el: this.$('.recipients'),
- });
-
- // Collection of contacts to match user input against
- this.typeahead = new ContactsTypeahead();
- this.typeahead.fetchContacts();
-
- // View to display the matched contacts from typeahead
- this.typeahead_view = new Whisper.SuggestionListView({
- collection: new Whisper.ConversationCollection([], {
- comparator(m) {
- return m.getTitle().toLowerCase();
- },
- }),
- });
- this.$('.contacts').append(this.typeahead_view.el);
- this.initNewContact();
- this.listenTo(this.typeahead, 'reset', this.filterContacts);
- },
-
- render_attributes() {
- return { placeholder: this.placeholder || 'name or phone number' };
- },
-
- events: {
- 'input input.search': 'filterContacts',
- 'select .new-contact': 'addNewRecipient',
- 'select .contacts': 'addRecipient',
- 'remove .recipient': 'removeRecipient',
- },
-
- filterContacts() {
- const query = this.$input.val();
- if (query.length) {
- if (this.maybeNumber(query)) {
- this.new_contact_view.model.set('id', query);
- this.new_contact_view.render().$el.show();
- } else {
- this.new_contact_view.$el.hide();
- }
- this.typeahead_view.collection.reset(this.typeahead.typeahead(query));
- } else {
- this.resetTypeahead();
- }
- },
-
- initNewContact() {
- if (this.new_contact_view) {
- this.new_contact_view.undelegateEvents();
- this.new_contact_view.$el.hide();
- }
- // Creates a view to display a new contact
- this.new_contact_view = new Whisper.ConversationListItemView({
- el: this.$new_contact,
- model: ConversationController.create({
- type: 'private',
- newContact: true,
- }),
- }).render();
- },
-
- addNewRecipient() {
- this.recipients.add(this.new_contact_view.model);
- this.initNewContact();
- this.resetTypeahead();
- },
-
- addRecipient(e, conversation) {
- this.recipients.add(this.typeahead.remove(conversation.id));
- this.resetTypeahead();
- },
-
- removeRecipient(e, data) {
- const model = this.recipients.remove(data.modelId);
- if (!model.get('newContact')) {
- this.typeahead.add(model);
- }
- this.filterContacts();
- },
-
- reset() {
- this.delegateEvents();
- this.typeahead_view.delegateEvents();
- this.recipients_view.delegateEvents();
- this.new_contact_view.delegateEvents();
- this.typeahead.add(
- this.recipients.filter(model => !model.get('newContact'))
- );
- this.recipients.reset([]);
- this.resetTypeahead();
- this.typeahead.fetchContacts();
- },
-
- resetTypeahead() {
- this.new_contact_view.$el.hide();
- this.$input.val('').focus();
- this.typeahead_view.collection.reset([]);
- },
-
- maybeNumber(number) {
- return number.match(/^\+?[0-9]*$/);
- },
- });
-})();
diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js
index c417434837..a8a7f23bae 100644
--- a/libtextsecure/message_receiver.js
+++ b/libtextsecure/message_receiver.js
@@ -229,6 +229,8 @@ MessageReceiver.prototype.extend({
const job = () => appJobPromise;
this.appPromise = promise.then(job, job);
+
+ return Promise.resolve();
},
onclose(ev) {
window.log.info(
@@ -868,7 +870,7 @@ MessageReceiver.prototype.extend({
p = this.handleEndSession(destination);
}
return p.then(() =>
- this.processDecrypted(envelope, msg, this.number).then(message => {
+ this.processDecrypted(envelope, msg).then(message => {
const groupId = message.group && message.group.id;
const isBlocked = this.isGroupBlocked(groupId);
const isMe = envelope.source === textsecure.storage.user.getNumber();
@@ -910,7 +912,7 @@ MessageReceiver.prototype.extend({
p = this.handleEndSession(envelope.source);
}
return p.then(() =>
- this.processDecrypted(envelope, msg, envelope.source).then(message => {
+ this.processDecrypted(envelope, msg).then(message => {
const groupId = message.group && message.group.id;
const isBlocked = this.isGroupBlocked(groupId);
const isMe = envelope.source === textsecure.storage.user.getNumber();
@@ -1168,39 +1170,13 @@ MessageReceiver.prototype.extend({
let groupDetails = groupBuffer.next();
const promises = [];
while (groupDetails !== undefined) {
- const getGroupDetails = details => {
- // eslint-disable-next-line no-param-reassign
- details.id = details.id.toBinary();
- if (details.active) {
- return textsecure.storage.groups
- .getGroup(details.id)
- .then(existingGroup => {
- if (existingGroup === undefined) {
- return textsecure.storage.groups.createNewGroup(
- details.members,
- details.id
- );
- }
- return textsecure.storage.groups.updateNumbers(
- details.id,
- details.members
- );
- })
- .then(() => details);
- }
- return Promise.resolve(details);
- };
-
- const promise = getGroupDetails(groupDetails)
- .then(details => {
- const ev = new Event('group');
- ev.confirm = this.removeFromCache.bind(this, envelope);
- ev.groupDetails = details;
- return this.dispatchAndWait(ev);
- })
- .catch(e => {
- window.log.error('error processing group', e);
- });
+ groupDetails.id = groupDetails.id.toBinary();
+ const ev = new Event('group');
+ ev.confirm = this.removeFromCache.bind(this, envelope);
+ ev.groupDetails = groupDetails;
+ const promise = this.dispatchAndWait(ev).catch(e => {
+ window.log.error('error processing group', e);
+ });
groupDetails = groupBuffer.next();
promises.push(promise);
}
@@ -1275,7 +1251,7 @@ MessageReceiver.prototype.extend({
})
);
},
- processDecrypted(envelope, decrypted, source) {
+ processDecrypted(envelope, decrypted) {
/* eslint-disable no-bitwise, no-param-reassign */
const FLAGS = textsecure.protobuf.DataMessage.Flags;
@@ -1311,63 +1287,24 @@ MessageReceiver.prototype.extend({
if (decrypted.group !== null) {
decrypted.group.id = decrypted.group.id.toBinary();
- const storageGroups = textsecure.storage.groups;
-
- promises.push(
- storageGroups.getNumbers(decrypted.group.id).then(existingGroup => {
- if (existingGroup === undefined) {
- if (
- decrypted.group.type !==
- textsecure.protobuf.GroupContext.Type.UPDATE
- ) {
- decrypted.group.members = [source];
- window.log.warn('Got message for unknown group');
- }
- return textsecure.storage.groups.createNewGroup(
- decrypted.group.members,
- decrypted.group.id
- );
- }
- const fromIndex = existingGroup.indexOf(source);
-
- if (fromIndex < 0) {
- // TODO: This could be indication of a race...
- window.log.warn(
- 'Sender was not a member of the group they were sending from'
- );
- }
-
- switch (decrypted.group.type) {
- case textsecure.protobuf.GroupContext.Type.UPDATE:
- decrypted.body = null;
- decrypted.attachments = [];
- return textsecure.storage.groups.updateNumbers(
- decrypted.group.id,
- decrypted.group.members
- );
- case textsecure.protobuf.GroupContext.Type.QUIT:
- decrypted.body = null;
- decrypted.attachments = [];
- if (source === this.number) {
- return textsecure.storage.groups.deleteGroup(
- decrypted.group.id
- );
- }
- return textsecure.storage.groups.removeNumber(
- decrypted.group.id,
- source
- );
- case textsecure.protobuf.GroupContext.Type.DELIVER:
- decrypted.group.name = null;
- decrypted.group.members = [];
- decrypted.group.avatar = null;
- return Promise.resolve();
- default:
- this.removeFromCache(envelope);
- throw new Error('Unknown group message type');
- }
- })
- );
+ switch (decrypted.group.type) {
+ case textsecure.protobuf.GroupContext.Type.UPDATE:
+ decrypted.body = null;
+ decrypted.attachments = [];
+ break;
+ case textsecure.protobuf.GroupContext.Type.QUIT:
+ decrypted.body = null;
+ decrypted.attachments = [];
+ break;
+ case textsecure.protobuf.GroupContext.Type.DELIVER:
+ decrypted.group.name = null;
+ decrypted.group.members = [];
+ decrypted.group.avatar = null;
+ break;
+ default:
+ this.removeFromCache(envelope);
+ throw new Error('Unknown group message type');
+ }
}
const attachmentCount = decrypted.attachments.length;
diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js
index 08ade90367..5357d1d73a 100644
--- a/libtextsecure/sendmessage.js
+++ b/libtextsecure/sendmessage.js
@@ -560,7 +560,7 @@ MessageSender.prototype = {
async sendTypingMessage(options = {}, sendOptions = {}) {
const ACTION_ENUM = textsecure.protobuf.TypingMessage.Action;
- const { recipientId, groupId, isTyping, timestamp } = options;
+ const { recipientId, groupId, groupNumbers, isTyping, timestamp } = options;
// We don't want to send typing messages to our other devices, but we will
// in the group case.
@@ -574,7 +574,7 @@ MessageSender.prototype = {
}
const recipients = groupId
- ? _.without(await textsecure.storage.groups.getNumbers(groupId), myNumber)
+ ? _.without(groupNumbers, myNumber)
: [recipientId];
const groupIdBuffer = groupId
? window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(groupId)
@@ -882,6 +882,7 @@ MessageSender.prototype = {
sendMessageToGroup(
groupId,
+ groupNumbers,
messageText,
attachments,
quote,
@@ -891,59 +892,50 @@ MessageSender.prototype = {
profileKey,
options
) {
- return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => {
- if (targetNumbers === undefined) {
- return Promise.reject(new Error('Unknown Group'));
- }
+ const me = textsecure.storage.user.getNumber();
+ const numbers = groupNumbers.filter(number => number !== me);
+ if (numbers.length === 0) {
+ return Promise.reject(new Error('No other members in the group'));
+ }
- const me = textsecure.storage.user.getNumber();
- const numbers = targetNumbers.filter(number => number !== me);
- if (numbers.length === 0) {
- return Promise.reject(new Error('No other members in the group'));
- }
-
- return this.sendMessage(
- {
- recipients: numbers,
- body: messageText,
- timestamp,
- attachments,
- quote,
- preview,
- needsSync: true,
- expireTimer,
- profileKey,
- group: {
- id: groupId,
- type: textsecure.protobuf.GroupContext.Type.DELIVER,
- },
+ return this.sendMessage(
+ {
+ recipients: numbers,
+ body: messageText,
+ timestamp,
+ attachments,
+ quote,
+ preview,
+ needsSync: true,
+ expireTimer,
+ profileKey,
+ group: {
+ id: groupId,
+ type: textsecure.protobuf.GroupContext.Type.DELIVER,
},
- options
- );
- });
+ },
+ options
+ );
},
- createGroup(targetNumbers, name, avatar, options) {
+ createGroup(targetNumbers, id, name, avatar, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
+ proto.group.id = stringToArrayBuffer(id);
- return textsecure.storage.groups
- .createNewGroup(targetNumbers)
- .then(group => {
- proto.group.id = stringToArrayBuffer(group.id);
- const { numbers } = group;
+ proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
+ proto.group.members = targetNumbers;
+ proto.group.name = name;
- proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
- proto.group.members = numbers;
- proto.group.name = name;
-
- return this.makeAttachmentPointer(avatar).then(attachment => {
- proto.group.avatar = attachment;
- return this.sendGroupProto(numbers, proto, Date.now(), options).then(
- () => proto.group.id
- );
- });
- });
+ return this.makeAttachmentPointer(avatar).then(attachment => {
+ proto.group.avatar = attachment;
+ return this.sendGroupProto(
+ targetNumbers,
+ proto,
+ Date.now(),
+ options
+ ).then(() => proto.group.id);
+ });
},
updateGroup(groupId, name, avatar, targetNumbers, options) {
@@ -953,121 +945,87 @@ MessageSender.prototype = {
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
+ proto.group.members = targetNumbers;
- return textsecure.storage.groups
- .addNumbers(groupId, targetNumbers)
- .then(numbers => {
- if (numbers === undefined) {
- return Promise.reject(new Error('Unknown Group'));
- }
- proto.group.members = numbers;
-
- return this.makeAttachmentPointer(avatar).then(attachment => {
- proto.group.avatar = attachment;
- return this.sendGroupProto(numbers, proto, Date.now(), options).then(
- () => proto.group.id
- );
- });
- });
+ return this.makeAttachmentPointer(avatar).then(attachment => {
+ proto.group.avatar = attachment;
+ return this.sendGroupProto(
+ targetNumbers,
+ proto,
+ Date.now(),
+ options
+ ).then(() => proto.group.id);
+ });
},
- addNumberToGroup(groupId, number, options) {
+ addNumberToGroup(groupId, newNumbers, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
-
- return textsecure.storage.groups
- .addNumbers(groupId, [number])
- .then(numbers => {
- if (numbers === undefined)
- return Promise.reject(new Error('Unknown Group'));
- proto.group.members = numbers;
-
- return this.sendGroupProto(numbers, proto, Date.now(), options);
- });
+ proto.group.members = newNumbers;
+ return this.sendGroupProto(newNumbers, proto, Date.now(), options);
},
- setGroupName(groupId, name, options) {
+ setGroupName(groupId, name, groupNumbers, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
+ proto.group.members = groupNumbers;
- return textsecure.storage.groups.getNumbers(groupId).then(numbers => {
- if (numbers === undefined)
- return Promise.reject(new Error('Unknown Group'));
- proto.group.members = numbers;
-
- return this.sendGroupProto(numbers, proto, Date.now(), options);
- });
+ return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
},
- setGroupAvatar(groupId, avatar, options) {
+ setGroupAvatar(groupId, avatar, groupNumbers, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
+ proto.group.members = groupNumbers;
- return textsecure.storage.groups.getNumbers(groupId).then(numbers => {
- if (numbers === undefined)
- return Promise.reject(new Error('Unknown Group'));
- proto.group.members = numbers;
-
- return this.makeAttachmentPointer(avatar).then(attachment => {
- proto.group.avatar = attachment;
- return this.sendGroupProto(numbers, proto, Date.now(), options);
- });
+ return this.makeAttachmentPointer(avatar).then(attachment => {
+ proto.group.avatar = attachment;
+ return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
});
},
- leaveGroup(groupId, options) {
+ leaveGroup(groupId, groupNumbers, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT;
-
- return textsecure.storage.groups.getNumbers(groupId).then(numbers => {
- if (numbers === undefined)
- return Promise.reject(new Error('Unknown Group'));
- return textsecure.storage.groups
- .deleteGroup(groupId)
- .then(() => this.sendGroupProto(numbers, proto, Date.now(), options));
- });
+ return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
},
sendExpirationTimerUpdateToGroup(
groupId,
+ groupNumbers,
expireTimer,
timestamp,
profileKey,
options
) {
- return textsecure.storage.groups.getNumbers(groupId).then(targetNumbers => {
- if (targetNumbers === undefined)
- return Promise.reject(new Error('Unknown Group'));
-
- const me = textsecure.storage.user.getNumber();
- const numbers = targetNumbers.filter(number => number !== me);
- if (numbers.length === 0) {
- return Promise.reject(new Error('No other members in the group'));
- }
- return this.sendMessage(
- {
- recipients: numbers,
- timestamp,
- needsSync: true,
- expireTimer,
- profileKey,
- flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
- group: {
- id: groupId,
- type: textsecure.protobuf.GroupContext.Type.DELIVER,
- },
+ const me = textsecure.storage.user.getNumber();
+ const numbers = groupNumbers.filter(number => number !== me);
+ if (numbers.length === 0) {
+ return Promise.reject(new Error('No other members in the group'));
+ }
+ return this.sendMessage(
+ {
+ recipients: numbers,
+ timestamp,
+ needsSync: true,
+ expireTimer,
+ profileKey,
+ flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
+ group: {
+ id: groupId,
+ type: textsecure.protobuf.GroupContext.Type.DELIVER,
},
- options
- );
- });
+ },
+ options
+ );
},
sendExpirationTimerUpdateToNumber(
number,
diff --git a/libtextsecure/storage/groups.js b/libtextsecure/storage/groups.js
deleted file mode 100644
index 67460c15e2..0000000000
--- a/libtextsecure/storage/groups.js
+++ /dev/null
@@ -1,160 +0,0 @@
-/* global window, getString, libsignal, textsecure */
-
-/* eslint-disable more/no-then */
-
-// eslint-disable-next-line func-names
-(function() {
- /** *******************
- *** Group Storage ***
- ******************** */
- window.textsecure = window.textsecure || {};
- window.textsecure.storage = window.textsecure.storage || {};
-
- // create a random group id that we haven't seen before.
- function generateNewGroupId() {
- const groupId = getString(libsignal.crypto.getRandomBytes(16));
- return textsecure.storage.protocol.getGroup(groupId).then(group => {
- if (group === undefined) {
- return groupId;
- }
- window.log.warn('group id collision'); // probably a bad sign.
- return generateNewGroupId();
- });
- }
-
- window.textsecure.storage.groups = {
- createNewGroup(numbers, groupId) {
- return new Promise(resolve => {
- if (groupId !== undefined) {
- resolve(
- textsecure.storage.protocol.getGroup(groupId).then(group => {
- if (group !== undefined) {
- throw new Error('Tried to recreate group');
- }
- })
- );
- } else {
- resolve(
- generateNewGroupId().then(newGroupId => {
- // eslint-disable-next-line no-param-reassign
- groupId = newGroupId;
- })
- );
- }
- }).then(() => {
- const me = textsecure.storage.user.getNumber();
- let haveMe = false;
- const finalNumbers = [];
- // eslint-disable-next-line no-restricted-syntax, guard-for-in
- for (const i in numbers) {
- const number = numbers[i];
- if (!textsecure.utils.isNumberSane(number))
- throw new Error('Invalid number in group');
- if (number === me) haveMe = true;
- if (finalNumbers.indexOf(number) < 0) finalNumbers.push(number);
- }
-
- if (!haveMe) finalNumbers.push(me);
-
- const groupObject = {
- numbers: finalNumbers,
- numberRegistrationIds: {},
- };
- // eslint-disable-next-line no-restricted-syntax, guard-for-in
- for (const i in finalNumbers) {
- groupObject.numberRegistrationIds[finalNumbers[i]] = {};
- }
-
- return textsecure.storage.protocol
- .putGroup(groupId, groupObject)
- .then(() => ({ id: groupId, numbers: finalNumbers }));
- });
- },
-
- getNumbers(groupId) {
- return textsecure.storage.protocol.getGroup(groupId).then(group => {
- if (!group) {
- return undefined;
- }
-
- return group.numbers;
- });
- },
-
- removeNumber(groupId, number) {
- return textsecure.storage.protocol.getGroup(groupId).then(group => {
- if (group === undefined) return undefined;
-
- const me = textsecure.storage.user.getNumber();
- if (number === me)
- throw new Error(
- 'Cannot remove ourselves from a group, leave the group instead'
- );
-
- const i = group.numbers.indexOf(number);
- if (i > -1) {
- group.numbers.splice(i, 1);
- // eslint-disable-next-line no-param-reassign
- delete group.numberRegistrationIds[number];
- return textsecure.storage.protocol
- .putGroup(groupId, group)
- .then(() => group.numbers);
- }
-
- return group.numbers;
- });
- },
-
- addNumbers(groupId, numbers) {
- return textsecure.storage.protocol.getGroup(groupId).then(group => {
- if (group === undefined) return undefined;
-
- // eslint-disable-next-line no-restricted-syntax, guard-for-in
- for (const i in numbers) {
- const number = numbers[i];
- if (!textsecure.utils.isNumberSane(number))
- throw new Error('Invalid number in set to add to group');
- if (group.numbers.indexOf(number) < 0) {
- group.numbers.push(number);
- // eslint-disable-next-line no-param-reassign
- group.numberRegistrationIds[number] = {};
- }
- }
-
- return textsecure.storage.protocol
- .putGroup(groupId, group)
- .then(() => group.numbers);
- });
- },
-
- deleteGroup(groupId) {
- return textsecure.storage.protocol.removeGroup(groupId);
- },
-
- getGroup(groupId) {
- return textsecure.storage.protocol.getGroup(groupId).then(group => {
- if (group === undefined) return undefined;
-
- return { id: groupId, numbers: group.numbers };
- });
- },
-
- updateNumbers(groupId, numbers) {
- return textsecure.storage.protocol.getGroup(groupId).then(group => {
- if (group === undefined)
- throw new Error('Tried to update numbers for unknown group');
-
- if (
- numbers.filter(textsecure.utils.isNumberSane).length < numbers.length
- )
- throw new Error('Invalid number in new group members');
-
- const added = numbers.filter(
- number => group.numbers.indexOf(number) < 0
- );
-
- return textsecure.storage.groups.addNumbers(groupId, added);
- });
- },
- };
-})();
diff --git a/test/index.html b/test/index.html
index 64964f8962..8d64375df2 100644
--- a/test/index.html
+++ b/test/index.html
@@ -362,8 +362,6 @@
-
-
@@ -386,7 +384,6 @@
-
diff --git a/test/views/group_update_view_test.js b/test/views/group_update_view_test.js
deleted file mode 100644
index 45c9a7440c..0000000000
--- a/test/views/group_update_view_test.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/* global Whisper */
-
-describe('GroupUpdateView', () => {
- it('should show new group members', () => {
- const view = new Whisper.GroupUpdateView({
- model: { joined: ['Alice', 'Bob'] },
- }).render();
- assert.match(view.$el.text(), /Alice.*Bob.*joined the group/);
- });
-
- it('should note updates to the title', () => {
- const view = new Whisper.GroupUpdateView({
- model: { name: 'New name' },
- }).render();
- assert.match(view.$el.text(), /Title is now 'New name'/);
- });
-
- it('should say "Updated the group"', () => {
- const view = new Whisper.GroupUpdateView({
- model: { avatar: 'New avatar' },
- }).render();
- assert.match(view.$el.text(), /Updated the group/);
- });
-});