Support for new GroupV2 groups
This commit is contained in:
parent
1ce0959fa1
commit
7a02cc815d
53 changed files with 7326 additions and 839 deletions
426
js/background.js
426
js/background.js
|
@ -546,7 +546,7 @@
|
|||
}
|
||||
|
||||
if (
|
||||
window.isBeforeVersion(lastVersion, 'v1.35.0-beta.11') &&
|
||||
window.isBeforeVersion(lastVersion, 'v1.36.0-beta.1') &&
|
||||
window.isAfterVersion(lastVersion, 'v1.35.0-beta.1')
|
||||
) {
|
||||
await window.Signal.Services.eraseAllStorageServiceState();
|
||||
|
@ -606,6 +606,17 @@
|
|||
// flags are represented in the cached props we generate on load of each convo.
|
||||
window.Signal.RemoteConfig.initRemoteConfig();
|
||||
|
||||
// On startup, we don't want to wait for the remote config fetch if we've already
|
||||
// learned that this instance supports GroupsV2.
|
||||
// This is how we keep it sticky. Once it is enabled, we never disable it.
|
||||
if (
|
||||
window.Signal.RemoteConfig.isEnabled('desktop.gv2') ||
|
||||
window.storage.get('gv2-enabled')
|
||||
) {
|
||||
window.GV2 = true;
|
||||
window.storage.put('gv2-enabled', true);
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
ConversationController.load(),
|
||||
|
@ -1552,6 +1563,33 @@
|
|||
removeMessageRequestListener();
|
||||
}
|
||||
);
|
||||
|
||||
// Listen for changes to the `desktop.gv2` remote configuration flag
|
||||
const removeGv2Listener = window.Signal.RemoteConfig.onChange(
|
||||
'desktop.gv2',
|
||||
async ({ enabled }) => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.GV2 = true;
|
||||
|
||||
await window.storage.put('gv2-enabled', true);
|
||||
|
||||
window.Signal.Services.handleUnknownRecords(
|
||||
window.textsecure.protobuf.ManifestRecord.Identifier.Type.GROUPV2
|
||||
);
|
||||
|
||||
// Erase current manifest version so we re-process storage service data
|
||||
await window.storage.remove('manifestVersion');
|
||||
|
||||
// Kick off storage service fetch to grab GroupV2 information
|
||||
await window.Signal.Services.runStorageServiceSyncJob();
|
||||
|
||||
// This is a one-time thing
|
||||
removeGv2Listener();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
window.getSyncRequest = () =>
|
||||
|
@ -1657,45 +1695,58 @@
|
|||
PASSWORD
|
||||
);
|
||||
|
||||
try {
|
||||
if (connectCount === 0) {
|
||||
const lonelyE164s = window
|
||||
.getConversations()
|
||||
.filter(
|
||||
c =>
|
||||
c.isPrivate() &&
|
||||
c.get('e164') &&
|
||||
!c.get('uuid') &&
|
||||
!c.isEverUnregistered()
|
||||
)
|
||||
.map(c => c.get('e164'));
|
||||
|
||||
if (lonelyE164s.length > 0) {
|
||||
const lookup = await textsecure.messaging.getUuidsForE164s(
|
||||
lonelyE164s
|
||||
);
|
||||
const e164s = Object.keys(lookup);
|
||||
e164s.forEach(e164 => {
|
||||
const uuid = lookup[e164];
|
||||
if (!uuid) {
|
||||
const byE164 = window.ConversationController.get(e164);
|
||||
if (byE164) {
|
||||
byE164.setUnregistered();
|
||||
}
|
||||
}
|
||||
window.ConversationController.ensureContactIds({
|
||||
e164,
|
||||
uuid,
|
||||
highTrust: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
if (connectCount === 0) {
|
||||
try {
|
||||
// Force a re-fetch before we process our queue. We may want to turn on something
|
||||
// which changes how we process incoming messages!
|
||||
await window.Signal.RemoteConfig.refreshRemoteConfig();
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'connect: Error refreshing remote config:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (window.Signal.RemoteConfig.isEnabled('desktop.cds')) {
|
||||
const lonelyE164s = window
|
||||
.getConversations()
|
||||
.filter(
|
||||
c =>
|
||||
c.isPrivate() &&
|
||||
c.get('e164') &&
|
||||
!c.get('uuid') &&
|
||||
!c.isEverUnregistered()
|
||||
)
|
||||
.map(c => c.get('e164'));
|
||||
|
||||
if (lonelyE164s.length > 0) {
|
||||
const lookup = await textsecure.messaging.getUuidsForE164s(
|
||||
lonelyE164s
|
||||
);
|
||||
const e164s = Object.keys(lookup);
|
||||
e164s.forEach(e164 => {
|
||||
const uuid = lookup[e164];
|
||||
if (!uuid) {
|
||||
const byE164 = window.ConversationController.get(e164);
|
||||
if (byE164) {
|
||||
byE164.setUnregistered();
|
||||
}
|
||||
}
|
||||
window.ConversationController.ensureContactIds({
|
||||
e164,
|
||||
uuid,
|
||||
highTrust: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'connect: Error fetching UUIDs for lonely e164s:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Error fetching UUIDs for lonely e164s:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
|
||||
connectCount += 1;
|
||||
|
@ -1718,6 +1769,8 @@
|
|||
);
|
||||
window.textsecure.messageReceiver = messageReceiver;
|
||||
|
||||
window.Signal.Services.initializeGroupCredentialFetcher();
|
||||
|
||||
preMessageReceiverStatus = null;
|
||||
|
||||
function addQueuedEventListener(name, handler) {
|
||||
|
@ -1810,26 +1863,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: uncomment this once we want to start registering UUID support
|
||||
// const hasRegisteredUuidSupportKey = 'hasRegisteredUuidSupport';
|
||||
// if (
|
||||
// !storage.get(hasRegisteredUuidSupportKey) &&
|
||||
// textsecure.storage.user.getUuid()
|
||||
// ) {
|
||||
// const server = WebAPI.connect({
|
||||
// username: USERNAME || OLD_USERNAME,
|
||||
// password: PASSWORD,
|
||||
// });
|
||||
// try {
|
||||
// await server.registerCapabilities({ uuid: true });
|
||||
// storage.put(hasRegisteredUuidSupportKey, true);
|
||||
// } catch (error) {
|
||||
// window.log.error(
|
||||
// 'Error: Unable to register support for UUID messages.',
|
||||
// error && error.stack ? error.stack : error
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
const hasRegisteredGroupV2SupportKey = 'hasRegisteredGroupV2Support';
|
||||
if (
|
||||
!storage.get(hasRegisteredGroupV2SupportKey) &&
|
||||
textsecure.storage.user.getUuid()
|
||||
) {
|
||||
const server = WebAPI.connect({
|
||||
username: USERNAME || OLD_USERNAME,
|
||||
password: PASSWORD,
|
||||
});
|
||||
try {
|
||||
await server.registerCapabilities({ gv2: true });
|
||||
storage.put(hasRegisteredGroupV2SupportKey, true);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Error: Unable to register support for GroupV2.',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const deviceId = textsecure.storage.user.getDeviceId();
|
||||
|
||||
|
@ -1918,6 +1970,49 @@
|
|||
view.applyTheme();
|
||||
}
|
||||
}
|
||||
|
||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||
|
||||
// Note: once this function returns, there still might be messages being processed on
|
||||
// a given conversation's queue. But we have processed all events from the websocket.
|
||||
async function waitForEmptyEventQueue() {
|
||||
if (!messageReceiver) {
|
||||
window.log.info(
|
||||
'waitForEmptyEventQueue: No messageReceiver available, returning early'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!messageReceiver.hasEmptied()) {
|
||||
window.log.info(
|
||||
'waitForEmptyEventQueue: Waiting for MessageReceiver empty event...'
|
||||
);
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise((innerResolve, innerReject) => {
|
||||
resolve = innerResolve;
|
||||
reject = innerReject;
|
||||
});
|
||||
|
||||
const timeout = setTimeout(reject, FIVE_MINUTES);
|
||||
const onEmptyOnce = () => {
|
||||
messageReceiver.removeEventListener('empty', onEmptyOnce);
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
};
|
||||
messageReceiver.addEventListener('empty', onEmptyOnce);
|
||||
|
||||
await promise;
|
||||
}
|
||||
|
||||
window.log.info(
|
||||
'waitForEmptyEventQueue: Waiting for event handler queue idle...'
|
||||
);
|
||||
await eventHandlerQueue.onIdle();
|
||||
}
|
||||
|
||||
window.waitForEmptyEventQueue = waitForEmptyEventQueue;
|
||||
|
||||
async function onEmpty() {
|
||||
await Promise.all([
|
||||
window.waitForAllBatchers(),
|
||||
|
@ -1937,11 +2032,6 @@
|
|||
logger: window.log,
|
||||
});
|
||||
|
||||
// Force a re-fetch here when we've processed our queue. Without this, we won't try
|
||||
// again for two hours after our first attempt. Which might have been while we were
|
||||
// offline or didn't have credentials.
|
||||
window.Signal.RemoteConfig.refreshRemoteConfig();
|
||||
|
||||
let interval = setInterval(() => {
|
||||
const view = window.owsDesktopApp.appView;
|
||||
if (view) {
|
||||
|
@ -2024,7 +2114,7 @@
|
|||
// Note: this type of message is automatically removed from cache in MessageReceiver
|
||||
|
||||
const { typing, sender, senderUuid, senderDevice } = ev;
|
||||
const { groupId, started } = typing || {};
|
||||
const { groupId, groupV2Id, started } = typing || {};
|
||||
|
||||
// We don't do anything with incoming typing messages if the setting is disabled
|
||||
if (!storage.get('typingIndicators')) {
|
||||
|
@ -2036,27 +2126,34 @@
|
|||
uuid: senderUuid,
|
||||
highTrust: true,
|
||||
});
|
||||
const conversation = ConversationController.get(groupId || senderId);
|
||||
const conversation = ConversationController.get(
|
||||
groupV2Id || groupId || senderId
|
||||
);
|
||||
const ourId = ConversationController.getOurConversationId();
|
||||
|
||||
if (conversation) {
|
||||
// We drop typing notifications in groups we're not a part of
|
||||
if (!conversation.isPrivate() && !conversation.hasMember(ourId)) {
|
||||
window.log.warn(
|
||||
`Received typing indicator for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
conversation.notifyTyping({
|
||||
isTyping: started,
|
||||
isMe: ourId === senderId,
|
||||
sender,
|
||||
senderUuid,
|
||||
senderId,
|
||||
senderDevice,
|
||||
});
|
||||
if (!conversation) {
|
||||
window.log.warn(
|
||||
`onTyping: Did not find conversation for typing indicator (groupv2(${groupV2Id}), group(${groupId}), ${sender}, ${senderUuid})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// We drop typing notifications in groups we're not a part of
|
||||
if (!conversation.isPrivate() && !conversation.hasMember(ourId)) {
|
||||
window.log.warn(
|
||||
`Received typing indicator for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
conversation.notifyTyping({
|
||||
isTyping: started,
|
||||
isMe: ourId === senderId,
|
||||
sender,
|
||||
senderUuid,
|
||||
senderId,
|
||||
senderDevice,
|
||||
});
|
||||
}
|
||||
|
||||
async function onStickerPack(ev) {
|
||||
|
@ -2227,6 +2324,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Note: this handler is only for v1 groups received via 'group sync' messages
|
||||
async function onGroupReceived(ev) {
|
||||
const details = ev.groupDetails;
|
||||
const { id } = details;
|
||||
|
@ -2244,6 +2342,13 @@
|
|||
id,
|
||||
'group'
|
||||
);
|
||||
if (conversation.get('groupVersion') > 1) {
|
||||
window.log.warn(
|
||||
'Got group sync for v2 group: ',
|
||||
conversation.idForLoggoing()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const memberConversations = details.membersE164.map(e164 =>
|
||||
ConversationController.getOrCreate(e164, 'private')
|
||||
|
@ -2321,37 +2426,6 @@
|
|||
);
|
||||
}
|
||||
|
||||
// Descriptors
|
||||
const getGroupDescriptor = group => ({
|
||||
type: Message.GROUP,
|
||||
id: group.id,
|
||||
});
|
||||
|
||||
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
|
||||
const getDescriptorForSent = ({ message, destination, destinationUuid }) =>
|
||||
message.group
|
||||
? getGroupDescriptor(message.group)
|
||||
: {
|
||||
type: Message.PRIVATE,
|
||||
id: ConversationController.ensureContactIds({
|
||||
e164: destination,
|
||||
uuid: destinationUuid,
|
||||
}),
|
||||
};
|
||||
|
||||
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
|
||||
const getDescriptorForReceived = ({ message, source, sourceUuid }) =>
|
||||
message.group
|
||||
? getGroupDescriptor(message.group)
|
||||
: {
|
||||
type: Message.PRIVATE,
|
||||
id: ConversationController.ensureContactIds({
|
||||
e164: source,
|
||||
uuid: sourceUuid,
|
||||
highTrust: true,
|
||||
}),
|
||||
};
|
||||
|
||||
// Received:
|
||||
async function handleMessageReceivedProfileUpdate({
|
||||
data,
|
||||
|
@ -2369,6 +2443,50 @@
|
|||
return confirm();
|
||||
}
|
||||
|
||||
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
|
||||
const getDescriptorForReceived = ({ message, source, sourceUuid }) => {
|
||||
if (message.groupV2) {
|
||||
const { id } = message.groupV2;
|
||||
const conversationId = ConversationController.ensureGroup(id, {
|
||||
groupVersion: 2,
|
||||
masterKey: message.groupV2.masterKey,
|
||||
secretParams: message.groupV2.secretParams,
|
||||
publicParams: message.groupV2.publicParams,
|
||||
});
|
||||
|
||||
return {
|
||||
type: Message.GROUP,
|
||||
id: conversationId,
|
||||
};
|
||||
}
|
||||
if (message.group) {
|
||||
const { id } = message.group;
|
||||
const fromContactId = ConversationController.ensureContactIds({
|
||||
e164: source,
|
||||
uuid: sourceUuid,
|
||||
highTrust: true,
|
||||
});
|
||||
|
||||
const conversationId = ConversationController.ensureGroup(id, {
|
||||
addedBy: fromContactId,
|
||||
});
|
||||
|
||||
return {
|
||||
type: Message.GROUP,
|
||||
id: conversationId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: Message.PRIVATE,
|
||||
id: ConversationController.ensureContactIds({
|
||||
e164: source,
|
||||
uuid: sourceUuid,
|
||||
highTrust: true,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
// Note: We do very little in this function, since everything in handleDataMessage is
|
||||
// inside a conversation-specific queue(). Any code here might run before an earlier
|
||||
// message is processed in handleDataMessage().
|
||||
|
@ -2392,13 +2510,16 @@
|
|||
|
||||
if (data.message.reaction) {
|
||||
const { reaction } = data.message;
|
||||
window.log.info('Queuing reaction for', reaction.targetTimestamp);
|
||||
window.log.info(
|
||||
'Queuing incoming reaction for',
|
||||
reaction.targetTimestamp
|
||||
);
|
||||
const reactionModel = Whisper.Reactions.add({
|
||||
emoji: reaction.emoji,
|
||||
remove: reaction.remove,
|
||||
targetAuthorE164: reaction.targetAuthorE164,
|
||||
targetAuthorUuid: reaction.targetAuthorUuid,
|
||||
targetTimestamp: reaction.targetTimestamp.toNumber(),
|
||||
targetTimestamp: reaction.targetTimestamp,
|
||||
timestamp: Date.now(),
|
||||
fromId: ConversationController.ensureContactIds({
|
||||
e164: data.source,
|
||||
|
@ -2413,7 +2534,7 @@
|
|||
|
||||
if (data.message.delete) {
|
||||
const { delete: del } = data.message;
|
||||
window.log.info('Queuing DOE for', del.targetSentTimestamp);
|
||||
window.log.info('Queuing incoming DOE for', del.targetSentTimestamp);
|
||||
const deleteModel = Whisper.Deletes.add({
|
||||
targetSentTimestamp: del.targetSentTimestamp,
|
||||
serverTimestamp: data.serverTimestamp,
|
||||
|
@ -2508,11 +2629,6 @@
|
|||
data.unidentifiedDeliveries = unidentified.map(item => item.destination);
|
||||
}
|
||||
|
||||
const isGroup = descriptor.type === Message.GROUP;
|
||||
const conversationId = isGroup
|
||||
? ConversationController.ensureGroup(descriptor.id)
|
||||
: descriptor.id;
|
||||
|
||||
return new Whisper.Message({
|
||||
source: textsecure.storage.user.getNumber(),
|
||||
sourceUuid: textsecure.storage.user.getUuid(),
|
||||
|
@ -2521,7 +2637,7 @@
|
|||
serverTimestamp: data.serverTimestamp,
|
||||
sent_to: sentTo,
|
||||
received_at: now,
|
||||
conversationId,
|
||||
conversationId: descriptor.id,
|
||||
type: 'outgoing',
|
||||
sent: true,
|
||||
unidentifiedDeliveries: data.unidentifiedDeliveries || [],
|
||||
|
@ -2532,6 +2648,42 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
|
||||
const getDescriptorForSent = ({ message, destination, destinationUuid }) => {
|
||||
if (message.groupV2) {
|
||||
const { id } = message.groupV2;
|
||||
const conversationId = ConversationController.ensureGroup(id, {
|
||||
groupVersion: 2,
|
||||
masterKey: message.groupV2.masterKey,
|
||||
secretParams: message.groupV2.secretParams,
|
||||
publicParams: message.groupV2.publicParams,
|
||||
});
|
||||
|
||||
return {
|
||||
type: Message.GROUP,
|
||||
id: conversationId,
|
||||
};
|
||||
}
|
||||
if (message.group) {
|
||||
const { id } = message.group;
|
||||
const conversationId = ConversationController.ensureGroup(id);
|
||||
|
||||
return {
|
||||
type: Message.GROUP,
|
||||
id: conversationId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: Message.PRIVATE,
|
||||
id: ConversationController.ensureContactIds({
|
||||
e164: destination,
|
||||
uuid: destinationUuid,
|
||||
highTrust: true,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
// Note: We do very little in this function, since everything in handleDataMessage is
|
||||
// inside a conversation-specific queue(). Any code here might run before an earlier
|
||||
// message is processed in handleDataMessage().
|
||||
|
@ -2555,12 +2707,13 @@
|
|||
|
||||
if (data.message.reaction) {
|
||||
const { reaction } = data.message;
|
||||
window.log.info('Queuing sent reaction for', reaction.targetTimestamp);
|
||||
const reactionModel = Whisper.Reactions.add({
|
||||
emoji: reaction.emoji,
|
||||
remove: reaction.remove,
|
||||
targetAuthorE164: reaction.targetAuthorE164,
|
||||
targetAuthorUuid: reaction.targetAuthorUuid,
|
||||
targetTimestamp: reaction.targetTimestamp.toNumber(),
|
||||
targetTimestamp: reaction.targetTimestamp,
|
||||
timestamp: Date.now(),
|
||||
fromId: ConversationController.getOurConversationId(),
|
||||
fromSync: true,
|
||||
|
@ -2574,6 +2727,7 @@
|
|||
|
||||
if (data.message.delete) {
|
||||
const { delete: del } = data.message;
|
||||
window.log.info('Queuing sent DOE for', del.targetSentTimestamp);
|
||||
const deleteModel = Whisper.Deletes.add({
|
||||
targetSentTimestamp: del.targetSentTimestamp,
|
||||
serverTimestamp: del.serverTimestamp,
|
||||
|
@ -2594,20 +2748,6 @@
|
|||
}
|
||||
|
||||
function initIncomingMessage(data, descriptor) {
|
||||
// Ensure that we have an accurate record for who this message is from
|
||||
const fromContactId = ConversationController.ensureContactIds({
|
||||
e164: data.source,
|
||||
uuid: data.sourceUuid,
|
||||
highTrust: true,
|
||||
});
|
||||
|
||||
const isGroup = descriptor.type === Message.GROUP;
|
||||
const conversationId = isGroup
|
||||
? ConversationController.ensureGroup(descriptor.id, {
|
||||
addedBy: fromContactId,
|
||||
})
|
||||
: fromContactId;
|
||||
|
||||
return new Whisper.Message({
|
||||
source: data.source,
|
||||
sourceUuid: data.sourceUuid,
|
||||
|
@ -2615,7 +2755,7 @@
|
|||
sent_at: data.timestamp,
|
||||
serverTimestamp: data.serverTimestamp,
|
||||
received_at: Date.now(),
|
||||
conversationId,
|
||||
conversationId: descriptor.id,
|
||||
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
|
||||
type: 'incoming',
|
||||
unread: 1,
|
||||
|
@ -2729,6 +2869,14 @@
|
|||
const conversationId = message.get('conversationId');
|
||||
const conversation = ConversationController.get(conversationId);
|
||||
|
||||
if (!conversation) {
|
||||
window.log.warn(
|
||||
'onError: No conversation id, cannot save error bubble'
|
||||
);
|
||||
ev.confirm();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// This matches the queueing behavior used in Message.handleDataMessage
|
||||
conversation.queueJob(async () => {
|
||||
const existingMessage = await window.Signal.Data.getMessageBySender(
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
if (conversation.isPrivate()) {
|
||||
recipients = [conversation.id];
|
||||
} else {
|
||||
recipients = conversation.get('members') || [];
|
||||
recipients = conversation.getMemberIds();
|
||||
}
|
||||
const receipts = this.filter(
|
||||
receipt =>
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
window.log.info(`adding groupId(${groupId}) to blocked list`);
|
||||
window.log.info(`adding group(${groupId}) to blocked list`);
|
||||
storage.put(BLOCKED_GROUPS_ID, groupIds.concat(groupId));
|
||||
};
|
||||
storage.removeBlockedGroup = groupId => {
|
||||
|
|
|
@ -78,6 +78,9 @@
|
|||
const e164 = this.get('e164');
|
||||
return `${uuid || e164} (${this.id})`;
|
||||
}
|
||||
if (this.get('groupVersion') > 1) {
|
||||
return `groupv2(${this.get('groupId')})`;
|
||||
}
|
||||
|
||||
const groupId = this.get('groupId');
|
||||
return `group(${groupId})`;
|
||||
|
@ -403,24 +406,83 @@
|
|||
}
|
||||
},
|
||||
|
||||
async fetchLatestGroupV2Data() {
|
||||
if (this.get('groupVersion') !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
await window.Signal.Groups.waitThenMaybeUpdateGroup({
|
||||
conversation: this,
|
||||
});
|
||||
},
|
||||
maybeRepairGroupV2(data) {
|
||||
if (
|
||||
this.get('groupVersion') &&
|
||||
this.get('masterKey') &&
|
||||
this.get('secretParams') &&
|
||||
this.get('publicParams')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.log.info(`Repairing GroupV2 conversation ${this.idForLogging()}`);
|
||||
const { masterKey, secretParams, publicParams } = data;
|
||||
|
||||
this.set({ masterKey, secretParams, publicParams, groupVersion: 2 });
|
||||
|
||||
window.Signal.Data.updateConversation(this.attributes);
|
||||
},
|
||||
getGroupV2Info(groupChange) {
|
||||
if (this.isPrivate() || this.get('groupVersion') !== 2) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
masterKey: window.Signal.Crypto.base64ToArrayBuffer(
|
||||
this.get('masterKey')
|
||||
),
|
||||
revision: this.get('revision'),
|
||||
members: this.getRecipients(),
|
||||
groupChange,
|
||||
};
|
||||
},
|
||||
getGroupV1Info() {
|
||||
if (this.isPrivate() || this.get('groupVersion') > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: this.get('groupId'),
|
||||
members: this.getRecipients(),
|
||||
};
|
||||
},
|
||||
|
||||
sendTypingMessage(isTyping) {
|
||||
if (!textsecure.messaging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupId = !this.isPrivate() ? this.get('groupId') : null;
|
||||
const groupNumbers = this.getRecipients();
|
||||
// We don't send typing messages to our other devices
|
||||
if (this.isMe()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientId = this.isPrivate() ? this.getSendTarget() : null;
|
||||
const groupId = !this.isPrivate() ? this.get('groupId') : null;
|
||||
const groupMembers = this.getRecipients();
|
||||
|
||||
// We don't send typing messages if our recipients list is empty
|
||||
if (!this.isPrivate() && !groupMembers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sendOptions = this.getSendOptions();
|
||||
|
||||
this.wrapSend(
|
||||
textsecure.messaging.sendTypingMessage(
|
||||
{
|
||||
isTyping,
|
||||
recipientId,
|
||||
groupId,
|
||||
groupNumbers,
|
||||
groupMembers,
|
||||
},
|
||||
sendOptions
|
||||
)
|
||||
|
@ -581,7 +643,7 @@
|
|||
lastUpdated: this.get('timestamp'),
|
||||
membersCount: this.isPrivate()
|
||||
? undefined
|
||||
: (this.get('members') || []).length,
|
||||
: (this.get('membersV2') || this.get('members') || []).length,
|
||||
messageRequestsEnabled,
|
||||
muteExpiresAt: this.get('muteExpiresAt'),
|
||||
name: this.get('name'),
|
||||
|
@ -793,7 +855,8 @@
|
|||
return;
|
||||
}
|
||||
|
||||
await this.fetchContacts();
|
||||
this.fetchContacts();
|
||||
|
||||
await Promise.all(
|
||||
this.contactCollection.map(async contact => {
|
||||
if (!contact.isMe()) {
|
||||
|
@ -1324,26 +1387,59 @@
|
|||
return this.jobQueue.add(taskWithTimeout);
|
||||
},
|
||||
|
||||
getRecipients() {
|
||||
getMembers() {
|
||||
if (this.isPrivate()) {
|
||||
return [this.getSendTarget()];
|
||||
return [this];
|
||||
}
|
||||
const me = ConversationController.getOurConversationId();
|
||||
|
||||
// The list of members might not always be conversationIds for old groups.
|
||||
if (this.get('membersV2')) {
|
||||
return _.compact(
|
||||
this.get('membersV2').map(member => {
|
||||
const c = ConversationController.get(member.conversationId);
|
||||
|
||||
// In groups we won't sent to contacts we believe are unregistered
|
||||
if (c && c.isUnregistered()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return c;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (this.get('members')) {
|
||||
return _.compact(
|
||||
this.get('members').map(id => {
|
||||
const c = ConversationController.get(id);
|
||||
|
||||
// In groups we won't sent to contacts we believe are unregistered
|
||||
if (c && c.isUnregistered()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return c;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
window.log.warn(
|
||||
'getMembers: Group conversation had neither membersV2 nor members'
|
||||
);
|
||||
|
||||
return [];
|
||||
},
|
||||
|
||||
getMemberIds() {
|
||||
const members = this.getMembers();
|
||||
return members.map(member => member.id);
|
||||
},
|
||||
|
||||
getRecipients() {
|
||||
const members = this.getMembers();
|
||||
|
||||
// Eliminate our
|
||||
return _.compact(
|
||||
this.get('members').map(memberId => {
|
||||
const c = ConversationController.get(memberId);
|
||||
if (c.id === me) {
|
||||
return null;
|
||||
}
|
||||
// We don't want to even attempt a send if we have recently discovered that they
|
||||
// are unregistered.
|
||||
if (c.isUnregistered()) {
|
||||
return null;
|
||||
}
|
||||
return c.getSendTarget();
|
||||
})
|
||||
members.map(member => (member.isMe() ? null : member.getSendTarget()))
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -1549,11 +1645,11 @@
|
|||
if (this.isMe()) {
|
||||
const dataMessage = await textsecure.messaging.getMessageProto(
|
||||
destination,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null, // body
|
||||
null, // attachments
|
||||
null, // quote
|
||||
null, // preview
|
||||
null, // sticker
|
||||
outgoingReaction,
|
||||
timestamp,
|
||||
expireTimer,
|
||||
|
@ -1568,11 +1664,11 @@
|
|||
if (this.isPrivate()) {
|
||||
return textsecure.messaging.sendMessageToIdentifier(
|
||||
destination,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null, // body
|
||||
null, // attachments
|
||||
null, // quote
|
||||
null, // preview
|
||||
null, // sticker
|
||||
outgoingReaction,
|
||||
timestamp,
|
||||
expireTimer,
|
||||
|
@ -1582,17 +1678,14 @@
|
|||
}
|
||||
|
||||
return textsecure.messaging.sendMessageToGroup(
|
||||
this.get('groupId'),
|
||||
this.getRecipients(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
outgoingReaction,
|
||||
timestamp,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
{
|
||||
groupV1: this.getGroupV1Info(),
|
||||
groupV2: this.getGroupV2Info(),
|
||||
reaction: outgoingReaction,
|
||||
timestamp,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
},
|
||||
options
|
||||
);
|
||||
})();
|
||||
|
@ -1741,7 +1834,7 @@
|
|||
quote,
|
||||
preview,
|
||||
sticker,
|
||||
null,
|
||||
null, // reaction
|
||||
now,
|
||||
expireTimer,
|
||||
profileKey
|
||||
|
@ -1752,43 +1845,38 @@
|
|||
const conversationType = this.get('type');
|
||||
const options = this.getSendOptions();
|
||||
|
||||
const promise = (() => {
|
||||
switch (conversationType) {
|
||||
case Message.PRIVATE:
|
||||
return textsecure.messaging.sendMessageToIdentifier(
|
||||
destination,
|
||||
messageBody,
|
||||
finalAttachments,
|
||||
quote,
|
||||
preview,
|
||||
sticker,
|
||||
null,
|
||||
now,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
options
|
||||
);
|
||||
case Message.GROUP:
|
||||
return textsecure.messaging.sendMessageToGroup(
|
||||
this.get('groupId'),
|
||||
this.getRecipients(),
|
||||
messageBody,
|
||||
finalAttachments,
|
||||
quote,
|
||||
preview,
|
||||
sticker,
|
||||
null,
|
||||
now,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
options
|
||||
);
|
||||
default:
|
||||
throw new TypeError(
|
||||
`Invalid conversation type: '${conversationType}'`
|
||||
);
|
||||
}
|
||||
})();
|
||||
let promise;
|
||||
if (conversationType === Message.GROUP) {
|
||||
promise = textsecure.messaging.sendMessageToGroup(
|
||||
{
|
||||
attachments: finalAttachments,
|
||||
expireTimer,
|
||||
groupV1: this.getGroupV1Info(),
|
||||
groupV2: this.getGroupV2Info(),
|
||||
messageText: messageBody,
|
||||
preview,
|
||||
profileKey,
|
||||
quote,
|
||||
sticker,
|
||||
timestamp: now,
|
||||
},
|
||||
options
|
||||
);
|
||||
} else {
|
||||
promise = textsecure.messaging.sendMessageToIdentifier(
|
||||
destination,
|
||||
messageBody,
|
||||
finalAttachments,
|
||||
quote,
|
||||
preview,
|
||||
sticker,
|
||||
null, // reaction
|
||||
now,
|
||||
expireTimer,
|
||||
profileKey,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
return message.send(this.wrapSend(promise));
|
||||
});
|
||||
|
@ -2012,7 +2100,9 @@
|
|||
|
||||
const currentTimestamp = this.get('timestamp') || null;
|
||||
const timestamp = activityMessage
|
||||
? activityMessage.get('sent_at') || currentTimestamp
|
||||
? activityMessage.get('sent_at') ||
|
||||
activityMessage.get('received_at') ||
|
||||
currentTimestamp
|
||||
: currentTimestamp;
|
||||
|
||||
this.set({
|
||||
|
@ -2043,12 +2133,90 @@
|
|||
}
|
||||
},
|
||||
|
||||
async updateExpirationTimerInGroupV2(seconds) {
|
||||
// Make change on the server
|
||||
const actions = window.Signal.Groups.buildDisappearingMessagesTimerChange(
|
||||
{
|
||||
expireTimer: seconds || 0,
|
||||
group: this.attributes,
|
||||
}
|
||||
);
|
||||
let signedGroupChange;
|
||||
try {
|
||||
signedGroupChange = await window.Signal.Groups.uploadGroupChange({
|
||||
actions,
|
||||
group: this.attributes,
|
||||
serverPublicParamsBase64: window.getServerPublicParams(),
|
||||
});
|
||||
} catch (error) {
|
||||
// Get latest GroupV2 data, since we ran into trouble updating it
|
||||
this.fetchLatestGroupV2Data();
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Update local conversation
|
||||
this.set({
|
||||
expireTimer: seconds || 0,
|
||||
revision: actions.version,
|
||||
});
|
||||
window.Signal.Data.updateConversation(this.attributes);
|
||||
|
||||
// Create local notification
|
||||
const timestamp = Date.now();
|
||||
const id = window.getGuid();
|
||||
const message = MessageController.register(
|
||||
id,
|
||||
new Whisper.Message({
|
||||
id,
|
||||
conversationId: this.id,
|
||||
sent_at: timestamp,
|
||||
received_at: timestamp,
|
||||
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
expirationTimerUpdate: {
|
||||
expireTimer: seconds,
|
||||
sourceUuid: this.ourUuid,
|
||||
},
|
||||
})
|
||||
);
|
||||
await window.Signal.Data.saveMessage(message.attributes, {
|
||||
Message: Whisper.Message,
|
||||
forceSave: true,
|
||||
});
|
||||
this.trigger('newmessage', message);
|
||||
|
||||
// Send message to all group members
|
||||
const profileKey = this.get('profileSharing')
|
||||
? storage.get('profileKey')
|
||||
: undefined;
|
||||
const sendOptions = this.getSendOptions();
|
||||
const promise = textsecure.messaging.sendMessageToGroup(
|
||||
{
|
||||
groupV2: this.getGroupV2Info(signedGroupChange.toArrayBuffer()),
|
||||
timestamp,
|
||||
profileKey,
|
||||
},
|
||||
sendOptions
|
||||
);
|
||||
|
||||
message.send(promise);
|
||||
},
|
||||
|
||||
async updateExpirationTimer(
|
||||
providedExpireTimer,
|
||||
providedSource,
|
||||
receivedAt,
|
||||
options = {}
|
||||
) {
|
||||
if (this.get('groupVersion') === 2) {
|
||||
if (providedSource || receivedAt) {
|
||||
throw new Error(
|
||||
'updateExpirationTimer: GroupV2 timers are not updated this way'
|
||||
);
|
||||
}
|
||||
await this.updateExpirationTimerInGroupV2(providedExpireTimer);
|
||||
return false;
|
||||
}
|
||||
|
||||
let expireTimer = providedExpireTimer;
|
||||
let source = providedSource;
|
||||
if (this.get('left')) {
|
||||
|
@ -2131,12 +2299,12 @@
|
|||
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
||||
const dataMessage = await textsecure.messaging.getMessageProto(
|
||||
this.getSendTarget(),
|
||||
null,
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
null, // body
|
||||
[], // attachments
|
||||
null, // quote
|
||||
[], // preview
|
||||
null, // sticker
|
||||
null, // reaction
|
||||
message.get('sent_at'),
|
||||
expireTimer,
|
||||
profileKey,
|
||||
|
@ -2250,79 +2418,6 @@
|
|||
}
|
||||
},
|
||||
|
||||
async updateGroup(providedGroupUpdate) {
|
||||
let groupUpdate = providedGroupUpdate;
|
||||
|
||||
if (this.isPrivate()) {
|
||||
throw new Error('Called update group on private conversation');
|
||||
}
|
||||
if (groupUpdate === undefined) {
|
||||
groupUpdate = this.pick(['name', 'avatar', 'members']);
|
||||
}
|
||||
const now = Date.now();
|
||||
const model = new Whisper.Message({
|
||||
conversationId: this.id,
|
||||
type: 'outgoing',
|
||||
sent_at: now,
|
||||
received_at: now,
|
||||
group_update: groupUpdate,
|
||||
});
|
||||
|
||||
const id = await window.Signal.Data.saveMessage(model.attributes, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
|
||||
model.set({ id });
|
||||
|
||||
const message = MessageController.register(model.id, model);
|
||||
this.addSingleMessage(message);
|
||||
|
||||
const options = this.getSendOptions();
|
||||
message.send(
|
||||
this.wrapSend(
|
||||
textsecure.messaging.updateGroup(
|
||||
this.id,
|
||||
this.get('name'),
|
||||
this.get('avatar'),
|
||||
this.get('members'),
|
||||
options
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async leaveGroup() {
|
||||
const now = Date.now();
|
||||
if (this.get('type') === 'group') {
|
||||
const groupIdentifiers = this.getRecipients();
|
||||
this.set({ left: true });
|
||||
window.Signal.Data.updateConversation(this.attributes);
|
||||
|
||||
const model = new Whisper.Message({
|
||||
group_update: { left: 'You' },
|
||||
conversationId: this.id,
|
||||
type: 'outgoing',
|
||||
sent_at: now,
|
||||
received_at: now,
|
||||
});
|
||||
|
||||
const id = await window.Signal.Data.saveMessage(model.attributes, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
model.set({ id });
|
||||
|
||||
const message = MessageController.register(model.id, model);
|
||||
this.addSingleMessage(message);
|
||||
|
||||
const options = this.getSendOptions();
|
||||
message.send(
|
||||
this.wrapSend(
|
||||
textsecure.messaging.leaveGroup(this.id, groupIdentifiers, options)
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async markRead(newestUnreadDate, providedOptions) {
|
||||
const options = providedOptions || {};
|
||||
_.defaults(options, { sendReadReceipts: true });
|
||||
|
@ -2444,14 +2539,7 @@
|
|||
|
||||
getProfiles() {
|
||||
// request all conversation members' keys
|
||||
let conversations = [];
|
||||
if (this.isPrivate()) {
|
||||
conversations = [this];
|
||||
} else {
|
||||
conversations = this.get('members')
|
||||
.map(id => ConversationController.get(id))
|
||||
.filter(Boolean);
|
||||
}
|
||||
const conversations = this.getMembers();
|
||||
return Promise.all(
|
||||
_.map(conversations, conversation => {
|
||||
this.getProfile(conversation.get('uuid'), conversation.get('e164'));
|
||||
|
@ -2822,30 +2910,21 @@
|
|||
},
|
||||
|
||||
hasMember(identifier) {
|
||||
const cid = ConversationController.getConversationId(identifier);
|
||||
return cid && _.contains(this.get('members'), cid);
|
||||
const id = ConversationController.getConversationId(identifier);
|
||||
const memberIds = this.getMemberIds();
|
||||
|
||||
return _.contains(memberIds, id);
|
||||
},
|
||||
fetchContacts() {
|
||||
if (this.isPrivate()) {
|
||||
this.contactCollection.reset([this]);
|
||||
return Promise.resolve();
|
||||
}
|
||||
const members = this.get('members') || [];
|
||||
const promises = members.map(identifier =>
|
||||
ConversationController.getOrCreateAndWait(identifier, 'private')
|
||||
);
|
||||
|
||||
return Promise.all(promises).then(contacts => {
|
||||
_.forEach(contacts, contact => {
|
||||
this.listenTo(
|
||||
contact,
|
||||
'change:verified',
|
||||
this.onMemberVerifiedChange
|
||||
);
|
||||
});
|
||||
|
||||
this.contactCollection.reset(contacts);
|
||||
const members = this.getMembers();
|
||||
_.forEach(members, member => {
|
||||
this.listenTo(member, 'change:verified', this.onMemberVerifiedChange);
|
||||
});
|
||||
|
||||
this.contactCollection.reset(members);
|
||||
},
|
||||
|
||||
async destroyMessages() {
|
||||
|
@ -2946,6 +3025,43 @@
|
|||
|
||||
return null;
|
||||
},
|
||||
canChangeTimer() {
|
||||
if (this.isPrivate()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.get('groupVersion') !== 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const accessControlEnum =
|
||||
textsecure.protobuf.AccessControl.AccessRequired;
|
||||
const accessControl = this.get('accessControl');
|
||||
const canAnyoneChangeTimer =
|
||||
accessControl &&
|
||||
(accessControl.attributes === accessControlEnum.ANY ||
|
||||
accessControl.attributes === accessControlEnum.MEMBER);
|
||||
if (canAnyoneChangeTimer) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const memberEnum = textsecure.protobuf.Member.Role;
|
||||
const members = this.get('membersV2') || [];
|
||||
const myId = ConversationController.getConversationId(
|
||||
textsecure.storage.user.getUuid() || textsecure.storage.user.getNumber()
|
||||
);
|
||||
const me = members.find(item => item.conversationId === myId);
|
||||
if (!me) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isAdministrator = me.role === memberEnum.ADMINISTRATOR;
|
||||
if (isAdministrator) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// Set of items to captureChanges on:
|
||||
// [-] uuid
|
||||
|
@ -3184,4 +3300,71 @@
|
|||
});
|
||||
|
||||
Whisper.Conversation.COLORS = COLORS.concat(['grey', 'default']).join(' ');
|
||||
|
||||
// This is a wrapper model used to display group members in the member list view, within
|
||||
// the world of backbone, but layering another bit of group-specific data top of base
|
||||
// conversation data.
|
||||
Whisper.GroupMemberConversation = Backbone.Model.extend({
|
||||
initialize(attributes) {
|
||||
const { conversation, isAdmin } = attributes;
|
||||
|
||||
if (!conversation) {
|
||||
throw new Error(
|
||||
'GroupMemberConversation.initialze: conversation required!'
|
||||
);
|
||||
}
|
||||
if (!_.isBoolean(isAdmin)) {
|
||||
throw new Error('GroupMemberConversation.initialze: isAdmin required!');
|
||||
}
|
||||
|
||||
// If our underlying conversation changes, we change too
|
||||
this.listenTo(conversation, 'change', () => {
|
||||
this.trigger('change', this);
|
||||
});
|
||||
|
||||
this.conversation = conversation;
|
||||
this.isAdmin = isAdmin;
|
||||
},
|
||||
|
||||
format() {
|
||||
return {
|
||||
...this.conversation.format(),
|
||||
isAdmin: this.isAdmin,
|
||||
};
|
||||
},
|
||||
|
||||
get(...params) {
|
||||
return this.conversation.get(...params);
|
||||
},
|
||||
|
||||
getTitle() {
|
||||
return this.conversation.getTitle();
|
||||
},
|
||||
|
||||
isMe() {
|
||||
return this.conversation.isMe();
|
||||
},
|
||||
});
|
||||
|
||||
// We need a custom collection here to get the sorting we need
|
||||
Whisper.GroupConversationCollection = Backbone.Collection.extend({
|
||||
model: Whisper.GroupMemberConversation,
|
||||
|
||||
initialize() {
|
||||
this.collator = new Intl.Collator();
|
||||
},
|
||||
|
||||
comparator(left, right) {
|
||||
if (left.isAdmin && !right.isAdmin) {
|
||||
return -1;
|
||||
}
|
||||
if (!left.isAdmin && right.isAdmin) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const leftLower = left.getTitle().toLowerCase();
|
||||
const rightLower = right.getTitle().toLowerCase();
|
||||
return this.collator.compare(leftLower, rightLower);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -45,6 +45,9 @@
|
|||
|
||||
const { addStickerPackReference, getMessageBySender } = window.Signal.Data;
|
||||
const { bytesFromString } = window.Signal.Crypto;
|
||||
const PLACEHOLDER_CONTACT = {
|
||||
title: i18n('unknownContact'),
|
||||
};
|
||||
|
||||
window.AccountCache = Object.create(null);
|
||||
window.AccountJobs = Object.create(null);
|
||||
|
@ -140,6 +143,7 @@
|
|||
!this.isEndSession() &&
|
||||
!this.isExpirationTimerUpdate() &&
|
||||
!this.isGroupUpdate() &&
|
||||
!this.isGroupV2Change() &&
|
||||
!this.isKeyChange() &&
|
||||
!this.isMessageHistoryUnsynced() &&
|
||||
!this.isProfileChange() &&
|
||||
|
@ -156,6 +160,12 @@
|
|||
data: this.getPropsForUnsupportedMessage(),
|
||||
};
|
||||
}
|
||||
if (this.isGroupV2Change()) {
|
||||
return {
|
||||
type: 'groupV2Change',
|
||||
data: this.getPropsForGroupV2Change(),
|
||||
};
|
||||
}
|
||||
if (this.isMessageHistoryUnsynced()) {
|
||||
return {
|
||||
type: 'linkNotification',
|
||||
|
@ -213,24 +223,13 @@
|
|||
|
||||
// Other top-level prop-generation
|
||||
getPropsForSearchResult() {
|
||||
const ourId = ConversationController.getOurConversationId();
|
||||
const sourceId = this.getContactId();
|
||||
const fromContact = this.findAndFormatContact(sourceId);
|
||||
|
||||
if (ourId === sourceId) {
|
||||
fromContact.isMe = true;
|
||||
}
|
||||
|
||||
const from = this.findAndFormatContact(sourceId);
|
||||
const convo = this.getConversation();
|
||||
|
||||
const to = convo ? this.findAndFormatContact(convo.get('id')) : {};
|
||||
|
||||
if (to && convo && convo.isMe()) {
|
||||
to.isMe = true;
|
||||
}
|
||||
const to = this.findAndFormatContact(convo.get('id'));
|
||||
|
||||
return {
|
||||
from: fromContact || {},
|
||||
from,
|
||||
to,
|
||||
|
||||
isSelected: this.isSelected,
|
||||
|
@ -358,6 +357,9 @@
|
|||
versionAtReceive < requiredVersion
|
||||
);
|
||||
},
|
||||
isGroupV2Change() {
|
||||
return Boolean(this.get('groupV2Change'));
|
||||
},
|
||||
isExpirationTimerUpdate() {
|
||||
const flag =
|
||||
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
||||
|
@ -399,6 +401,16 @@
|
|||
contact: this.findAndFormatContact(sourceId),
|
||||
};
|
||||
},
|
||||
getPropsForGroupV2Change() {
|
||||
const { protobuf } = window.textsecure;
|
||||
|
||||
return {
|
||||
AccessControlEnum: protobuf.AccessControl.AccessRequired,
|
||||
RoleEnum: protobuf.Member.Role,
|
||||
ourConversationId: window.ConversationController.getOurConversationId(),
|
||||
change: this.get('groupV2Change'),
|
||||
};
|
||||
},
|
||||
getPropsForTimerNotification() {
|
||||
const timerUpdate = this.get('expirationTimerUpdate');
|
||||
if (!timerUpdate) {
|
||||
|
@ -414,9 +426,10 @@
|
|||
uuid: sourceUuid,
|
||||
});
|
||||
const ourId = ConversationController.getOurConversationId();
|
||||
const formattedContact = this.findAndFormatContact(sourceId);
|
||||
|
||||
const basicProps = {
|
||||
...this.findAndFormatContact(sourceId),
|
||||
...formattedContact,
|
||||
type: 'fromOther',
|
||||
timespan,
|
||||
disabled,
|
||||
|
@ -434,6 +447,12 @@
|
|||
type: 'fromMe',
|
||||
};
|
||||
}
|
||||
if (!sourceId) {
|
||||
return {
|
||||
...basicProps,
|
||||
type: 'fromMember',
|
||||
};
|
||||
}
|
||||
|
||||
return basicProps;
|
||||
},
|
||||
|
@ -473,10 +492,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
const placeholderContact = {
|
||||
title: i18n('unknownContact'),
|
||||
};
|
||||
|
||||
if (groupUpdate.joined) {
|
||||
changes.push({
|
||||
type: 'add',
|
||||
|
@ -484,8 +499,7 @@
|
|||
Array.isArray(groupUpdate.joined)
|
||||
? groupUpdate.joined
|
||||
: [groupUpdate.joined],
|
||||
identifier =>
|
||||
this.findAndFormatContact(identifier) || placeholderContact
|
||||
identifier => this.findAndFormatContact(identifier)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -502,8 +516,7 @@
|
|||
Array.isArray(groupUpdate.left)
|
||||
? groupUpdate.left
|
||||
: [groupUpdate.left],
|
||||
identifier =>
|
||||
this.findAndFormatContact(identifier) || placeholderContact
|
||||
identifier => this.findAndFormatContact(identifier)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -600,15 +613,6 @@
|
|||
const reactions = (this.get('reactions') || []).map(re => {
|
||||
const c = this.findAndFormatContact(re.fromId);
|
||||
|
||||
if (!c) {
|
||||
return {
|
||||
emoji: re.emoji,
|
||||
from: {
|
||||
id: re.fromId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
emoji: re.emoji,
|
||||
timestamp: re.timestamp,
|
||||
|
@ -661,17 +665,29 @@
|
|||
|
||||
// Dependencies of prop-generation functions
|
||||
findAndFormatContact(identifier) {
|
||||
if (!identifier) {
|
||||
return PLACEHOLDER_CONTACT;
|
||||
}
|
||||
|
||||
const contactModel = this.findContact(identifier);
|
||||
if (contactModel) {
|
||||
return contactModel.format();
|
||||
}
|
||||
|
||||
const { format } = PhoneNumber;
|
||||
const { format, isValidNumber } = PhoneNumber;
|
||||
const regionCode = storage.get('regionCode');
|
||||
|
||||
if (!isValidNumber(identifier, { regionCode })) {
|
||||
return PLACEHOLDER_CONTACT;
|
||||
}
|
||||
|
||||
const phoneNumber = format(identifier, {
|
||||
ourRegionCode: regionCode,
|
||||
});
|
||||
|
||||
return {
|
||||
phoneNumber: format(identifier, {
|
||||
ourRegionCode: regionCode,
|
||||
}),
|
||||
title: phoneNumber,
|
||||
phoneNumber,
|
||||
};
|
||||
},
|
||||
findContact(identifier) {
|
||||
|
@ -910,6 +926,29 @@
|
|||
};
|
||||
}
|
||||
|
||||
if (this.isGroupV2Change()) {
|
||||
const { protobuf } = window.textsecure;
|
||||
const change = this.get('groupV2Change');
|
||||
|
||||
const lines = window.Signal.GroupChange.renderChange(change, {
|
||||
AccessControlEnum: protobuf.AccessControl.AccessRequired,
|
||||
i18n: window.i18n,
|
||||
ourConversationId: window.ConversationController.getOurConversationId(),
|
||||
renderContact: conversationId => {
|
||||
const conversation = window.ConversationController.get(
|
||||
conversationId
|
||||
);
|
||||
return conversation
|
||||
? conversation.getTitle()
|
||||
: window.i18n('unknownUser');
|
||||
},
|
||||
renderString: (key, i18n, placeholders) => i18n(key, placeholders),
|
||||
RoleEnum: protobuf.Member.Role,
|
||||
});
|
||||
|
||||
return { text: lines.join(' ') };
|
||||
}
|
||||
|
||||
const attachments = this.get('attachments') || [];
|
||||
|
||||
if (this.isTapToView()) {
|
||||
|
@ -1315,6 +1354,7 @@
|
|||
// Rendered sync messages
|
||||
const isCallHistory = this.isCallHistory();
|
||||
const isGroupUpdate = this.isGroupUpdate();
|
||||
const isGroupV2Change = this.isGroupV2Change();
|
||||
const isEndSession = this.isEndSession();
|
||||
const isExpirationTimerUpdate = this.isExpirationTimerUpdate();
|
||||
const isVerifiedChange = this.isVerifiedChange();
|
||||
|
@ -1342,6 +1382,7 @@
|
|||
// Rendered sync messages
|
||||
isCallHistory ||
|
||||
isGroupUpdate ||
|
||||
isGroupV2Change ||
|
||||
isEndSession ||
|
||||
isExpirationTimerUpdate ||
|
||||
isVerifiedChange ||
|
||||
|
@ -1634,6 +1675,8 @@
|
|||
// Because this is a partial group send, we manually construct the request like
|
||||
// sendMessageToGroup does.
|
||||
|
||||
const groupV2 = conversation.getGroupV2Info();
|
||||
|
||||
promise = textsecure.messaging.sendMessage(
|
||||
{
|
||||
recipients,
|
||||
|
@ -1645,10 +1688,13 @@
|
|||
sticker: stickerWithData,
|
||||
expireTimer: this.get('expireTimer'),
|
||||
profileKey,
|
||||
group: {
|
||||
id: this.getConversation().get('groupId'),
|
||||
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||
},
|
||||
groupV2,
|
||||
group: groupV2
|
||||
? null
|
||||
: {
|
||||
id: this.getConversation().get('groupId'),
|
||||
type: textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||
},
|
||||
},
|
||||
options
|
||||
);
|
||||
|
@ -2392,19 +2438,79 @@
|
|||
}
|
||||
}
|
||||
|
||||
// We drop incoming messages for groups we already know about, which we're not a
|
||||
// part of, except for group updates.
|
||||
const ourUuid = textsecure.storage.user.getUuid();
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const isGroupUpdate =
|
||||
const existingRevision = conversation.get('revision');
|
||||
const isGroupV2 = Boolean(initialMessage.groupV2);
|
||||
const isV2GroupUpdate =
|
||||
initialMessage.groupV2 &&
|
||||
(!existingRevision ||
|
||||
initialMessage.groupV2.revision > existingRevision);
|
||||
|
||||
// GroupV2
|
||||
if (isGroupV2) {
|
||||
conversation.maybeRepairGroupV2(
|
||||
_.pick(initialMessage.groupV2, [
|
||||
'masterKey',
|
||||
'secretParams',
|
||||
'publicParams',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (isV2GroupUpdate) {
|
||||
const { revision, groupChange } = initialMessage.groupV2;
|
||||
try {
|
||||
await window.Signal.Groups.maybeUpdateGroup({
|
||||
conversation,
|
||||
groupChangeBase64: groupChange,
|
||||
newRevision: revision,
|
||||
timestamp: message.get('received_at'),
|
||||
});
|
||||
} catch (error) {
|
||||
const errorText = error && error.stack ? error.stack : error;
|
||||
window.log.error(
|
||||
`handleDataMessage: Failed to process group update for ${conversation.idForLogging()} as part of message ${message.idForLogging()}: ${errorText}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const ourConversationId = ConversationController.getOurConversationId();
|
||||
const senderId = ConversationController.ensureContactIds({
|
||||
e164: source,
|
||||
uuid: sourceUuid,
|
||||
});
|
||||
const isV1GroupUpdate =
|
||||
initialMessage.group &&
|
||||
initialMessage.group.type !==
|
||||
textsecure.protobuf.GroupContext.Type.DELIVER;
|
||||
|
||||
// Drop an incoming GroupV2 message if we or the sender are not part of the group
|
||||
// after applying the message's associated group chnages.
|
||||
if (
|
||||
type === 'incoming' &&
|
||||
!conversation.isPrivate() &&
|
||||
!conversation.hasMember(ourNumber || ourUuid) &&
|
||||
!isGroupUpdate
|
||||
isGroupV2 &&
|
||||
(conversation.get('left') ||
|
||||
!conversation.hasMember(ourConversationId) ||
|
||||
!conversation.hasMember(senderId))
|
||||
) {
|
||||
window.log.warn(
|
||||
`Received message destined for group ${conversation.idForLogging()}, which we or the sender are not a part of. Dropping.`
|
||||
);
|
||||
confirm();
|
||||
return;
|
||||
}
|
||||
|
||||
// We drop incoming messages for v1 groups we already know about, which we're not
|
||||
// a part of, except for group updates. Because group v1 updates haven't been
|
||||
// applied by this point.
|
||||
if (
|
||||
type === 'incoming' &&
|
||||
!conversation.isPrivate() &&
|
||||
!isGroupV2 &&
|
||||
!isV1GroupUpdate &&
|
||||
(conversation.get('left') ||
|
||||
!conversation.hasMember(ourConversationId))
|
||||
) {
|
||||
window.log.warn(
|
||||
`Received message destined for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`
|
||||
|
@ -2488,7 +2594,9 @@
|
|||
let attributes = {
|
||||
...conversation.attributes,
|
||||
};
|
||||
if (dataMessage.group) {
|
||||
|
||||
// GroupV1
|
||||
if (!isGroupV2 && dataMessage.group) {
|
||||
const pendingGroupUpdate = [];
|
||||
const memberConversations = await Promise.all(
|
||||
dataMessage.group.membersE164.map(e164 =>
|
||||
|
@ -2597,10 +2705,6 @@
|
|||
conversation.set({ addedBy: message.getContactId() });
|
||||
}
|
||||
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
|
||||
const senderId = ConversationController.ensureContactIds({
|
||||
e164: source,
|
||||
uuid: sourceUuid,
|
||||
});
|
||||
const sender = ConversationController.get(senderId);
|
||||
const inGroup = Boolean(
|
||||
sender &&
|
||||
|
@ -2638,6 +2742,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Drop empty messages after. This needs to happen after the initial
|
||||
// message.set call and after GroupV1 processing to make sure all possible
|
||||
// properties are set before we determine that a message is empty.
|
||||
if (message.isEmpty()) {
|
||||
window.log.info(
|
||||
`handleDataMessage: Dropping empty message ${message.idForLogging()} in conversation ${conversation.idForLogging()}`
|
||||
);
|
||||
confirm();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'outgoing') {
|
||||
const receipts = Whisper.DeliveryReceipts.forMessage(
|
||||
conversation,
|
||||
|
@ -2652,61 +2767,66 @@
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
attributes.active_at = now;
|
||||
conversation.set(attributes);
|
||||
|
||||
if (message.isExpirationTimerUpdate()) {
|
||||
message.set({
|
||||
expirationTimerUpdate: {
|
||||
source,
|
||||
sourceUuid,
|
||||
expireTimer: dataMessage.expireTimer,
|
||||
},
|
||||
});
|
||||
conversation.set({ expireTimer: dataMessage.expireTimer });
|
||||
} else if (dataMessage.expireTimer) {
|
||||
if (dataMessage.expireTimer) {
|
||||
message.set({ expireTimer: dataMessage.expireTimer });
|
||||
}
|
||||
|
||||
// NOTE: Remove once the above uses
|
||||
// `Conversation::updateExpirationTimer`:
|
||||
const { expireTimer } = dataMessage;
|
||||
const shouldLogExpireTimerChange =
|
||||
message.isExpirationTimerUpdate() || expireTimer;
|
||||
if (shouldLogExpireTimerChange) {
|
||||
window.log.info("Update conversation 'expireTimer'", {
|
||||
id: conversation.idForLogging(),
|
||||
expireTimer,
|
||||
source: 'handleDataMessage',
|
||||
});
|
||||
}
|
||||
if (!isGroupV2) {
|
||||
if (message.isExpirationTimerUpdate()) {
|
||||
message.set({
|
||||
expirationTimerUpdate: {
|
||||
source,
|
||||
sourceUuid,
|
||||
expireTimer: dataMessage.expireTimer,
|
||||
},
|
||||
});
|
||||
conversation.set({ expireTimer: dataMessage.expireTimer });
|
||||
}
|
||||
|
||||
if (!message.isEndSession()) {
|
||||
if (dataMessage.expireTimer) {
|
||||
if (
|
||||
dataMessage.expireTimer !== conversation.get('expireTimer')
|
||||
// NOTE: Remove once the above calls this.model.updateExpirationTimer()
|
||||
const { expireTimer } = dataMessage;
|
||||
const shouldLogExpireTimerChange =
|
||||
message.isExpirationTimerUpdate() || expireTimer;
|
||||
if (shouldLogExpireTimerChange) {
|
||||
window.log.info("Update conversation 'expireTimer'", {
|
||||
id: conversation.idForLogging(),
|
||||
expireTimer,
|
||||
source: 'handleDataMessage',
|
||||
});
|
||||
}
|
||||
|
||||
if (!message.isEndSession()) {
|
||||
if (dataMessage.expireTimer) {
|
||||
if (
|
||||
dataMessage.expireTimer !== conversation.get('expireTimer')
|
||||
) {
|
||||
conversation.updateExpirationTimer(
|
||||
dataMessage.expireTimer,
|
||||
source,
|
||||
message.get('received_at'),
|
||||
{
|
||||
fromGroupUpdate: message.isGroupUpdate(),
|
||||
}
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
conversation.get('expireTimer') &&
|
||||
// We only turn off timers if it's not a group update
|
||||
!message.isGroupUpdate()
|
||||
) {
|
||||
conversation.updateExpirationTimer(
|
||||
dataMessage.expireTimer,
|
||||
null,
|
||||
source,
|
||||
message.get('received_at'),
|
||||
{
|
||||
fromGroupUpdate: message.isGroupUpdate(),
|
||||
}
|
||||
message.get('received_at')
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
conversation.get('expireTimer') &&
|
||||
// We only turn off timers if it's not a group update
|
||||
!message.isGroupUpdate()
|
||||
) {
|
||||
conversation.updateExpirationTimer(
|
||||
null,
|
||||
source,
|
||||
message.get('received_at')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'incoming') {
|
||||
const readSync = Whisper.ReadSyncs.forMessage(message);
|
||||
if (readSync) {
|
||||
|
@ -2804,17 +2924,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Drop empty messages. This needs to happen after the initial
|
||||
// message.set call to make sure all possible properties are set
|
||||
// before we determine that a message is empty.
|
||||
if (message.isEmpty()) {
|
||||
window.log.info(
|
||||
`Dropping empty datamessage ${message.idForLogging()} in conversation ${conversation.idForLogging()}`
|
||||
);
|
||||
confirm();
|
||||
return;
|
||||
}
|
||||
|
||||
const conversationTimestamp = conversation.get('timestamp');
|
||||
if (
|
||||
!conversationTimestamp ||
|
||||
|
|
|
@ -10,6 +10,7 @@ const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..');
|
|||
const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g;
|
||||
const UUID_PATTERN = /[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{9}([0-9A-F]{3})/gi;
|
||||
const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g;
|
||||
const GROUP_V2_ID_PATTERN = /(groupv2\()([^=)]+)(=?=?\))/g;
|
||||
const REDACTION_PLACEHOLDER = '[REDACTED]';
|
||||
|
||||
// _redactPath :: Path -> String -> String
|
||||
|
@ -80,11 +81,21 @@ exports.redactGroupIds = text => {
|
|||
throw new TypeError("'text' must be a string");
|
||||
}
|
||||
|
||||
return text.replace(
|
||||
GROUP_ID_PATTERN,
|
||||
(match, before, id, after) =>
|
||||
`${before}${REDACTION_PLACEHOLDER}${removeNewlines(id).slice(-3)}${after}`
|
||||
);
|
||||
return text
|
||||
.replace(
|
||||
GROUP_ID_PATTERN,
|
||||
(match, before, id, after) =>
|
||||
`${before}${REDACTION_PLACEHOLDER}${removeNewlines(id).slice(
|
||||
-3
|
||||
)}${after}`
|
||||
)
|
||||
.replace(
|
||||
GROUP_V2_ID_PATTERN,
|
||||
(match, before, id, after) =>
|
||||
`${before}${REDACTION_PLACEHOLDER}${removeNewlines(id).slice(
|
||||
-3
|
||||
)}${after}`
|
||||
);
|
||||
};
|
||||
|
||||
// redactSensitivePaths :: String -> String
|
||||
|
|
|
@ -9,6 +9,8 @@ const {
|
|||
const Data = require('../../ts/sql/Client').default;
|
||||
const Emojis = require('./emojis');
|
||||
const EmojiLib = require('../../ts/components/emoji/lib');
|
||||
const Groups = require('../../ts/groups');
|
||||
const GroupChange = require('../../ts/groupChange');
|
||||
const IndexedDB = require('./indexeddb');
|
||||
const Notifications = require('../../ts/notifications');
|
||||
const OS = require('../../ts/OS');
|
||||
|
@ -108,6 +110,9 @@ const { IdleDetector } = require('./idle_detector');
|
|||
const MessageDataMigrator = require('./messages_data_migrator');
|
||||
|
||||
// Processes / Services
|
||||
const {
|
||||
initializeGroupCredentialFetcher,
|
||||
} = require('../../ts/services/groupCredentialFetcher');
|
||||
const {
|
||||
initializeNetworkObserver,
|
||||
} = require('../../ts/services/networkObserver');
|
||||
|
@ -333,6 +338,7 @@ exports.setup = (options = {}) => {
|
|||
calling,
|
||||
eraseAllStorageServiceState,
|
||||
handleUnknownRecords,
|
||||
initializeGroupCredentialFetcher,
|
||||
initializeNetworkObserver,
|
||||
initializeUpdateListener,
|
||||
notify,
|
||||
|
@ -378,6 +384,8 @@ exports.setup = (options = {}) => {
|
|||
Data,
|
||||
Emojis,
|
||||
EmojiLib,
|
||||
Groups,
|
||||
GroupChange,
|
||||
IndexedDB,
|
||||
LinkPreviews,
|
||||
Metadata,
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
/* global crypto, window */
|
||||
/* global window */
|
||||
|
||||
const { isFunction, isNumber } = require('lodash');
|
||||
const {
|
||||
arrayBufferToBase64,
|
||||
base64ToArrayBuffer,
|
||||
computeHash,
|
||||
} = require('../../../ts/Crypto');
|
||||
|
||||
async function computeHash(arraybuffer) {
|
||||
const hash = await crypto.subtle.digest({ name: 'SHA-512' }, arraybuffer);
|
||||
return arrayBufferToBase64(hash);
|
||||
}
|
||||
|
||||
function buildAvatarUpdater({ field }) {
|
||||
return async (conversation, data, options = {}) => {
|
||||
if (!conversation) {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
if (conversation.isPrivate()) {
|
||||
ids = [conversation.id];
|
||||
} else {
|
||||
ids = conversation.get('members');
|
||||
ids = conversation.getMemberIds();
|
||||
}
|
||||
const receipts = this.filter(
|
||||
receipt =>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
className: 'contact-wrapper',
|
||||
Component: window.Signal.Components.ContactListItem,
|
||||
props: {
|
||||
...this.model.cachedProps,
|
||||
...this.model.format(),
|
||||
onClick: this.showIdentity.bind(this),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -215,6 +215,9 @@
|
|||
Whisper.MaxAttachmentsToast = Whisper.ToastView.extend({
|
||||
template: i18n('maximumAttachments'),
|
||||
});
|
||||
Whisper.TimerConflictToast = Whisper.ToastView.extend({
|
||||
template: i18n('GroupV2--timerConflict'),
|
||||
});
|
||||
|
||||
Whisper.ConversationLoadingScreen = Whisper.View.extend({
|
||||
templateName: 'conversation-loading-screen',
|
||||
|
@ -311,6 +314,13 @@
|
|||
this.model.updateSharedGroups.bind(this.model),
|
||||
FIVE_MINUTES
|
||||
);
|
||||
this.model.throttledFetchLatestGroupV2Data =
|
||||
this.model.throttledFetchLatestGroupV2Data ||
|
||||
_.throttle(
|
||||
this.model.fetchLatestGroupV2Data.bind(this.model),
|
||||
FIVE_MINUTES
|
||||
);
|
||||
|
||||
this.debouncedMaybeGrabLinkPreview = _.debounce(
|
||||
this.maybeGrabLinkPreview.bind(this),
|
||||
200
|
||||
|
@ -385,8 +395,13 @@
|
|||
|
||||
leftGroup: this.model.get('left'),
|
||||
|
||||
expirationSettingName,
|
||||
disableTimerChanges:
|
||||
this.model.get('left') ||
|
||||
!this.model.getAccepted() ||
|
||||
!this.model.canChangeTimer(),
|
||||
showBackButton: Boolean(this.panels && this.panels.length),
|
||||
|
||||
expirationSettingName,
|
||||
timerOptions: Whisper.ExpirationTimerOptions.map(item => ({
|
||||
name: item.getName(),
|
||||
value: item.get('seconds'),
|
||||
|
@ -1826,6 +1841,8 @@
|
|||
this.setQuoteMessage(quotedMessageId);
|
||||
}
|
||||
|
||||
this.model.throttledFetchLatestGroupV2Data();
|
||||
|
||||
const statusPromise = this.model.throttledGetProfiles();
|
||||
// eslint-disable-next-line more/no-then
|
||||
this.statusFetch = statusPromise.then(() =>
|
||||
|
@ -2044,7 +2061,18 @@
|
|||
async showMembers(e, providedMembers, options = {}) {
|
||||
_.defaults(options, { needVerify: false });
|
||||
|
||||
const model = providedMembers || this.model.contactCollection;
|
||||
let model = providedMembers || this.model.contactCollection;
|
||||
|
||||
if (!providedMembers && this.model.get('groupVersion') === 2) {
|
||||
model = new Whisper.GroupConversationCollection(
|
||||
this.model.get('membersV2').map(({ conversationId, role }) => ({
|
||||
conversation: ConversationController.get(conversationId),
|
||||
isAdmin:
|
||||
role === window.textsecure.protobuf.Member.Role.ADMINISTRATOR,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const view = new Whisper.GroupMemberList({
|
||||
model,
|
||||
// we pass this in to allow nested panels
|
||||
|
@ -2496,11 +2524,17 @@
|
|||
this.model.endSession();
|
||||
},
|
||||
|
||||
setDisappearingMessages(seconds) {
|
||||
if (seconds > 0) {
|
||||
this.model.updateExpirationTimer(seconds);
|
||||
} else {
|
||||
this.model.updateExpirationTimer(null);
|
||||
async setDisappearingMessages(seconds) {
|
||||
try {
|
||||
if (seconds > 0) {
|
||||
await this.model.updateExpirationTimer(seconds);
|
||||
} else {
|
||||
await this.model.updateExpirationTimer(null);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 409) {
|
||||
this.showToast(Whisper.TimerConflictToast);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue