Skip to content

Commit

Permalink
Merge pull request mozilla-b2g#31051 from azasypkin/bug-1169576-conve…
Browse files Browse the repository at this point in the history
…rsation-service-get-threads

Bug 1169576 - [Messages][NG] Implement Conversation service: method for streaming joined threads and drafts list. r=julien
  • Loading branch information
rvandermeulen committed Aug 27, 2015
2 parents 628a4c4 + 51e8f74 commit 664bed4
Show file tree
Hide file tree
Showing 25 changed files with 1,022 additions and 73 deletions.
4 changes: 3 additions & 1 deletion apps/sms/desktop-mock/navigator_moz_mobilemessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -1262,7 +1262,9 @@
var request = {
error: null
};
var threads = messagesDb.threads.slice();
var threads = messagesDb.threads.slice().sort((threadA, threadB) => {
return threadB.timestamp - threadA.timestamp;
});
var idx = 0;
var len, continueCursor;

Expand Down
99 changes: 99 additions & 0 deletions apps/sms/services/js/conversation/conversation_client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* global bridge,
streamClient,
Thread
*/

(function(exports) {
'use strict';

const debug = 0 ?
(arg1, ...args) => console.log(`[ConversationClient] ${arg1}`, ...args):
() => {};

const mark = 0 ?
(...args) => exports.performance.mark(`[ConversationClient] ${args}`):
() => {};

const priv = {
client: Symbol('client'),
appInstanceId: Symbol('appInstanceId')
};

/**
* Name of the service that is responsible for managing conversation.
* @type {string}
*/
const SERVICE_NAME = 'conversation-service';

/**
* ConversationClient is a wrapper around bridge client connected to the
* bridge service hosted in a Shared Worker.
* @type {Object}
*/
var ConversationClient = {
/**
* Reference to active bridge client instance.
* @type {Client}
*/
[priv.client]: null,

/**
* Unique identifier of app instance where client resides in.
* @type {string}
*/
[priv.appInstanceId]: null,

/**
* Initialized conversation service client bridge.
* @param {string} appInstanceId Unique identifier of app instance where
* client resides in.
*/
init(appInstanceId) {
if (!appInstanceId) {
throw new Error('AppInstanceId is required!');
}

this[priv.client] = bridge.client({
service: SERVICE_NAME,
endpoint: new SharedWorker(
'/services/js/conversation/conversation_service.js'
),
timeout: false
}).plugin(streamClient);

this[priv.appInstanceId] = appInstanceId;
},

/**
* Retrieves all conversations and drafts.
* @param {Function} onConversationRetrieved Callback that is called for
* every retrieved conversation.
* @returns {Promise} Promise is resolved once all conversations are
* retrieved and rejected if conversation stream is closed unexpectedly.
*/
getAllConversations(onConversationRetrieved) {
mark('start retrieving conversations');

var getAllConversationsStream = this[priv.client].stream(
'getAllConversations', this[priv.appInstanceId]
);

var index = 0;
getAllConversationsStream.listen((thread) => {
mark(++index, ' conversation retrieved');
debug('Conversation with id %s is retrieved from stream', thread.id);

onConversationRetrieved(new Thread(thread));
});

return getAllConversationsStream.closed.then(() => {
mark('all conversations retrieved');
}).catch((e) => {
console.error('Conversation stream is closed unexpectedly', e);
throw e;
});
}
};

exports.ConversationClient = Object.seal(ConversationClient);
})(window);
218 changes: 195 additions & 23 deletions apps/sms/services/js/conversation/conversation_service.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,187 @@
/*global BridgeServiceMixin */
/*global bridge,
BridgeServiceMixin,
BroadcastChannel,
Draft,
Drafts,
streamClient
*/
'use strict';

if (!('BridgeServiceMixin' in self)) {
importScripts('/services/js/bridge_service_mixin.js');
}
/**
* Type description for the ConversationSummary object that is return from the
* Conversation service.
* @typedef {Object} ConversationSummary
* @property {number} id Unique identifier of the the conversation.
* @property {string} body Short content of the last message in the
* conversation.
* @property {Array.<string>} participants List of the conversation
* participants.
* @property {Date} timestamp Conversation timestamp.
* @property {{ hasUnread: boolean, hasNewError: boolean }} status Conversation
* status that indicates whether conversation has unread messages or/and failed
* messages that user hasn't seen yet.
* @property {string} lastMessageType Type of the last message in the
* conversation. Can be either 'sms' or 'mms'.
* @property {Draft} draft If conversation is draft or has draft, then this
* field contains appropriate draft information, otherwise it's null.
*/

[
['bridge', '/lib/bridge/bridge.js'],
['streamClient', '/lib/bridge/plugins/stream/client.js'],
['BridgeServiceMixin', '/services/js/bridge_service_mixin.js'],
['Drafts', '/services/js/drafts.js']
].forEach(([dependencyName, dependencyPath]) => {
if (!(dependencyName in self)) {
importScripts(dependencyPath);
}
});

(function(exports) {
const SERVICE_NAME = 'conversation-service';
const debug = 0 ?
(arg1, ...args) => console.log(`[ConversationService] ${arg1}`, ...args):
() => {};

const STREAMS = Object.freeze(
['getAllConversations', 'getMessagesForConversation']
);
// We use @-directive to override marker context that is scoped by
// SharedWorker lifetime by default, but we need it to be aligned with the
// main app context.
const mark = 0 ?
(...args) => exports.performance.mark(
`[ConversationService] ${args}@sms.gaiamobile.org`
):
() => {};

const priv = {
mobileMessageClients: Symbol('mobileMessageClients'),

getMobileMessageClient: Symbol('getMobileMessageClient')
};

const SERVICE_CONTRACT = Object.freeze({
name: 'conversation-service',

streams: Object.freeze(
['getAllConversations', 'getMessagesForConversation']
),

methods: Object.freeze([
'deleteConversations', 'deleteMessages', 'markConversationsAs',
'getConversationSummary', 'getMessage', 'findConversationFromAddress'
]),

const METHODS = Object.freeze([
'deleteConversations', 'deleteMessages', 'markConversationsAs',
'getConversationSummary', 'getMessage', 'findConversationFromAddress'
]);
events: Object.freeze(['message-change'])
});

const EVENTS = Object.freeze([
'message-change'
]);
function draftToConversationSummary(draft) {
var body = Array.isArray(draft.content) ?
draft.content.find((content) => typeof content === 'string') : '';

return {
id: +draft.id,
participants: Array.isArray(draft.recipients) ? draft.recipients : [],
body: body,
timestamp: new Date(draft.timestamp),
status: { hasUnread: false, hasNewError: false },
lastMessageType: draft.type || 'sms',
draft: new Draft(draft)
};
}

function threadToConversationSummary(thread) {
return {
id: thread.id,
participants: thread.participants,
body: thread.body,
timestamp: thread.timestamp,
status: { hasUnread: thread.unreadCount > 0, hasNewError: false },
lastMessageType: thread.lastMessageType,
draft: Drafts.byThreadId(thread.id)
};
}

var ConversationService = {
/**
* List of mobileMessageClients mapped to specific application id.
* @type {Map.<string, Client>}
*/
[priv.mobileMessageClients]: null,

/**
* Initializes our service using the Service mixin's initService.
*/
init() {
this.initService();

this[priv.mobileMessageClients] = new Map();
},

/**
* Stream that returns everything needed to display conversations in the
* inbox view.
* @param {ServiceStream.<ConversationSummary>} The stream to use in the
* implementation: it will be passed autoamtically by the Bridge library.
* @param {ServiceStream.<ConversationSummary>} serviceStream The stream to
* use in the implementation: it will be passed automatically by the Bridge
* library.
* @param {appInstanceId} appInstanceId Unique id of the app instance
* requesting service.
*/
getAllConversations(stream) {},
getAllConversations(serviceStream, appInstanceId) {
mark('start retrieving conversations');

var getThreadsStream = this[priv.getMobileMessageClient](
appInstanceId
).stream('getThreads');

// If conversation stream is cancelled we should cancel internal threads
// stream as well.
serviceStream.cancel = (reason) => {
debug('getAllConversations stream is cancelled: %s', reason);
return getThreadsStream.cancel(reason);
};

var draftsPromise = Drafts.request().then(() => {
// Return draft list that is sorted by timestamp in inverse order.
return Drafts.getAllThreadless().sort(
(draftA, draftB) => draftB.timestamp - draftA.timestamp
);
});

// Assume that we get the data in an inverse sort order.
var index = 0;
getThreadsStream.listen((thread) => {
mark(++index, ' conversation retrieved');
debug('Retrieved conversation with id %s', thread.id);

draftsPromise.then((drafts) => {
// Flush drafts that are created earlier than current thread.
while(drafts.length && drafts[0].timestamp >= thread.timestamp) {
serviceStream.write(draftToConversationSummary(drafts.shift()));
}

serviceStream.write(threadToConversationSummary(thread));
});
});

return getThreadsStream.closed.then(() => draftsPromise).then(
(drafts) => {
for(var draft of drafts) {
serviceStream.write(draftToConversationSummary(draft));
}

mark('All conversations retrieved');

serviceStream.close();
}).catch((e) => {
// Pass error object once the following issue is fixed:
// https://github.com/gaia-components/threads/issues/74
serviceStream.abort(`[${e.name}] ${e.message || ''}`);
});
},

/**
* Stream that returns messages in a conversation, with all suitable
* information to display in the conversation view.
* @param {ServiceStream.<MessageSummary>} The stream to use in the
* implementation: it will be passed autoamtically by the Bridge library.
* implementation: it will be passed automatically by the Bridge library.
* @param {Number} conversationId Conversation to retrieve.
*/
getMessagesForConversation(stream, conversationId) {},
Expand All @@ -50,6 +190,7 @@ if (!('BridgeServiceMixin' in self)) {
* Returns all information related to a single conversation, to display in
* the conversation view.
* @param {Number} conversationId Conversation to retrieve.
* @returns {ConversationSummary}
*/
getConversationSummary(conversationId) {},

Expand Down Expand Up @@ -91,14 +232,45 @@ if (!('BridgeServiceMixin' in self)) {
* match an existing conversation from.
* @returns {Number?} The id of the conversation, or null if not found.
*/
findConversationFromAddress(address) {}
findConversationFromAddress(address) {},

/**
* Returns client that serves to specified app instance, client is created
* if it's requested for the first time.
* @param {string} appInstanceId Unique identified of the app instance.
*/
[priv.getMobileMessageClient](appInstanceId) {
var mobileMessageClient = this[priv.mobileMessageClients].get(
appInstanceId
);

if (!mobileMessageClient) {
mobileMessageClient = bridge.client({
service: 'moz-mobile-message-shim',
endpoint: new BroadcastChannel(
'moz-mobile-message-shim-channel-' + appInstanceId
)
}).plugin(streamClient);

this[priv.mobileMessageClients].set(appInstanceId, mobileMessageClient);

debug(
'Create MobileMessageClient for app instance %s', appInstanceId
);
}

return mobileMessageClient;
}
};

exports.ConversationService = Object.seal(
BridgeServiceMixin.mixin(
ConversationService,
SERVICE_NAME,
{ methods: METHODS, streams: STREAMS, events: EVENTS }
ConversationService, SERVICE_CONTRACT.name, SERVICE_CONTRACT
)
);

// Automatically init service if it's run inside worker.
if (!self.document) {
ConversationService.init();
}
})(self);
Loading

0 comments on commit 664bed4

Please sign in to comment.