Remove groups table, conversation is single source of truth
parent
b69eea543c
commit
5b54c9554e
@ -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]*$/);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})();
|
|
@ -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);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})();
|
|
@ -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…
Reference in New Issue