Remove groups table, conversation is single source of truth
This commit is contained in:
parent
b69eea543c
commit
5b54c9554e
16 changed files with 214 additions and 912 deletions
49
app/sql.js
49
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;'),
|
||||
|
|
|
@ -615,7 +615,6 @@
|
|||
<script type='text/javascript' src='js/views/conversation_list_item_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/conversation_list_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/contact_list_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/new_group_update_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/attachment_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/timestamp_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/message_view.js'></script>
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -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]*$/);
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -362,8 +362,6 @@
|
|||
<script type='text/javascript' src='../js/views/conversation_list_item_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/conversation_list_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/contact_list_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/new_group_update_view.js' data-cover></script>
|
||||
<script type="text/javascript" src="../js/views/group_update_view.js"></script>
|
||||
<script type='text/javascript' src='../js/views/attachment_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/timestamp_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/message_view.js' data-cover></script>
|
||||
|
@ -386,7 +384,6 @@
|
|||
<script type="text/javascript" src="metadata/SecretSessionCipher_test.js"></script>
|
||||
|
||||
<script type="text/javascript" src="views/whisper_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/group_update_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/attachment_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/timestamp_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/list_view_test.js"></script>
|
||||
|
|
|
@ -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/);
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue