move ConversationController to typescript

This commit is contained in:
Audric Ackermann 2021-01-06 14:05:13 +11:00
parent 1a128ab055
commit 2fe6b11e89
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4
66 changed files with 719 additions and 846 deletions

View File

@ -148,7 +148,6 @@
<script type='text/javascript' src='js/chromium.js'></script>
<script type='text/javascript' src='js/registration.js'></script>
<script type='text/javascript' src='js/expire.js'></script>
<script type='text/javascript' src='js/conversation_controller.js'></script>
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
<script type='text/javascript' src='js/views/whisper_view.js'></script>

View File

@ -152,7 +152,6 @@
<script type='text/javascript' src='js/chromium.js'></script>
<script type='text/javascript' src='js/registration.js'></script>
<script type='text/javascript' src='js/expire.js'></script>
<script type='text/javascript' src='js/conversation_controller.js'></script>
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
<script type='text/javascript' src='js/views/whisper_view.js'></script>

View File

@ -1,16 +0,0 @@
import { ConversationModel } from './models/conversations';
export type ConversationControllerType = {
reset: () => void;
load: () => Promise<void>;
get: (id: string) => ConversationModel | undefined;
getOrThrow: (id: string) => ConversationModel;
getOrCreateAndWait: (id: string, type: string) => Promise<ConversationModel>;
getOrCreate: (id: string, type: string) => Promise<ConversationModel>;
dangerouslyCreateAndAdd: (any) => any;
getContactProfileNameOrShortenedPubKey: (id: string) => string;
getContactProfileNameOrFullPubKey: (
hexEncodedGroupPublicKey: string
) => string;
isMediumGroup: (id: string) => boolean;
};

View File

@ -2,7 +2,6 @@
$,
_,
Backbone,
ConversationController,
getAccountManager,
Signal,
storage,
@ -371,7 +370,7 @@
try {
await Promise.all([
ConversationController.load(),
window.getConversationController().load(),
textsecure.storage.protocol.hydrateCaches(),
BlockedNumberController.load(),
]);
@ -406,7 +405,9 @@
return;
}
const conversation = ConversationController.get(conversationId);
const conversation = window
.getConversationController()
.get(conversationId);
messageIds.forEach(id => {
if (conversation) {
conversation.removeMessage(id);
@ -561,10 +562,9 @@
window.showEditProfileDialog = async () => {
const ourNumber = window.storage.get('primaryDevicePubKey');
const conversation = await ConversationController.getOrCreateAndWait(
ourNumber,
'private'
);
const conversation = await window
.getConversationController()
.getOrCreateAndWait(ourNumber, 'private');
const readFile = attachment =>
new Promise((resolve, reject) => {
@ -679,6 +679,7 @@
if (avatar) {
window
.getConversationController()
.getConversations()
.filter(convo => convo.isPublic() && !convo.isRss())
.forEach(convo =>
@ -765,9 +766,9 @@
const conversationId = `publicChat:${channelId}@${rawServerURL}`;
// Quickly peak to make sure we don't already have it
const conversationExists = window.ConversationController.get(
conversationId
);
const conversationExists = window
.getConversationController()
.get(conversationId);
if (conversationExists) {
// We are already a member of this public chat
return new Promise((_resolve, reject) => {
@ -788,10 +789,9 @@
}
// Create conversation
const conversation = await window.ConversationController.getOrCreateAndWait(
conversationId,
'group'
);
const conversation = await window
.getConversationController()
.getOrCreateAndWait(conversationId, 'group');
// Convert conversation to a public one
await conversation.setPublicSource(completeServerURL, channelId);
@ -842,7 +842,9 @@
const sslServerUrl = `https://${rawServerUrl}`;
const conversationId = `publicChat:${channelId}@${rawServerUrl}`;
const conversationExists = ConversationController.get(conversationId);
const conversationExists = window
.getConversationController()
.get(conversationId);
if (conversationExists) {
window.log.warn('We are already a member of this public chat');
window.libsession.Utils.ToastUtils.pushAlreadyMemberOpenGroup();
@ -850,10 +852,9 @@
return;
}
const conversation = await ConversationController.getOrCreateAndWait(
conversationId,
'group'
);
const conversation = await window
.getConversationController()
.getOrCreateAndWait(conversationId, 'group');
await conversation.setPublicSource(sslServerUrl, channelId);
const channelAPI = await window.lokiPublicChatAPI.findOrCreateChannel(
@ -897,10 +898,9 @@
});
Whisper.events.on('onShowUserDetails', async ({ userPubKey }) => {
const conversation = await ConversationController.getOrCreateAndWait(
userPubKey,
'private'
);
const conversation = await window
.getConversationController()
.getOrCreateAndWait(userPubKey, 'private');
const avatarPath = conversation.getAvatarPath();
const profile = conversation.getLokiProfile();
@ -953,7 +953,7 @@
Whisper.events.on('calculatingPoW', ({ pubKey, timestamp }) => {
try {
const conversation = ConversationController.get(pubKey);
const conversation = window.getConversationController().get(pubKey);
conversation.onCalculatingPoW(pubKey, timestamp);
} catch (e) {
window.log.error('Error showing PoW cog');
@ -964,7 +964,7 @@
'publicMessageSent',
({ pubKey, timestamp, serverId, serverTimestamp }) => {
try {
const conversation = ConversationController.get(pubKey);
const conversation = window.getConversationController().get(pubKey);
conversation.onPublicMessageSent(
pubKey,
timestamp,
@ -1031,7 +1031,7 @@
await libsession.getMessageQueue().send(device, unlinkMessage);
// Remove all traces of the device
setTimeout(() => {
ConversationController.deleteContact(pubKey);
window.getConversationController().deleteContact(pubKey);
Whisper.events.trigger('refreshLinkedDeviceList');
callback();
}, 1000);

View File

@ -1,329 +0,0 @@
/* global Whisper, textsecure, libsignal, log */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Whisper = window.Whisper || {};
const conversations = new Whisper.ConversationCollection();
window.getConversations = () => conversations;
window.getMessagesByKey = async key => {
// loadLive gets messages live, not from the database which can lag behind.
let messages = [];
const messageSet = await window.Signal.Data.getMessagesByConversation(key, {
limit: 100,
MessageCollection: Whisper.MessageCollection,
});
messages = messageSet.models.map(conv => conv.attributes);
return messages;
};
window.ConversationController = {
get(id) {
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
return conversations.get(id);
},
getOrThrow(id) {
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
const convo = conversations.get(id);
if (convo) {
return convo;
}
throw new Error(
`Conversation ${id} does not exist on ConversationController.get()`
);
},
// Needed for some model setup which happens during the initial fetch() call below
getUnsafe(id) {
return conversations.get(id);
},
dangerouslyCreateAndAdd(attributes) {
return conversations.add(attributes);
},
getOrCreate(id, type) {
if (typeof id !== 'string') {
throw new TypeError("'id' must be a string");
}
if (type !== 'private' && type !== 'group') {
throw new TypeError(
`'type' must be 'private' or 'group'; got: '${type}'`
);
}
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
let conversation = conversations.get(id);
if (conversation) {
return conversation;
}
conversation = conversations.add({
id,
type,
version: 2,
});
const create = async () => {
if (!conversation.isValid()) {
const validationError = conversation.validationError || {};
window.log.error(
'Contact is not valid. Not saving, but adding to collection:',
conversation.idForLogging(),
validationError.stack
);
return conversation;
}
try {
await window.Signal.Data.saveConversation(conversation.attributes, {
Conversation: Whisper.Conversation,
});
} catch (error) {
window.log.error(
'Conversation save failed! ',
id,
type,
'Error:',
error && error.stack ? error.stack : error
);
throw error;
}
return conversation;
};
conversation.initialPromise = create();
conversation.initialPromise.then(() => {
if (!conversation.isPublic() && !conversation.isRss()) {
Promise.all([
conversation.updateProfileAvatar(),
// NOTE: we request snodes updating the cache, but ignore the result
window.SnodePool.getSnodesFor(id),
]);
}
if (window.inboxStore) {
conversation.on('change', this.updateReduxConvoChanged);
window.inboxStore.dispatch(
window.actionsCreators.conversationAdded(
conversation.id,
conversation.getProps()
)
);
}
});
return conversation;
},
async deleteContact(id) {
if (typeof id !== 'string') {
throw new TypeError("'id' must be a string");
}
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
const conversation = conversations.get(id);
if (!conversation) {
return;
}
// Close group leaving
if (conversation.isClosedGroup()) {
await conversation.leaveGroup();
} else if (conversation.isPublic()) {
const channelAPI = await conversation.getPublicSendData();
if (channelAPI === null) {
log.warn(`Could not get API for public conversation ${id}`);
} else {
channelAPI.serverAPI.partChannel(channelAPI.channelId);
}
} else if (conversation.isPrivate()) {
const deviceIds = await textsecure.storage.protocol.getDeviceIds(id);
await Promise.all(
deviceIds.map(deviceId => {
const address = new libsignal.SignalProtocolAddress(id, deviceId);
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
);
return sessionCipher.deleteAllSessionsForDevice();
})
);
}
await conversation.destroyMessages();
await window.Signal.Data.removeConversation(id, {
Conversation: Whisper.Conversation,
});
conversation.off('change', this.updateReduxConvoChanged);
conversations.remove(conversation);
if (window.inboxStore) {
window.inboxStore.dispatch(
window.actionsCreators.conversationRemoved(conversation.id)
);
}
},
getOrCreateAndWait(id, type) {
return this._initialPromise.then(() => {
if (!id) {
return Promise.reject(
new Error('getOrCreateAndWait: invalid id passed.')
);
}
const pubkey = id && id.key ? id.key : id;
const conversation = this.getOrCreate(pubkey, type);
if (conversation) {
return conversation.initialPromise.then(() => conversation);
}
return Promise.reject(
new Error('getOrCreateAndWait: did not get conversation')
);
});
},
async getAllGroupsInvolvingId(id) {
const groups = await window.Signal.Data.getAllGroupsInvolvingId(id, {
ConversationCollection: Whisper.ConversationCollection,
});
return groups.map(group => conversations.add(group));
},
loadPromise() {
return this._initialPromise;
},
reset() {
this._initialPromise = Promise.resolve();
this._initialFetchComplete = false;
conversations.reset([]);
if (window.inboxStore) {
conversations.forEach(convo =>
convo.off('change', this.updateReduxConvoChanged)
);
window.inboxStore.dispatch(
window.actionsCreators.removeAllConversations()
);
}
},
updateReduxConvoChanged(convo) {
if (window.inboxStore) {
window.inboxStore.dispatch(
window.actionsCreators.conversationChanged(convo.id, convo.getProps())
);
}
},
async load() {
window.log.info('ConversationController: starting initial fetch');
if (conversations.length) {
throw new Error('ConversationController: Already loaded!');
}
const load = async () => {
try {
const collection = await window.Signal.Data.getAllConversations({
ConversationCollection: Whisper.ConversationCollection,
});
conversations.add(collection.models);
this._initialFetchComplete = true;
const promises = [];
conversations.forEach(conversation => {
if (!conversation.get('lastMessage')) {
promises.push(conversation.updateLastMessage());
}
promises.concat([
conversation.updateProfileName(),
conversation.updateProfileAvatar(),
]);
});
conversations.forEach(conversation => {
// register for change event on each conversation, and forward to redux
conversation.on('change', this.updateReduxConvoChanged);
});
await Promise.all(promises);
// Remove any unused images
window.profileImages.removeImagesNotInArray(
conversations.map(c => c.id)
);
window.log.info('ConversationController: done with initial fetch');
} catch (error) {
window.log.error(
'ConversationController: initial fetch failed',
error && error.stack ? error.stack : error
);
throw error;
}
};
await window.BlockedNumberController.load();
this._initialPromise = load();
return this._initialPromise;
},
getContactProfileNameOrShortenedPubKey: pubKey => {
const conversation = window.ConversationController.get(pubKey);
if (!conversation) {
return pubKey;
}
return conversation.getContactProfileNameOrShortenedPubKey();
},
getContactProfileNameOrFullPubKey: pubKey => {
const conversation = window.ConversationController.get(pubKey);
if (!conversation) {
return pubKey;
}
return conversation.getContactProfileNameOrFullPubKey();
},
isMediumGroup: hexEncodedGroupPublicKey =>
conversations
.filter(c => c.isMediumGroup())
.some(c => c.id === hexEncodedGroupPublicKey),
_handleOnline: pubKey => {
try {
const conversation = this.get(pubKey);
conversation.set({ isOnline: true });
} catch (e) {} // eslint-disable-line
},
_handleOffline: pubKey => {
try {
const conversation = this.get(pubKey);
conversation.set({ isOnline: false });
} catch (e) {} // eslint-disable-line
},
};
})();

View File

@ -1,7 +1,6 @@
/* global
Backbone,
Whisper,
ConversationController,
getMessageController,
_,
*/
@ -106,9 +105,9 @@
}
// notify frontend listeners
const conversation = ConversationController.get(
message.get('conversationId')
);
const conversation = window
.getConversationController()
.get(message.get('conversationId'));
if (conversation) {
conversation.trigger('delivered', message);
}

View File

@ -1,4 +1,4 @@
/* global Whisper, SignalProtocolStore, ConversationController, _ */
/* global Whisper, SignalProtocolStore, _ */
/* eslint-disable more/no-then */
@ -15,13 +15,14 @@
}
signalProtocolStore.on('keychange', async id => {
const conversation = await ConversationController.getOrCreateAndWait(
id,
'private'
);
const conversation = await window
.getConversationController()
.getOrCreateAndWait(id, 'private');
conversation.addKeyChange(id);
const groups = await ConversationController.getAllGroupsInvolvingId(id);
const groups = await window
.getConversationController()
.getAllGroupsInvolvingId(id);
_.forEach(groups, group => {
group.addKeyChange(id);
});

View File

@ -21,10 +21,15 @@ interface ConversationAttributes {
subscriberCount?: number;
sessionRestoreSeen?: boolean;
is_medium_group?: boolean;
type: string;
lastMessage?: string;
}
export interface ConversationModel
extends Backbone.Model<ConversationAttributes> {
destroyMessages();
getPublicSendData();
leaveGroup();
idForLogging: () => string;
// Save model changes to the database
commit: () => Promise<void>;
@ -64,7 +69,6 @@ export interface ConversationModel
isModerator: (id?: string) => boolean;
throttledBumpTyping: () => void;
lastMessage: string;
messageCollection: Backbone.Collection<MessageModel>;
// types to make more specific
@ -106,4 +110,9 @@ export interface ConversationModel
sendGroupInfo: any;
onUpdateGroupName: any;
getContactProfileNameOrShortenedPubKey: () => string;
getContactProfileNameOrFullPubKey: () => string;
getProps(): any;
updateLastMessage: () => void;
updateProfileName: any;
updateProfileAvatar: any;
}

View File

@ -4,7 +4,6 @@
i18n,
Backbone,
libsession,
ConversationController,
getMessageController,
storage,
textsecure,
@ -670,10 +669,9 @@
device
);
return ConversationController.getOrCreateAndWait(
primary.key,
'private'
);
return window
.getConversationController()
.getOrCreateAndWait(primary.key, 'private');
}
// Something funky has happened
@ -925,11 +923,14 @@
);
if (this.isPrivate()) {
ConversationController.getAllGroupsInvolvingId(this.id).then(groups => {
_.forEach(groups, group => {
group.addVerifiedChange(this.id, verified, options);
window
.getConversationController()
.getAllGroupsInvolvingId(this.id)
.then(groups => {
_.forEach(groups, group => {
group.addVerifiedChange(this.id, verified, options);
});
});
});
}
},
@ -2026,7 +2027,9 @@
// This function is wrongly named by signal
// This is basically an `update` function and thus we have overwritten it with such
async getProfile(id) {
const c = await ConversationController.getOrCreateAndWait(id, 'private');
const c = await window
.getConversationController()
.getOrCreateAndWait(id, 'private');
// We only need to update the profile as they are all stored inside the conversation
await c.updateProfileName();
@ -2141,7 +2144,7 @@
}
const members = this.get('members') || [];
const promises = members.map(number =>
ConversationController.getOrCreateAndWait(number, 'private')
window.getConversationController().getOrCreateAndWait(number, 'private')
);
return Promise.all(promises).then(contacts => {
@ -2188,7 +2191,7 @@
title,
message,
resolve: () => {
ConversationController.deleteContact(this.id);
window.getConversationController().deleteContact(this.id);
},
});
},
@ -2364,7 +2367,9 @@
// Secondary devices have their profile stored
// in their primary device's conversation
const ourNumber = window.storage.get('primaryDevicePubKey');
const ourConversation = window.ConversationController.get(ourNumber);
const ourConversation = window
.getConversationController()
.get(ourNumber);
let profileKey = null;
if (this.get('profileSharing')) {
profileKey = new Uint8Array(storage.get('profileKey'));
@ -2424,32 +2429,32 @@
}
const conversationId = this.id;
return ConversationController.getOrCreateAndWait(
message.get('source'),
'private'
).then(sender =>
sender.getNotificationIcon().then(iconUrl => {
const messageJSON = message.toJSON();
const messageSentAt = messageJSON.sent_at;
const messageId = message.id;
const isExpiringMessage = Message.hasExpiration(messageJSON);
return window
.getConversationController()
.getOrCreateAndWait(message.get('source'), 'private')
.then(sender =>
sender.getNotificationIcon().then(iconUrl => {
const messageJSON = message.toJSON();
const messageSentAt = messageJSON.sent_at;
const messageId = message.id;
const isExpiringMessage = Message.hasExpiration(messageJSON);
// window.log.info('Add notification', {
// conversationId: this.idForLogging(),
// isExpiringMessage,
// messageSentAt,
// });
Whisper.Notifications.add({
conversationId,
iconUrl,
isExpiringMessage,
message: message.getNotificationText(),
messageId,
messageSentAt,
title: sender.getTitle(),
});
})
);
// window.log.info('Add notification', {
// conversationId: this.idForLogging(),
// isExpiringMessage,
// messageSentAt,
// });
Whisper.Notifications.add({
conversationId,
iconUrl,
isExpiringMessage,
message: message.getNotificationText(),
messageId,
messageSentAt,
title: sender.getTitle(),
});
})
);
},
notifyTyping(options = {}) {
const { isTyping, sender, senderDevice } = options;

View File

@ -3,7 +3,6 @@
Backbone,
storage,
filesize,
ConversationController,
getMessageController,
i18n,
Signal,
@ -177,9 +176,9 @@
} else if (groupUpdate.left) {
return i18n(
'leftTheGroup',
ConversationController.getContactProfileNameOrShortenedPubKey(
groupUpdate.left
)
window
.getConversationController()
.getContactProfileNameOrShortenedPubKey(groupUpdate.left)
);
}
@ -196,7 +195,9 @@
}
if (groupUpdate.joined && groupUpdate.joined.length) {
const names = groupUpdate.joined.map(pubKey =>
ConversationController.getContactProfileNameOrFullPubKey(pubKey)
window
.getConversationController()
.getContactProfileNameOrFullPubKey(pubKey)
);
if (names.length > 1) {
@ -209,7 +210,8 @@
if (groupUpdate.kicked && groupUpdate.kicked.length) {
const names = _.map(
groupUpdate.kicked,
ConversationController.getContactProfileNameOrShortenedPubKey
window.getConversationController()
.getContactProfileNameOrShortenedPubKey
);
if (names.length > 1) {
@ -260,9 +262,9 @@
);
const pubkeysInDesc = description.match(regex);
(pubkeysInDesc || []).forEach(pubkey => {
const displayName = ConversationController.getContactProfileNameOrShortenedPubKey(
pubkey.slice(1)
);
const displayName = window
.getConversationController()
.getContactProfileNameOrShortenedPubKey(pubkey.slice(1));
if (displayName && displayName.length) {
description = description.replace(pubkey, `@${displayName}`);
}
@ -383,7 +385,7 @@
};
},
findContact(phoneNumber) {
return ConversationController.get(phoneNumber);
return window.getConversationController().get(phoneNumber);
},
findAndFormatContact(phoneNumber) {
const { format } = PhoneNumber;
@ -726,7 +728,7 @@
const regionCode = storage.get('regionCode');
const { author, id, referencedMessageNotFound } = quote;
const contact = author && ConversationController.get(author);
const contact = author && window.getConversationController().get(author);
const authorPhoneNumber = format(author, {
ourRegionCode: regionCode,
@ -1029,7 +1031,9 @@
const { body, attachments, preview, quote } = await this.uploadData();
const ourNumber = window.storage.get('primaryDevicePubKey');
const ourConversation = window.ConversationController.get(ourNumber);
const ourConversation = window
.getConversationController()
.get(ourNumber);
const chatParams = {
identifier: this.id,
@ -1304,7 +1308,7 @@
if (error.name === 'SignedPreKeyRotationError') {
await window.getAccountManager().rotateSignedPreKey();
} else if (error.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(sentMessage.device);
const c = window.getConversationController().get(sentMessage.device);
await c.getProfiles();
}
}
@ -1330,15 +1334,16 @@
// This needs to be an unsafe call, because this method is called during
// initial module setup. We may be in the middle of the initial fetch to
// the database.
return ConversationController.getUnsafe(this.get('conversationId'));
return window
.getConversationController()
.getUnsafe(this.get('conversationId'));
},
getSourceDeviceConversation() {
// This gets the conversation of the device that sent this message
// while getConversation will return the primary device conversation
return ConversationController.getOrCreateAndWait(
this.get('source'),
'private'
);
return window
.getConversationController()
.getOrCreateAndWait(this.get('source'), 'private');
},
getIncomingContact() {
if (!this.isIncoming()) {
@ -1351,7 +1356,7 @@
const key = source.key ? source.key : source;
return ConversationController.getOrCreate(key, 'private');
return window.getConversationController().getOrCreate(key, 'private');
},
getQuoteContact() {
const quote = this.get('quote');
@ -1363,7 +1368,7 @@
return null;
}
return ConversationController.get(author);
return window.getConversationController().get(author);
},
getSource() {
@ -1384,7 +1389,9 @@
// is PubKey ano not a string
const sourceStr = source.key ? source.key : source;
return ConversationController.getOrCreate(sourceStr, 'private');
return window
.getConversationController()
.getOrCreate(sourceStr, 'private');
},
isOutgoing() {
return this.get('type') === 'outgoing';

View File

@ -1,169 +0,0 @@
/* eslint-env node */
/* global log */
const fs = require('fs-extra');
const path = require('path');
const {
isFunction,
isNumber,
isObject,
isString,
random,
range,
sample,
} = require('lodash');
const Attachments = require('../../app/attachments');
const Message = require('./types/message');
const { sleep } = require('./sleep');
// See: https://en.wikipedia.org/wiki/Fictitious_telephone_number#North_American_Numbering_Plan
const SENDER_ID = '+12126647665';
exports.createConversation = async ({
ConversationController,
numMessages,
WhisperMessage,
} = {}) => {
if (
!isObject(ConversationController) ||
!isFunction(ConversationController.getOrCreateAndWait)
) {
throw new TypeError("'ConversationController' is required");
}
if (!isNumber(numMessages) || numMessages <= 0) {
throw new TypeError("'numMessages' must be a positive number");
}
if (!isFunction(WhisperMessage)) {
throw new TypeError("'WhisperMessage' is required");
}
const conversation = await ConversationController.getOrCreateAndWait(
SENDER_ID,
'private'
);
conversation.set({
active_at: Date.now(),
unread: numMessages,
});
const conversationId = conversation.get('id');
await conversation.commit();
await Promise.all(
range(0, numMessages).map(async index => {
await sleep(index * 100);
log.info(`Create message ${index + 1}`);
const message = await createRandomMessage({ conversationId });
return message.commit();
})
);
};
const SAMPLE_MESSAGES = [
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'Integer et rutrum leo, eu ultrices ligula.',
'Nam vel aliquam quam.',
'Suspendisse posuere nunc vitae pulvinar lobortis.',
'Nunc et sapien ex.',
'Duis nec neque eu arcu ultrices ullamcorper in et mauris.',
'Praesent mi felis, hendrerit a nulla id, mattis consectetur est.',
'Duis venenatis posuere est sit amet congue.',
'Vestibulum vitae sapien ultricies, auctor purus vitae, laoreet lacus.',
'Fusce laoreet nisi dui, a bibendum metus consequat in.',
'Nulla sed iaculis odio, sed lobortis lacus.',
'Etiam massa felis, gravida at nibh viverra, tincidunt convallis justo.',
'Maecenas ut egestas urna.',
'Pellentesque consectetur mattis imperdiet.',
'Maecenas pulvinar efficitur justo a cursus.',
];
const ATTACHMENT_SAMPLE_RATE = 0.33;
const createRandomMessage = async ({ conversationId } = {}) => {
if (!isString(conversationId)) {
throw new TypeError("'conversationId' must be a string");
}
const sentAt = Date.now() - random(100 * 24 * 60 * 60 * 1000);
const receivedAt = sentAt + random(30 * 1000);
const hasAttachment = Math.random() <= ATTACHMENT_SAMPLE_RATE;
const attachments = hasAttachment
? [await createRandomInMemoryAttachment()]
: [];
const type = sample(['incoming', 'outgoing']);
const commonProperties = {
attachments,
body: sample(SAMPLE_MESSAGES),
conversationId,
received_at: receivedAt,
sent_at: sentAt,
timestamp: receivedAt,
type,
};
const message = _createMessage({ commonProperties, conversationId, type });
return Message.initializeSchemaVersion({ message, logger: log });
};
const _createMessage = ({ commonProperties, conversationId, type } = {}) => {
switch (type) {
case 'incoming':
return Object.assign({}, commonProperties, {
flags: 0,
source: conversationId,
sourceDevice: 1,
});
case 'outgoing':
return Object.assign({}, commonProperties, {
delivered: 1,
delivered_to: [conversationId],
expireTimer: 0,
recipients: [conversationId],
sent_to: [conversationId],
synced: true,
});
default:
throw new TypeError(`Unknown message type: '${type}'`);
}
};
const FIXTURES_PATH = path.join(__dirname, '..', '..', 'fixtures');
const readData = Attachments.createReader(FIXTURES_PATH);
const createRandomInMemoryAttachment = async () => {
const files = (await fs.readdir(FIXTURES_PATH)).map(createFileEntry);
const { contentType, fileName } = sample(files);
const data = await readData(fileName);
return {
contentType,
data,
fileName,
size: data.byteLength,
};
};
const createFileEntry = fileName => ({
fileName,
contentType: fileNameToContentType(fileName),
});
const fileNameToContentType = fileName => {
const fileExtension = path.extname(fileName).toLowerCase();
switch (fileExtension) {
case '.gif':
return 'image/gif';
case '.png':
return 'image/png';
case '.jpg':
case '.jpeg':
return 'image/jpeg';
case '.mp4':
return 'video/mp4';
case '.txt':
return 'text/plain';
default:
return 'application/octet-stream';
}
};

View File

@ -1,4 +1,4 @@
/* global log, textsecure, libloki, Signal, Whisper, ConversationController,
/* global log, textsecure, libloki, Signal, Whisper,
clearTimeout, getMessageController, libsignal, StringView, window, _,
dcodeIO, Buffer, process */
const nodeFetch = require('node-fetch');
@ -599,7 +599,7 @@ class LokiAppDotNetServerAPI {
const ourNumber =
window.storage.get('primaryDevicePubKey') ||
textsecure.storage.user.getNumber();
const profileConvo = ConversationController.get(ourNumber);
const profileConvo = window.getConversationController().get(ourNumber);
const profile = profileConvo && profileConvo.getLokiProfile();
const profileName = profile && profile.displayName;
// if doesn't match, write it to the network
@ -1050,7 +1050,9 @@ class LokiPublicChannelAPI {
this.channelId = channelId;
this.baseChannelUrl = `channels/${this.channelId}`;
this.conversationId = conversationId;
this.conversation = ConversationController.getOrThrow(conversationId);
this.conversation = window
.getConversationController()
.getOrThrow(conversationId);
this.lastMessageServerID = null;
this.modStatus = false;
this.deleteLastId = 1;
@ -2012,7 +2014,9 @@ class LokiPublicChannelAPI {
// if we received one of our own messages
if (lastProfileName !== false) {
// get current profileName
const profileConvo = ConversationController.get(ourNumberProfile);
const profileConvo = window
.getConversationController()
.get(ourNumberProfile);
const profileName = profileConvo.getProfileName();
// check to see if it out of sync
if (profileName !== lastProfileName) {

View File

@ -2,7 +2,6 @@
Whisper,
Backbone,
_,
ConversationController,
getMessageController,
window
*/
@ -105,9 +104,9 @@
}
// notify frontend listeners
const conversation = ConversationController.get(
message.get('conversationId')
);
const conversation = window
.getConversationController()
.get(message.get('conversationId'));
if (conversation) {
conversation.trigger('read', message);
}

View File

@ -5,7 +5,6 @@
_,
libsignal,
textsecure,
ConversationController,
stringObject,
BlockedNumberController
*/
@ -890,9 +889,9 @@
window.storage.reset();
await window.storage.fetch();
ConversationController.reset();
window.getConversationController().reset();
BlockedNumberController.reset();
await ConversationController.load();
await window.getConversationController().load();
await BlockedNumberController.load();
},
async removeAllConfiguration() {

View File

@ -1,4 +1,4 @@
/* global Backbone, i18n, Whisper, storage, _, ConversationController, $ */
/* global Backbone, i18n, Whisper, storage, _, $ */
/* eslint-disable more/no-then */
@ -112,9 +112,12 @@
window,
initialLoadComplete: options.initialLoadComplete,
});
return ConversationController.loadPromise().then(() => {
this.openView(this.inboxView);
});
return window
.getConversationController()
.loadPromise()
.then(() => {
this.openView(this.inboxView);
});
}
if (!$.contains(this.el, this.inboxView.el)) {
this.openView(this.inboxView);
@ -212,7 +215,8 @@
window.confirmationDialog({
title,
message,
resolve: () => ConversationController.deleteContact(groupConvo.id),
resolve: () =>
window.getConversationController().deleteContact(groupConvo.id),
theme: this.getThemeObject(),
});
},

View File

@ -6,7 +6,6 @@
Signal,
storage,
Whisper,
ConversationController,
*/
// eslint-disable-next-line func-names
@ -138,7 +137,7 @@
);
const allMembers = allPubKeys.map(pubKey => {
const conv = ConversationController.get(pubKey);
const conv = window.getConversationController().get(pubKey);
let profileName = 'Anonymous';
if (conv) {
profileName = conv.getProfileName();
@ -1149,8 +1148,9 @@
}
const privateConvos = window
.getConversationController()
.getConversations()
.models.filter(d => d.isPrivate());
.filter(d => d.isPrivate());
const memberConvos = members
.map(m => privateConvos.find(c => c.id === m))
.filter(c => !!c && c.getLokiProfile());

View File

@ -105,7 +105,10 @@
this.isAdmin = groupConvo.isMediumGroup()
? true
: groupConvo.get('groupAdmins').includes(ourPK);
const convos = window.getConversations().models.filter(d => !!d);
const convos = window
.getConversationController()
.getConversations()
.filter(d => !!d);
this.existingMembers = groupConvo.get('members') || [];
// Show a contact if they are our friend or if they are a member

View File

@ -1,4 +1,4 @@
/* global Whisper, storage, i18n, ConversationController */
/* global Whisper, storage, i18n */
/* eslint-disable more/no-then */
@ -171,9 +171,11 @@
});
},
finishLightImport(directory) {
ConversationController.reset();
window.getConversationController().reset();
return ConversationController.load()
return window
.getConversationController()
.load()
.then(() =>
Promise.all([
Whisper.Import.saveLocation(directory),

View File

@ -12,7 +12,7 @@
this.close = this.close.bind(this);
this.submit = this.submit.bind(this);
this.theme = convo.theme;
const convos = window.getConversations().models;
const convos = window.getConversationController().getConversations();
this.contacts = convos.filter(
d =>
@ -64,10 +64,9 @@
channelId: this.channelId,
};
pubkeys.forEach(async pubkeyStr => {
const convo = await window.ConversationController.getOrCreateAndWait(
pubkeyStr,
'private'
);
const convo = await window
.getConversationController()
.getOrCreateAndWait(pubkeyStr, 'private');
if (convo) {
convo.sendMessage('', null, null, null, serverInfos);

View File

@ -19,7 +19,7 @@
// get current list of moderators
this.channelAPI = await convo.getPublicSendData();
const modPubKeys = await this.channelAPI.getModerators();
const convos = window.getConversations().models;
const convos = window.getConversationController().getConversations();
const moderators = modPubKeys
.map(
pubKey =>

View File

@ -1,4 +1,4 @@
/* global window, textsecure, libsession, ConversationController */
/* global window, textsecure, libsession */
/* eslint-disable no-bitwise */
// eslint-disable-next-line func-names
@ -101,7 +101,9 @@
const ourPubKey = textsecure.storage.user.getNumber();
if (memberStr !== ourPubKey) {
const memberPubkey = new libsession.Types.PubKey(memberStr);
await ConversationController.getOrCreateAndWait(memberStr, 'private');
await window
.getConversationController()
.getOrCreateAndWait(memberStr, 'private');
await libsession.Protocols.SessionProtocol.sendSessionRequestIfNeeded(
memberPubkey
);

View File

@ -417,10 +417,9 @@
let conversation;
try {
conversation = await window.ConversationController.getOrCreateAndWait(
this.protocolAddress.getName(),
'private'
);
conversation = await window
.getConversationController()
.getOrCreateAndWait(this.protocolAddress.getName(), 'private');
} catch (e) {
window.log.info(
'Error getting conversation: ',

View File

@ -13,7 +13,6 @@
StringView,
log,
Event,
ConversationController,
Whisper
*/
@ -407,10 +406,9 @@
textsecure.storage.put('primaryDevicePubKey', number);
}
// Ensure that we always have a conversation for ourself
const conversation = await ConversationController.getOrCreateAndWait(
number,
'private'
);
const conversation = await window
.getConversationController()
.getOrCreateAndWait(number, 'private');
await conversation.setLokiProfile({ displayName });
this.dispatchEvent(new Event('registration'));
@ -419,10 +417,9 @@
// throws if invalid
this.validatePubKeyHex(primaryDevicePubKey);
// we need a conversation for sending a message
await ConversationController.getOrCreateAndWait(
primaryDevicePubKey,
'private'
);
await window
.getConversationController()
.getOrCreateAndWait(primaryDevicePubKey, 'private');
const ourPubKey = textsecure.storage.user.getNumber();
if (primaryDevicePubKey === ourPubKey) {
throw new Error('Cannot request to pair with ourselves');
@ -491,10 +488,9 @@
await libsession.Protocols.MultiDeviceProtocol.savePairingAuthorisation(
authorisation
);
const ourConversation = await ConversationController.getOrCreateAndWait(
ourPubKey,
'private'
);
const ourConversation = await window
.getConversationController()
.getOrCreateAndWait(ourPubKey, 'private');
// We need to send the our profile to the secondary device
const lokiProfile = ourConversation.getOurProfile();
@ -531,7 +527,9 @@
// Send sync messages
// bad hack to send sync messages when secondary device is ready to process them
setTimeout(async () => {
const conversations = window.getConversations().models;
const conversations = window
.getConversationController()
.getConversations();
await textsecure.messaging.sendGroupSyncMessage(conversations);
await textsecure.messaging.sendOpenGroupsSyncMessage(conversations);
await textsecure.messaging.sendContactSyncMessage();

View File

@ -152,9 +152,9 @@ Message.prototype = {
profile.displayName = this.profile.displayName;
}
const conversation = window.ConversationController.get(
textsecure.storage.user.getNumber()
);
const conversation = window
.getConversationController()
.get(textsecure.storage.user.getNumber());
const avatarPointer = conversation.get('avatarPointer');
if (avatarPointer) {
profile.avatar = avatarPointer;

View File

@ -180,6 +180,9 @@ window.libsession = require('./ts/session');
window.getMessageController =
window.libsession.Messages.MessageController.getInstance;
window.getConversationController =
window.libsession.Conversations.ConversationController.getInstance;
// We never do these in our code, so we'll prevent it everywhere
window.open = () => null;
// eslint-disable-next-line no-eval, no-multi-assign
@ -217,9 +220,9 @@ window.onUnblockNumber = async number => {
}
// Update the conversation
if (window.ConversationController) {
if (window.getConversationController()) {
try {
const conversation = window.ConversationController.get(number);
const conversation = window.getConversationController().get(number);
await conversation.unblock();
} catch (e) {
window.log.info(
@ -415,7 +418,6 @@ window.Signal = Signal.setup({
// Pulling these in separately since they access filesystem, electron
window.Signal.Backup = require('./js/modules/backup');
window.Signal.Debug = require('./js/modules/debug');
window.Signal.Logs = require('./js/modules/logs');
window.addEventListener('contextmenu', e => {

View File

@ -1,4 +1,4 @@
/* global $, ConversationController, textsecure, Whisper */
/* global $, textsecure, Whisper */
'use strict';
@ -14,17 +14,16 @@ describe('Fixtures', () => {
'testDevice'
);
await ConversationController.getOrCreateAndWait(
textsecure.storage.user.getNumber(),
'private'
);
await window
.getConversationController()
.getOrCreateAndWait(textsecure.storage.user.getNumber(), 'private');
});
it('renders', async () => {
await Whisper.Fixtures().saveAll();
ConversationController.reset();
await ConversationController.load();
window.getConversationController().reset();
await window.getConversationController().load();
let view = new Whisper.InboxView({ window });
view.$el.prependTo($('#render-light-theme'));

View File

@ -184,7 +184,6 @@
<script type="text/javascript" src="../js/models/messages.js" data-cover></script>
<script type="text/javascript" src="../js/models/conversations.js" data-cover></script>
<script type="text/javascript" src="../js/conversation_controller.js" data-cover></script>
<script type="text/javascript" src="../js/keychange_listener.js" data-cover></script>
<script type="text/javascript" src="../js/expiring_messages.js" data-cover></script>
<script type="text/javascript" src="../js/notifications.js" data-cover></script>
@ -225,7 +224,6 @@
<script type="text/javascript" src="models/conversations_test.js"></script>
<script type="text/javascript" src="models/messages_test.js"></script>
<script type="text/javascript" src="conversation_controller_test.js"></script>
<script type="text/javascript" src="storage_test.js"></script>
<script type="text/javascript" src="keychange_listener_test.js"></script>
<script type="text/javascript" src="reliable_trigger_test.js"></script>

View File

@ -1,4 +1,4 @@
/* global ConversationController, i18n, Whisper */
/* global i18n, Whisper */
'use strict';
@ -15,8 +15,8 @@ const source = '+14155555555';
describe('MessageCollection', () => {
before(async () => {
await clearDatabase();
ConversationController.reset();
await ConversationController.load();
window.getConversationController().reset();
await window.getConversationController().load();
});
after(() => {
return clearDatabase();

View File

@ -1,7 +1,8 @@
// tslint:disable-next-line: no-implicit-dependencies
import { assert } from 'chai';
import { ConversationController } from '../../ts/session/conversations';
const { libsignal, Whisper, ConversationController } = window;
const { libsignal, Whisper } = window;
describe('KeyChangeListener', () => {
const phoneNumberWithKeyChange = '+13016886524'; // nsa
@ -29,10 +30,10 @@ describe('KeyChangeListener', () => {
let convo: any;
before(async () => {
convo = ConversationController.dangerouslyCreateAndAdd({
convo = ConversationController.getInstance().dangerouslyCreateAndAdd({
id: phoneNumberWithKeyChange,
type: 'private',
});
} as any);
await window.Signal.Data.saveConversation(convo.attributes, {
Conversation: Whisper.Conversation,
});
@ -58,11 +59,11 @@ describe('KeyChangeListener', () => {
let convo: any;
before(async () => {
convo = ConversationController.dangerouslyCreateAndAdd({
convo = ConversationController.getInstance().dangerouslyCreateAndAdd({
id: 'groupId',
type: 'group',
members: [phoneNumberWithKeyChange],
});
} as any);
await window.Signal.Data.saveConversation(convo.attributes, {
Conversation: Whisper.Conversation,
});

View File

@ -8,6 +8,7 @@ import { toast } from 'react-toastify';
import { SessionToast, SessionToastType } from './session/SessionToast';
import { ToastUtils } from '../session/utils';
import { DefaultTheme } from 'styled-components';
import { ConversationController } from '../session/conversations';
interface Props {
onClose: any;
@ -191,7 +192,7 @@ export class DevicePairingDialog extends React.Component<Props, State> {
throw new Error('pubKeyToUnpair must be set in renderUnpairDeviceView()');
}
const secretWords = window.mnemonic.pubkey_to_secret_words(pubKeyToUnpair);
const conv = window.ConversationController.get(pubKeyToUnpair);
const conv = ConversationController.getInstance().get(pubKeyToUnpair);
let description;
if (conv && conv.getNickname()) {
@ -302,7 +303,7 @@ export class DevicePairingDialog extends React.Component<Props, State> {
);
const { currentPubKey } = this.state;
if (currentPubKey) {
const conv = window.ConversationController.get(currentPubKey);
const conv = ConversationController.getInstance().get(currentPubKey);
if (conv) {
void conv.setNickname(this.state.deviceAlias);
}

View File

@ -284,6 +284,7 @@ class MessageInner extends React.PureComponent<Props, State> {
showSkipControls={false}
showJumpControls={false}
showDownloadProgress={false}
listenInterval={100}
customIcons={{
play: (
<SessionIcon

View File

@ -13,6 +13,7 @@ import { StateType } from '../../state/reducer';
import { MessageEncrypter } from '../../session/crypto';
import { PubKey } from '../../session/types';
import { UserUtil } from '../../util';
import { ConversationController } from '../../session/conversations';
// tslint:disable-next-line: no-import-side-effect no-submodule-imports
export enum SectionType {
@ -89,7 +90,7 @@ class ActionsPanelPrivate extends React.Component<Props> {
if (type === SectionType.Profile) {
const ourPrimary = window.storage.get('primaryDevicePubKey');
const conversation = window.ConversationController.get(ourPrimary);
const conversation = ConversationController.getInstance().get(ourPrimary);
const profile = conversation?.getLokiProfile();
const userName = (profile && profile.displayName) || ourPrimary;

View File

@ -17,6 +17,7 @@ import {
import { ToastUtils } from '../../session/utils';
import { DefaultTheme } from 'styled-components';
import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
import { ConversationController } from '../../session/conversations';
export interface Props {
directContacts: Array<ConversationType>;
@ -127,12 +128,11 @@ export class LeftPaneContactSection extends React.Component<Props, State> {
ToastUtils.pushToastError('addContact', error);
} else {
// tslint:disable-next-line: no-floating-promises
window.ConversationController.getOrCreateAndWait(
sessionID,
'private'
).then(() => {
this.props.openConversationExternal(sessionID);
});
ConversationController.getInstance()
.getOrCreateAndWait(sessionID, 'private')
.then(() => {
this.props.openConversationExternal(sessionID);
});
}
}

View File

@ -30,6 +30,7 @@ import { OpenGroup } from '../../session/types';
import { ToastUtils } from '../../session/utils';
import { DefaultTheme } from 'styled-components';
import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
import { ConversationController } from '../../session/conversations';
export interface Props {
searchTerm: string;
@ -395,7 +396,10 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
const error = validateNumber(pubkey);
if (!error) {
await window.ConversationController.getOrCreateAndWait(pubkey, 'private');
await ConversationController.getInstance().getOrCreateAndWait(
pubkey,
'private'
);
openConversationExternal(pubkey);
} else {
ToastUtils.pushToastError('invalidPubKey', error);

View File

@ -13,6 +13,7 @@ import { SessionIdEditable } from './SessionIdEditable';
import { SessionSpinner } from './SessionSpinner';
import { StringUtils, ToastUtils } from '../../session/utils';
import { lightTheme } from '../../state/ducks/SessionTheme';
import { ConversationController } from '../../session/conversations';
enum SignInMode {
Default,
@ -788,8 +789,8 @@ export class RegistrationTabs extends React.Component<any, State> {
private async resetRegistration() {
await window.Signal.Data.removeAll();
await window.storage.fetch();
window.ConversationController.reset();
await window.ConversationController.load();
ConversationController.getInstance().reset();
await ConversationController.getInstance().load();
window.Whisper.RotateSignedPreKeyListener.stop(window.Whisper.events);
this.setState({

View File

@ -2,6 +2,8 @@ import React from 'react';
import { Provider } from 'react-redux';
import { bindActionCreators } from 'redux';
import { getMessageQueue } from '../../session';
import { ConversationController } from '../../session/conversations/ConversationController';
import { MessageController } from '../../session/messages';
import { OpenGroupMessage } from '../../session/messages/outgoing';
import { RawMessage } from '../../session/types';
import { createStore } from '../../state/createStore';
@ -130,7 +132,7 @@ export class SessionInboxView extends React.Component<Props, State> {
}
// find the corresponding conversation of this message
const conv = window.ConversationController.get(
const conv = ConversationController.getInstance().get(
tmpMsg.get('conversationId')
);
@ -175,13 +177,15 @@ export class SessionInboxView extends React.Component<Props, State> {
private async setupLeftPane() {
// Here we set up a full redux store with initial state for our LeftPane Root
const convoCollection = window.getConversations();
const convoCollection = ConversationController.getInstance().getConversations();
const conversations = convoCollection.map(
(conversation: any) => conversation.cachedProps
);
const filledConversations = conversations.map(async (conv: any) => {
const messages = await window.getMessagesByKey(conv.id);
const messages = await MessageController.getInstance().getMessagesByKeyFromDb(
conv.id
);
return { ...conv, messages };
});

View File

@ -28,6 +28,7 @@ import { Mention, MentionsInput } from 'react-mentions';
import { MemberItem } from '../../conversation/MemberList';
import { CaptionEditor } from '../../CaptionEditor';
import { DefaultTheme } from 'styled-components';
import { ConversationController } from '../../../session/conversations/ConversationController';
export interface ReplyingToMessageProps {
convoId: string;
@ -451,7 +452,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
}
private fetchUsersForClosedGroup(query: any, callback: any) {
const conversationModel = window.ConversationController.get(
const conversationModel = ConversationController.getInstance().get(
this.props.conversationKey
);
if (!conversationModel) {
@ -460,7 +461,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
const allPubKeys = conversationModel.get('members');
const allMembers = allPubKeys.map(pubKey => {
const conv = window.ConversationController.get(pubKey);
const conv = ConversationController.getInstance().get(pubKey);
let profileName = 'Anonymous';
if (conv) {
profileName = conv.getProfileName();
@ -722,7 +723,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
// Also, check for a message length change before firing it up, to avoid
// catching ESC, tab, or whatever which is not typing
if (message.length && message.length !== this.lastBumpTypingMessageLength) {
const conversationModel = window.ConversationController.get(
const conversationModel = ConversationController.getInstance().get(
this.props.conversationKey
);
if (!conversationModel) {

View File

@ -31,6 +31,7 @@ import { MessageView } from '../../MainViewController';
import { getMessageById } from '../../../../js/modules/data';
import { pushUnblockToSend } from '../../../session/utils/Toast';
import { MessageDetail } from '../../conversation/MessageDetail';
import { ConversationController } from '../../../session/conversations';
interface State {
// Message sending progress
@ -87,7 +88,7 @@ export class SessionConversation extends React.Component<Props, State> {
const { conversationKey } = this.props;
const conversationModel = window.ConversationController.get(
const conversationModel = ConversationController.getInstance().get(
conversationKey
);
@ -253,7 +254,7 @@ export class SessionConversation extends React.Component<Props, State> {
const selectionMode = !!selectedMessages.length;
const { conversation, conversationKey, messages } = this.props;
const conversationModel = window.ConversationController.get(
const conversationModel = ConversationController.getInstance().get(
conversationKey
);
@ -395,7 +396,7 @@ export class SessionConversation extends React.Component<Props, State> {
public async loadInitialMessages() {
const { conversationKey } = this.props;
const conversationModel = window.ConversationController.get(
const conversationModel = ConversationController.getInstance().get(
conversationKey
);
if (!conversationModel) {
@ -419,7 +420,7 @@ export class SessionConversation extends React.Component<Props, State> {
infoViewState,
messageDetailShowProps,
} = this.state;
const conversation = window.ConversationController.getOrThrow(
const conversation = ConversationController.getInstance().getOrThrow(
conversationKey
);
const expireTimer = conversation.get('expireTimer');
@ -545,7 +546,7 @@ export class SessionConversation extends React.Component<Props, State> {
public getRightPanelProps() {
const { conversationKey } = this.props;
const conversation = window.ConversationController.getOrThrow(
const conversation = ConversationController.getInstance().getOrThrow(
conversationKey
);
@ -661,7 +662,7 @@ export class SessionConversation extends React.Component<Props, State> {
// Get message objects
const { conversationKey, messages } = this.props;
const conversationModel = window.ConversationController.getOrThrow(
const conversationModel = ConversationController.getInstance().getOrThrow(
conversationKey
);
const selectedMessages = messages.filter(message =>
@ -815,7 +816,7 @@ export class SessionConversation extends React.Component<Props, State> {
}
if (!_.isEqual(this.state.quotedMessageTimestamp, quotedMessageTimestamp)) {
const { messages, conversationKey } = this.props;
const conversationModel = window.ConversationController.getOrThrow(
const conversationModel = ConversationController.getInstance().getOrThrow(
conversationKey
);
@ -1222,7 +1223,7 @@ export class SessionConversation extends React.Component<Props, State> {
);
const allMembers = allPubKeys.map((pubKey: string) => {
const conv = window.ConversationController.get(pubKey);
const conv = ConversationController.getInstance().get(pubKey);
let profileName = 'Anonymous';
if (conv) {
profileName = conv.getProfileName();

View File

@ -17,6 +17,7 @@ import { SessionLastSeenIndicator } from './SessionLastSeedIndicator';
import { VerificationNotification } from '../../conversation/VerificationNotification';
import { ToastUtils } from '../../../session/utils';
import { TypingBubble } from '../../conversation/TypingBubble';
import { ConversationController } from '../../../session/conversations';
interface State {
showScrollButton: boolean;
@ -134,7 +135,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
let displayedName = null;
if (conversation.type === 'direct') {
displayedName = window.ConversationController.getContactProfileNameOrShortenedPubKey(
displayedName = ConversationController.getInstance().getContactProfileNameOrShortenedPubKey(
conversationKey
);
}
@ -366,7 +367,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
return;
}
const conversation = window.ConversationController.getOrThrow(
const conversation = ConversationController.getInstance().getOrThrow(
conversationKey
);

View File

@ -18,6 +18,7 @@ import {
import { mapDispatchToProps } from '../../../state/actions';
import { connect } from 'react-redux';
import { StateType } from '../../../state/reducer';
import { ConversationController } from '../../../session/conversations';
export enum SessionSettingCategory {
Appearance = 'appearance',
@ -341,7 +342,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
}
const secretWords = window.mnemonic.pubkey_to_secret_words(pubKey);
const conv = window.ConversationController.get(pubKey);
const conv = ConversationController.getInstance().get(pubKey);
const deviceAlias = conv ? conv.getNickname() : 'Unnamed Device';
return { deviceAlias, secretWords };
@ -594,7 +595,9 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
for (const blockedNumber of blockedNumbers) {
let title: string;
const currentModel = window.ConversationController.get(blockedNumber);
const currentModel = ConversationController.getInstance().get(
blockedNumber
);
if (currentModel) {
title =
currentModel.getProfileName() ||

View File

@ -3,6 +3,7 @@ import { UserUtil } from '../../util';
import { PubKey } from '../../session/types';
import React from 'react';
import * as _ from 'lodash';
import { ConversationController } from '../../session/conversations';
export type ConversationAvatar = {
avatarPath?: string;
@ -69,7 +70,10 @@ export function usingClosedConversationDetails(WrappedComponent: any) {
members = members.slice(0, 2);
const memberConvos = await Promise.all(
members.map(async m =>
window.ConversationController.getOrCreateAndWait(m.key, 'private')
ConversationController.getInstance().getOrCreateAndWait(
m.key,
'private'
)
)
);
const memberAvatars = memberConvos.map(m => {

View File

@ -20,6 +20,7 @@ import { StringUtils } from '../session/utils';
import { UserUtil } from '../util';
import { fromHex, toHex } from '../session/utils/String';
import { concatUInt8Array, getSodium } from '../session/crypto';
import { ConversationController } from '../session/conversations';
export async function handleContentMessage(envelope: EnvelopePlus) {
try {
@ -147,7 +148,7 @@ async function decryptWithSessionProtocol(
case SignalService.Envelope.Type.MEDIUM_GROUP_CIPHERTEXT: {
const hexEncodedGroupPublicKey = envelope.source;
isMediumGroup = window.ConversationController.isMediumGroup(
isMediumGroup = ConversationController.getInstance().isMediumGroup(
hexEncodedGroupPublicKey
);
@ -490,7 +491,7 @@ async function decrypt(
// include the prekeyId in that new message.
// We won't find this preKeyId as we already burnt it when the sender established the session.
const convo = window.ConversationController.get(envelope.source);
const convo = ConversationController.getInstance().get(envelope.source);
if (!convo) {
window.log.warn('PreKeyMissing but convo is missing too. Dropping...');
return;
@ -543,7 +544,7 @@ function shouldDropBlockedUserMessage(content: SignalService.Content): boolean {
}
const groupId = StringUtils.decode(content.dataMessage.group.id, 'utf8');
const groupConvo = window.ConversationController.get(groupId);
const groupConvo = ConversationController.getInstance().get(groupId);
if (!groupConvo) {
return true;
}
@ -579,7 +580,6 @@ export async function innerHandleContentMessage(
envelope: EnvelopePlus,
plaintext: ArrayBuffer
): Promise<void> {
const { ConversationController } = window;
try {
const content = SignalService.Content.decode(new Uint8Array(plaintext));
@ -597,7 +597,10 @@ export async function innerHandleContentMessage(
}
const { FALLBACK_MESSAGE } = SignalService.Envelope.Type;
await ConversationController.getOrCreateAndWait(envelope.source, 'private');
await ConversationController.getInstance().getOrCreateAndWait(
envelope.source,
'private'
);
if (envelope.type !== FALLBACK_MESSAGE) {
const device = new PubKey(envelope.source);
@ -737,7 +740,6 @@ async function handleTypingMessage(
const typingMessage = iTypingMessage as SignalService.TypingMessage;
const { ConversationController } = window;
const { timestamp, groupId, action } = typingMessage;
const { source } = envelope;
@ -770,7 +772,7 @@ async function handleTypingMessage(
const convoId = (primaryDevice && primaryDevice.key) || source;
const conversation = ConversationController.get(convoId);
const conversation = ConversationController.getInstance().get(convoId);
const started = action === SignalService.TypingMessage.Action.STARTED;

View File

@ -16,6 +16,7 @@ import _ from 'lodash';
import { StringUtils } from '../session/utils';
import { DeliveryReceiptMessage } from '../session/messages/outgoing';
import { getMessageQueue } from '../session';
import { ConversationController } from '../session/conversations';
export async function updateProfile(
conversation: any,
@ -75,11 +76,10 @@ export async function updateProfile(
const allUserDevices = await MultiDeviceProtocol.getAllDevices(
conversation.id
);
const { ConversationController } = window;
await Promise.all(
allUserDevices.map(async device => {
const conv = await ConversationController.getOrCreateAndWait(
const conv = await ConversationController.getInstance().getOrCreateAndWait(
device.key,
'private'
);
@ -300,7 +300,7 @@ export async function handleDataMessage(
const ourPubKey = window.textsecure.storage.user.getNumber();
const senderPubKey = envelope.senderIdentity || envelope.source;
const isMe = senderPubKey === ourPubKey;
const conversation = window.ConversationController.get(senderPubKey);
const conversation = ConversationController.getInstance().get(senderPubKey);
const { UNPAIRING_REQUEST } = SignalService.DataMessage.Flags;
@ -417,7 +417,7 @@ async function handleProfileUpdate(
const profileKey = StringUtils.decode(profileKeyBuffer, 'base64');
if (!isIncoming) {
const receiver = await window.ConversationController.getOrCreateAndWait(
const receiver = await ConversationController.getInstance().getOrCreateAndWait(
convoId,
convoType
);
@ -427,7 +427,7 @@ async function handleProfileUpdate(
// Then we update our own profileKey if it's different from what we have
const ourNumber = window.textsecure.storage.user.getNumber();
const me = await window.ConversationController.getOrCreate(
const me = await ConversationController.getInstance().getOrCreate(
ourNumber,
'private'
);
@ -435,7 +435,7 @@ async function handleProfileUpdate(
// Will do the save for us if needed
await me.setProfileKey(profileKey);
} else {
const sender = await window.ConversationController.getOrCreateAndWait(
const sender = await ConversationController.getInstance().getOrCreateAndWait(
convoId,
'private'
);
@ -669,7 +669,7 @@ export async function handleMessageEvent(event: MessageEvent): Promise<void> {
);
}
const conv = await window.ConversationController.getOrCreateAndWait(
const conv = await ConversationController.getInstance().getOrCreateAndWait(
conversationId,
type
);
@ -720,7 +720,9 @@ export async function handleMessageEvent(event: MessageEvent): Promise<void> {
}
// the conversation with the primary device of that source (can be the same as conversationOrigin)
const conversation = window.ConversationController.getOrThrow(conversationId);
const conversation = ConversationController.getInstance().getOrThrow(
conversationId
);
conversation.queueJob(() => {
handleMessageJob(

View File

@ -2,13 +2,12 @@ import { initIncomingMessage } from './dataMessage';
import { toNumber } from 'lodash';
import { SessionProtocol } from '../session/protocols';
import { PubKey } from '../session/types';
import { ConversationController } from '../session/conversations';
async function onNoSession(ev: any) {
const { ConversationController, Whisper } = window;
const pubkey = ev.proto.source;
const convo = await ConversationController.getOrCreateAndWait(
const convo = await ConversationController.getInstance().getOrCreateAndWait(
pubkey,
'private'
);
@ -40,7 +39,6 @@ export async function onError(ev: any) {
return;
}
const { ConversationController, Whisper } = window;
const { error } = ev;
window.log.error(
'background onError:',
@ -62,7 +60,7 @@ export async function onError(ev: any) {
message.saveErrors(error || new Error('Error was null'));
const id = message.get('conversationId');
const conversation = await ConversationController.getOrCreateAndWait(
const conversation = await ConversationController.getInstance().getOrCreateAndWait(
id,
'private'
);

View File

@ -4,6 +4,7 @@ import { getMessageQueue } from '../session';
import { PubKey } from '../session/types';
import _ from 'lodash';
import { BlockedNumberController } from '../util/blockedNumberController';
import { ConversationController } from '../session/conversations';
function isGroupBlocked(groupId: string) {
return BlockedNumberController.isGroupBlocked(groupId);
@ -34,7 +35,7 @@ export async function preprocessGroupMessage(
primarySource: string
) {
const conversationId = group.id;
const conversation = await window.ConversationController.getOrCreateAndWait(
const conversation = await ConversationController.getInstance().getOrCreateAndWait(
conversationId,
'group'
);

View File

@ -14,6 +14,7 @@ import {
createSenderKeyForGroup,
shareSenderKeys,
} from '../session/medium_group/senderKeys';
import { ConversationController } from '../session/conversations';
async function handleSenderKeyRequest(
envelope: EnvelopePlus,
@ -161,7 +162,7 @@ async function handleNewGroup(
const members = membersBinary.map(toHex);
const admins = adminsBinary.map(toHex);
const maybeConvo = window.ConversationController.get(groupId);
const maybeConvo = ConversationController.getInstance().get(groupId);
const groupExists = !!maybeConvo;
@ -185,7 +186,10 @@ async function handleNewGroup(
const convo =
maybeConvo ||
(await window.ConversationController.getOrCreateAndWait(groupId, 'group'));
(await ConversationController.getInstance().getOrCreateAndWait(
groupId,
'group'
));
await SenderKeyAPI.addUpdateMessage(convo, { newName: name }, 'incoming');
@ -265,7 +269,7 @@ async function handleMediumGroupChange(
const groupId = toHex(groupPublicKey);
const convo = window.ConversationController.get(groupId);
const convo = ConversationController.getInstance().get(groupId);
if (!convo) {
log.warn(

View File

@ -13,6 +13,7 @@ import { PubKey } from '../session/types';
import ByteBuffer from 'bytebuffer';
import { BlockedNumberController } from '../util';
import { ConversationController } from '../session/conversations';
async function unpairingRequestIsLegit(source: string, ourPubKey: string) {
const { textsecure, storage, lokiFileServerAPI } = window;
@ -144,7 +145,7 @@ async function handleAuthorisationForSelf(
pairingAuthorisation: SignalService.IPairingAuthorisationMessage,
dataMessage: SignalService.IDataMessage | undefined | null
) {
const { ConversationController, libloki, Whisper } = window;
const { libloki, Whisper } = window;
const valid = await libloki.crypto.validateAuthorisation(
pairingAuthorisation
@ -174,7 +175,7 @@ async function handleAuthorisationForSelf(
await MultiDeviceProtocol.savePairingAuthorisation(
pairingAuthorisation as Data.PairingAuthorisation
);
const primaryConversation = await ConversationController.getOrCreateAndWait(
const primaryConversation = await ConversationController.getInstance().getOrCreateAndWait(
primaryDevicePubKey,
'private'
);
@ -186,7 +187,7 @@ async function handleAuthorisationForSelf(
if (profile && profileKey) {
const ourNumber = window.storage.get('primaryDevicePubKey');
const me = window.ConversationController.get(ourNumber);
const me = ConversationController.getInstance().get(ourNumber);
if (me) {
await updateProfile(me, profile, profileKey);
}
@ -298,13 +299,7 @@ export async function handleContacts(
// tslint:disable-next-line: max-func-body-length
async function onContactReceived(details: any) {
const {
ConversationController,
storage,
textsecure,
libloki,
Whisper,
} = window;
const { storage, textsecure, libloki, Whisper } = window;
const { Errors } = window.Signal.Types;
const id = details.number;
@ -334,7 +329,7 @@ async function onContactReceived(details: any) {
}
try {
const conversation = await ConversationController.getOrCreateAndWait(
const conversation = await ConversationController.getInstance().getOrCreateAndWait(
id,
'private'
);
@ -349,13 +344,13 @@ async function onContactReceived(details: any) {
const primaryDevice = await MultiDeviceProtocol.getPrimaryDevice(id);
const secondaryDevices = await MultiDeviceProtocol.getSecondaryDevices(id);
const primaryConversation = await ConversationController.getOrCreateAndWait(
const primaryConversation = await ConversationController.getInstance().getOrCreateAndWait(
primaryDevice.key,
'private'
);
const secondaryConversations = await Promise.all(
secondaryDevices.map(async d => {
const secondaryConv = await ConversationController.getOrCreateAndWait(
const secondaryConv = await ConversationController.getInstance().getOrCreateAndWait(
d.key,
'private'
);

View File

@ -8,6 +8,7 @@ import _ from 'lodash';
import { MultiDeviceProtocol } from '../session/protocols';
import { SignalService } from '../protobuf';
import { StringUtils } from '../session/utils';
import { ConversationController } from '../session/conversations';
async function handleGroups(
conversation: ConversationModel,
@ -372,7 +373,6 @@ async function handleRegularMessage(
ourNumber: any,
primarySource: PubKey
) {
const { ConversationController } = window;
const { upgradeMessageSchema } = window.Signal.Migrations;
const type = message.get('type');
@ -448,13 +448,13 @@ async function handleRegularMessage(
!conversationTimestamp ||
message.get('sent_at') > conversationTimestamp
) {
conversation.lastMessage = message.getNotificationText();
conversation.set({
timestamp: message.get('sent_at'),
lastMessage: message.getNotificationText(),
});
}
const sendingDeviceConversation = await ConversationController.getOrCreateAndWait(
const sendingDeviceConversation = await ConversationController.getInstance().getOrCreateAndWait(
source,
'private'
);

View File

@ -23,6 +23,7 @@ import { getEnvelopeId } from './common';
import { StringUtils } from '../session/utils';
import { SignalService } from '../protobuf';
import { MultiDeviceProtocol } from '../session/protocols';
import { ConversationController } from '../session/conversations';
// TODO: check if some of these exports no longer needed
@ -285,7 +286,7 @@ export async function handleUnencryptedMessage({ message: outerMessage }: any) {
const isMe = source === ourNumber;
if (!isMe && profile) {
const conversation = await window.ConversationController.getOrCreateAndWait(
const conversation = await ConversationController.getInstance().getOrCreateAndWait(
source,
'private'
);

View File

@ -1,10 +1,10 @@
import { ConversationController } from '../session/conversations';
export async function handleEndSession(number: string): Promise<void> {
window.log.info('got end session');
const { ConversationController } = window;
try {
const conversation = ConversationController.get(number);
const conversation = ConversationController.getInstance().get(number);
if (conversation) {
// this just marks the conversation as being waiting for a new session
// it does trigger a message to be sent. (the message is sent from handleSessionRequestMessage())

View File

@ -17,6 +17,7 @@ import { handleContacts } from './multidevice';
import { updateOrCreateGroupFromSync } from '../session/medium_group';
import { MultiDeviceProtocol } from '../session/protocols';
import { BlockedNumberController } from '../util';
import { ConversationController } from '../session/conversations';
export async function handleSyncMessage(
envelope: EnvelopePlus,
@ -107,8 +108,6 @@ async function handleSentMessage(
return;
}
const { ConversationController } = window;
// tslint:disable-next-line no-bitwise
if (msg.flags && msg.flags & SignalService.DataMessage.Flags.END_SESSION) {
await handleEndSession(destination as string);
@ -125,7 +124,9 @@ async function handleSentMessage(
// handle profileKey and avatar updates
if (envelope.source === primaryDevicePubKey) {
const { profileKey, profile } = message;
const primaryConversation = ConversationController.get(primaryDevicePubKey);
const primaryConversation = ConversationController.getInstance().get(
primaryDevicePubKey
);
if (profile) {
await updateProfile(primaryConversation, profile, profileKey);
}
@ -188,7 +189,7 @@ async function handleBlocked(
);
async function markConvoBlocked(block: boolean, n: string) {
const conv = window.ConversationController.get(n);
const conv = ConversationController.getInstance().get(n);
if (conv) {
if (conv.isPrivate()) {
await BlockedNumberController.setBlocked(n, block);
@ -260,7 +261,7 @@ async function handleVerified(
}
export async function onVerified(ev: any) {
const { ConversationController, textsecure, Whisper } = window;
const { Whisper } = window;
const { Errors } = window.Signal.Types;
const number = ev.verified.destination;
@ -300,7 +301,7 @@ export async function onVerified(ev: any) {
ev.viaContactSync ? 'via contact sync' : ''
);
const contact = await ConversationController.getOrCreateAndWait(
const contact = await ConversationController.getInstance().getOrCreateAndWait(
number,
'private'
);

View File

@ -0,0 +1,336 @@
import {
ConversationAttributes,
ConversationModel,
} from '../../../js/models/conversations';
import { BlockedNumberController } from '../../util';
// It's not only data from the db which is stored on the MessageController entries, we could fetch this again. What we cannot fetch from the db and which is stored here is all listeners a particular messages is linked to for instance. We will be able to get rid of this once we don't use backbone models at all
export class ConversationController {
private static instance: ConversationController | null;
private readonly conversations: any;
private _initialFetchComplete: boolean = false;
private _initialPromise?: Promise<any>;
private constructor() {
this.conversations = new window.Whisper.ConversationCollection();
}
public static getInstance() {
if (ConversationController.instance) {
return ConversationController.instance;
}
ConversationController.instance = new ConversationController();
return ConversationController.instance;
}
public get(id: string): ConversationModel {
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
return this.conversations.get(id);
}
public getOrThrow(id: string) {
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
const convo = this.conversations.get(id);
if (convo) {
return convo;
}
throw new Error(
`Conversation ${id} does not exist on ConversationController.get()`
);
}
// Needed for some model setup which happens during the initial fetch() call below
public getUnsafe(id: string) {
return this.conversations.get(id);
}
public dangerouslyCreateAndAdd(attributes: ConversationAttributes) {
return this.conversations.add(attributes);
}
public getOrCreate(id: string, type: string) {
if (typeof id !== 'string') {
throw new TypeError("'id' must be a string");
}
if (type !== 'private' && type !== 'group') {
throw new TypeError(
`'type' must be 'private' or 'group'; got: '${type}'`
);
}
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
let conversation = this.conversations.get(id);
if (conversation) {
return conversation;
}
conversation = this.conversations.add({
id,
type,
version: 2,
} as any);
const create = async () => {
if (!conversation.isValid()) {
const validationError = conversation.validationError || {};
window.log.error(
'Contact is not valid. Not saving, but adding to collection:',
conversation.idForLogging(),
validationError.stack
);
return conversation;
}
try {
await window.Signal.Data.saveConversation(conversation.attributes, {
Conversation: window.Whisper.Conversation,
});
} catch (error) {
window.log.error(
'Conversation save failed! ',
id,
type,
'Error:',
error && error.stack ? error.stack : error
);
throw error;
}
return conversation;
};
conversation.initialPromise = create();
conversation.initialPromise.then(async () => {
if (!conversation.isPublic() && !conversation.isRss()) {
await Promise.all([
conversation.updateProfileAvatar(),
// NOTE: we request snodes updating the cache, but ignore the result
window.SnodePool.getSnodesFor(id),
]);
}
if (window.inboxStore) {
conversation.on('change', this.updateReduxConvoChanged);
window.inboxStore.dispatch(
window.actionsCreators.conversationAdded(
conversation.id,
conversation.getProps()
)
);
}
});
return conversation;
}
public getContactProfileNameOrShortenedPubKey(pubKey: string): string {
const conversation = ConversationController.getInstance().get(pubKey);
if (!conversation) {
return pubKey;
}
return conversation.getContactProfileNameOrShortenedPubKey();
}
public getContactProfileNameOrFullPubKey(pubKey: string): string {
const conversation = this.conversations.get(pubKey);
if (!conversation) {
return pubKey;
}
return conversation.getContactProfileNameOrFullPubKey();
}
public isMediumGroup(hexEncodedGroupPublicKey: string): boolean {
const convo = this.conversations.get(hexEncodedGroupPublicKey);
if (convo) {
return convo.isMediumGroup();
}
return false;
}
public async getOrCreateAndWait(id: any, type: string) {
const initialPromise = this._initialPromise !== undefined ? this._initialPromise : Promise.resolve();
return initialPromise.then(() => {
if (!id) {
return Promise.reject(
new Error('getOrCreateAndWait: invalid id passed.')
);
}
const pubkey = id && id.key ? id.key : id;
const conversation = this.getOrCreate(pubkey, type);
if (conversation) {
return conversation.initialPromise.then(() => conversation);
}
return Promise.reject(
new Error('getOrCreateAndWait: did not get conversation')
);
});
}
public async getAllGroupsInvolvingId(id: String) {
const groups = await window.Signal.Data.getAllGroupsInvolvingId(id, {
ConversationCollection: window.Whisper.ConversationCollection,
});
return groups.map((group: any) => this.conversations.add(group));
}
public async deleteContact(id: string) {
if (typeof id !== 'string') {
throw new TypeError("'id' must be a string");
}
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
const conversation = this.conversations.get(id);
if (!conversation) {
return;
}
// Close group leaving
if (conversation.isClosedGroup()) {
await conversation.leaveGroup();
} else if (conversation.isPublic()) {
const channelAPI = await conversation.getPublicSendData();
if (channelAPI === null) {
window.log.warn(`Could not get API for public conversation ${id}`);
} else {
channelAPI.serverAPI.partChannel(channelAPI.channelId);
}
} else if (conversation.isPrivate()) {
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
id
);
await Promise.all(
deviceIds.map((deviceId: string) => {
const address = new window.libsignal.SignalProtocolAddress(
id,
deviceId
);
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address
);
return sessionCipher.deleteAllSessionsForDevice();
})
);
}
await conversation.destroyMessages();
await window.Signal.Data.removeConversation(id, {
Conversation: window.Whisper.Conversation,
});
conversation.off('change', this.updateReduxConvoChanged);
this.conversations.remove(conversation);
if (window.inboxStore) {
window.inboxStore.dispatch(
window.actionsCreators.conversationRemoved(conversation.id)
);
}
}
public getConversations(): Array<ConversationModel> {
return Array.from(this.conversations.models.values());
}
public async load() {
window.log.info('ConversationController: starting initial fetch');
if (this.conversations.length) {
throw new Error('ConversationController: Already loaded!');
}
const load = async () => {
try {
const collection = await window.Signal.Data.getAllConversations({
ConversationCollection: window.Whisper.ConversationCollection,
});
this.conversations.add(collection.models);
this._initialFetchComplete = true;
const promises: any = [];
this.conversations.forEach((conversation: ConversationModel) => {
if (!conversation.get('lastMessage')) {
// tslint:disable-next-line: no-void-expression
promises.push(conversation.updateLastMessage());
}
promises.concat([
conversation.updateProfileName(),
conversation.updateProfileAvatar(),
]);
});
this.conversations.forEach((conversation: ConversationModel) => {
// register for change event on each conversation, and forward to redux
conversation.on('change', this.updateReduxConvoChanged);
});
await Promise.all(promises);
// Remove any unused images
window.profileImages.removeImagesNotInArray(
this.conversations.map((c: any) => c.id)
);
window.log.info('ConversationController: done with initial fetch');
} catch (error) {
window.log.error(
'ConversationController: initial fetch failed',
error && error.stack ? error.stack : error
);
throw error;
}
};
await BlockedNumberController.load();
this._initialPromise = load();
return this._initialPromise;
}
public loadPromise() {
return this._initialPromise;
}
public reset() {
this._initialPromise = Promise.resolve();
this._initialFetchComplete = false;
if (window.inboxStore) {
this.conversations.forEach((convo: ConversationModel) =>
convo.off('change', this.updateReduxConvoChanged)
);
window.inboxStore.dispatch(
window.actionsCreators.removeAllConversations()
);
}
this.conversations.reset([]);
}
private updateReduxConvoChanged(convo: ConversationModel) {
if (window.inboxStore) {
window.inboxStore.dispatch(
window.actionsCreators.conversationChanged(convo.id, convo.getProps())
);
}
}
}

View File

@ -0,0 +1,3 @@
import { ConversationController } from './ConversationController';
export { ConversationController };

View File

@ -1,4 +1,5 @@
import * as Messages from './messages';
import * as Conversations from './conversations';
import * as Protocols from './protocols';
import * as Types from './types';
import * as Utils from './utils';
@ -8,4 +9,13 @@ import * as MediumGroup from './medium_group';
export * from './instance';
export { Messages, Utils, Protocols, Types, Sending, Constants, MediumGroup };
export {
Conversations,
Messages,
Utils,
Protocols,
Types,
Sending,
Constants,
MediumGroup,
};

View File

@ -28,6 +28,7 @@ import { ConversationModel } from '../../../js/models/conversations';
import { MediumGroupUpdateMessage } from '../messages/outgoing/content/data/mediumgroup/MediumGroupUpdateMessage';
import uuid from 'uuid';
import { BlockedNumberController } from '../../util/blockedNumberController';
import { ConversationController } from '../conversations';
export {
createSenderKeyForGroup,
@ -63,7 +64,7 @@ export async function createMediumGroup(
groupName: string,
members: Array<string>
) {
const { ConversationController, libsignal } = window;
const { libsignal } = window;
// ***** 1. Create group parameters *****
@ -92,7 +93,7 @@ export async function createMediumGroup(
// ***** 2. Send group update message *****
const convo = await ConversationController.getOrCreateAndWait(
const convo = await ConversationController.getInstance().getOrCreateAndWait(
groupId,
'group'
);
@ -142,7 +143,7 @@ export async function createLegacyGroup(
groupName: string,
members: Array<string>
) {
const { ConversationController, libsignal } = window;
const { libsignal } = window;
const keypair = await libsignal.KeyHelper.generateIdentityKeyPair();
const groupId = toHex(keypair.pubKey);
@ -163,7 +164,7 @@ export async function createLegacyGroup(
await updateOrCreateGroup(groupDetails);
const convo = await ConversationController.getOrCreateAndWait(
const convo = await ConversationController.getInstance().getOrCreateAndWait(
groupId,
'group'
);
@ -187,7 +188,6 @@ export async function createLegacyGroup(
}
export async function leaveMediumGroup(groupId: string) {
const { ConversationController } = window;
// NOTE: we should probably remove sender keys for groupId,
// and its secret key, but it is low priority
@ -195,7 +195,7 @@ export async function leaveMediumGroup(groupId: string) {
window.SwarmPolling.removePubkey(groupId);
// TODO audric: we just left a group, we have to regenerate our senderkey
const maybeConvo = ConversationController.get(groupId);
const maybeConvo = ConversationController.getInstance().get(groupId);
if (!maybeConvo) {
window.log.error('Cannot leave non-existing group');
@ -360,9 +360,7 @@ export async function initiateGroupUpdate(
members: Array<string>,
avatar: any
) {
const { ConversationController } = window;
const convo = await ConversationController.getOrCreateAndWait(
const convo = await ConversationController.getInstance().getOrCreateAndWait(
groupId,
'group'
);
@ -470,8 +468,7 @@ async function sendToMembers(
message: MediumGroupMessage,
dbMessage: MessageModel
) {
const { ConversationController } = window;
const convo = await ConversationController.getOrCreateAndWait(
const convo = await ConversationController.getInstance().getOrCreateAndWait(
groupId,
'group'
);
@ -801,7 +798,7 @@ export async function updateOrCreateGroupFromSync(details: GroupInfo) {
// update conversation model
async function updateOrCreateGroup(details: GroupInfo) {
const { ConversationController, libloki, storage, textsecure } = window;
const { libloki, textsecure } = window;
const { id } = details;
@ -812,7 +809,7 @@ async function updateOrCreateGroup(details: GroupInfo) {
details
);
const conversation = await ConversationController.getOrCreateAndWait(
const conversation = await ConversationController.getInstance().getOrCreateAndWait(
id,
'group'
);

View File

@ -1,6 +1,7 @@
// You can see MessageController for in memory registered messages.
// Ee register messages to it everytime we send one, so that when an event happens we can find which message it was based on this id.
import { ConversationModel } from '../../../js/models/conversations';
import { MessageModel } from '../../../js/models/messages';
type MessageControllerEntry = {
@ -70,4 +71,19 @@ export class MessageController {
public get(identifier: string) {
return this.messageLookup.get(identifier);
}
public async getMessagesByKeyFromDb(key: string) {
// loadLive gets messages live, not from the database which can lag behind.
let messages = [];
const messageSet = await window.Signal.Data.getMessagesByConversation(key, {
limit: 100,
MessageCollection: window.Whisper.MessageCollection,
});
messages = messageSet.models.map(
(conv: ConversationModel) => conv.attributes
);
return messages;
}
}

View File

@ -7,6 +7,7 @@ import _ from 'lodash';
import * as Data from '../../../js/modules/data';
import { StringUtils } from '../../session/utils';
import { ConversationController } from '../conversations/ConversationController';
type PubkeyToHash = { [key: string]: string };
@ -147,7 +148,7 @@ export class SwarmPolling {
private loadGroupIds() {
// Start polling for medium size groups as well (they might be in different swarms)
const convos = window
const convos = ConversationController.getInstance()
.getConversations()
.filter((c: any) => c.isMediumGroup());

View File

@ -1,4 +1,5 @@
import { ConversationModel } from '../../../js/models/conversations';
import { ConversationController } from '../conversations';
import { PromiseUtils } from '../utils';
interface OpenGroupParams {
@ -160,7 +161,7 @@ export class OpenGroup {
const conversationId = `publicChat:${channelId}@${rawServerURL}`;
// Quickly peak to make sure we don't already have it
return window.ConversationController.get(conversationId);
return ConversationController.getInstance().get(conversationId);
}
/**

View File

@ -1,11 +1,14 @@
import _ from 'lodash';
import { PrimaryPubKey, PubKey } from '../types';
import { MultiDeviceProtocol } from '../protocols';
import { ConversationController } from '../conversations';
export async function getGroupMembers(
groupId: PubKey
): Promise<Array<PrimaryPubKey>> {
const groupConversation = window.ConversationController.get(groupId.key);
const groupConversation = ConversationController.getInstance().get(
groupId.key
);
const groupMembers = groupConversation
? groupConversation.attributes.members
: undefined;
@ -23,7 +26,7 @@ export async function getGroupMembers(
}
export function isMediumGroup(groupId: PubKey): boolean {
const conversation = window.ConversationController.get(groupId.key);
const conversation = ConversationController.getInstance().get(groupId.key);
if (!conversation) {
return false;

View File

@ -9,6 +9,7 @@ import {
SentSyncMessage,
} from '../messages/outgoing';
import { PubKey } from '../types';
import { ConversationController } from '../conversations';
export function getSentSyncMessage(params: {
message: ContentMessage;
@ -64,7 +65,7 @@ export async function getSyncContacts(): Promise<Array<any> | undefined> {
);
const secondaryContactsPromise = secondaryContactsPartial.map(async c =>
window.ConversationController.getOrCreateAndWait(
ConversationController.getInstance().getOrCreateAndWait(
c.getPrimaryDevicePubKey(),
'private'
)

View File

@ -3,6 +3,7 @@ import _, { omit } from 'lodash';
import { Constants } from '../../session';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { MessageModel } from '../../../js/models/messages';
import { ConversationController } from '../../session/conversations';
// State
@ -92,7 +93,9 @@ async function getMessages(
conversationKey: string,
numMessages: number
): Promise<Array<MessageTypeInConvo>> {
const conversation = window.ConversationController.get(conversationKey);
const conversation = ConversationController.getInstance().get(
conversationKey
);
if (!conversation) {
// no valid conversation, early return
window.log.error('Failed to get convo on reducer.');

View File

@ -6,6 +6,7 @@ import { TestUtils } from '../../../test-utils';
import { UserUtil } from '../../../../util';
import { MultiDeviceProtocol } from '../../../../session/protocols';
import { SyncMessage } from '../../../../session/messages/outgoing';
import { ConversationController } from '../../../../session/conversations';
// tslint:disable-next-line: no-require-imports no-var-requires
const chaiAsPromised = require('chai-as-promised');
@ -17,7 +18,7 @@ describe('Sync Message Utils', () => {
describe('getSyncContacts', () => {
let getAllConversationsStub: sinon.SinonStub;
let getOrCreateAndWaitStub: sinon.SinonStub;
let getOrCreatAndWaitItem: any;
let getOrCreateAndWaitItem: any;
// Fill half with secondaries, half with primaries
const numConversations = 20;
@ -57,7 +58,7 @@ describe('Sync Message Utils', () => {
secondaryConversations[getOrCreateAndWaitStub.callCount - 1];
// Make the item a primary device to match the call in SyncMessage under secondaryContactsPromise
getOrCreatAndWaitItem = {
getOrCreateAndWaitItem = {
...item,
getPrimaryDevicePubKey: () => item.id,
attributes: {
@ -65,12 +66,12 @@ describe('Sync Message Utils', () => {
},
};
return getOrCreatAndWaitItem;
return getOrCreateAndWaitItem;
});
TestUtils.stubWindow('ConversationController', {
getOrCreateAndWait: getOrCreateAndWaitStub,
});
sandbox
.stub(ConversationController.getInstance(), 'getOrCreateAndWait')
.resolves(getOrCreateAndWaitStub);
// Stubs
sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber);
@ -94,20 +95,5 @@ describe('Sync Message Utils', () => {
expect(contacts).to.have.length(numConversations / 2);
expect(contacts?.find(c => c.attributes.secondaryStatus)).to.not.exist;
});
it('can get sync contacts of assorted primaries and secondaries', async () => {
// Map secondary contacts to stub resolution
const contacts = await SyncMessageUtils.getSyncContacts();
expect(getAllConversationsStub.callCount).to.equal(1);
// We should have numConversations unique contacts
expect(contacts).to.have.length(numConversations);
// All contacts should be primary; half of which some from secondaries in secondaryContactsPromise
expect(contacts?.find(c => c.attributes.secondaryStatus)).to.not.exist;
expect(contacts?.filter(c => c.isPrimary)).to.have.length(
numConversations / 2
);
});
});
});

View File

@ -81,6 +81,7 @@ export class MockConversation {
this.attributes = {
id: this.id,
name: '',
type: '',
members,
left: false,
expireTimer: dayInSeconds,

View File

@ -1,4 +1,5 @@
import { ConversationModel } from '../../js/models/conversations';
import { ConversationController } from '../session/conversations/ConversationController';
// tslint:disable: no-unnecessary-class
export class FindMember {
@ -9,9 +10,11 @@ export class FindMember {
): Promise<ConversationModel | null> {
let groupMembers;
const groupConvos = window.getConversations().models.filter((d: any) => {
return !d.isPrivate();
});
const groupConvos = ConversationController.getInstance()
.getConversations()
.filter((d: any) => {
return !d.isPrivate();
});
const thisConvo = groupConvos.find((d: any) => {
return d.id === convoId;
});
@ -29,14 +32,16 @@ export class FindMember {
const publicMembers = await window.lokiPublicChatAPI.getListOfMembers();
const memberConversations = publicMembers
.map(publicMember =>
window.ConversationController.get(publicMember.authorPhoneNumber)
ConversationController.getInstance().get(
publicMember.authorPhoneNumber
)
)
.filter((c: any) => !!c);
groupMembers = memberConversations;
} else {
const privateConvos = window
const privateConvos = ConversationController.getInstance()
.getConversations()
.models.filter((d: any) => d.isPrivate());
.filter((d: any) => d.isPrivate());
const members = thisConvo.attributes.members;
if (!members) {
return null;

View File

@ -75,22 +75,6 @@
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-load(",
"path": "js/conversation_controller.js",
"line": " async load() {",
"lineNumber": 178,
"reasonCategory": "falseMatch",
"updated": "2018-10-02T21:00:44.007Z"
},
{
"rule": "jQuery-load(",
"path": "js/conversation_controller.js",
"line": " this._initialPromise = load();",
"lineNumber": 213,
"reasonCategory": "falseMatch",
"updated": "2018-10-02T21:00:44.007Z"
},
{
"rule": "jQuery-$(",
"path": "js/debug_log_start.js",
@ -209,14 +193,6 @@
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-load(",
"path": "js/signal_protocol_store.js",
"line": " await ConversationController.load();",
"lineNumber": 868,
"reasonCategory": "falseMatch",
"updated": "2018-09-15T00:38:04.183Z"
},
{
"rule": "jQuery-wrap(",
"path": "js/util_worker_tasks.js",
@ -341,14 +317,6 @@
"updated": "2018-09-15T00:38:04.183Z",
"reasonDetail": "Getting the value, not setting it"
},
{
"rule": "jQuery-load(",
"path": "js/views/import_view.js",
"line": " return ConversationController.load()",
"lineNumber": 176,
"reasonCategory": "falseMatch",
"updated": "2018-09-15T00:38:04.183Z"
},
{
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",

6
ts/window.d.ts vendored
View File

@ -28,7 +28,6 @@ If you import anything in global.d.ts, the type system won't work correctly.
declare global {
interface Window {
CONSTANTS: any;
ConversationController: ConversationControllerType;
SignalProtocolStore: any;
Events: any;
Lodash: any;
@ -104,6 +103,10 @@ declare global {
ContactBuffer: any;
GroupBuffer: any;
SwarmPolling: SwarmPolling;
SnodePool: {
getSnodesFor: (string) => any;
};
profileImages: any;
MediaRecorder: any;
dataURLToBlobSync: any;
autoOrientImage: any;
@ -113,7 +116,6 @@ declare global {
) => Promise<{ pubKey: ArrayBufferLike; privKey: ArrayBufferLike }>;
setClockParams: any;
clientClockSynced: number | undefined;
getMessagesByKey: any;
inboxStore: Store;
getSocketStatus: any;
actionsCreators: any;