Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
123
gaia/apps/sms/services/js/activity/activity_client.js
Executable file
123
gaia/apps/sms/services/js/activity/activity_client.js
Executable file
|
|
@ -0,0 +1,123 @@
|
|||
/* global bridge,
|
||||
BroadcastChannel,
|
||||
EventDispatcher
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Name of the service that is responsible for managing activities.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'activity-service';
|
||||
|
||||
/**
|
||||
* Reference to active bridge client instance. For every app instance that is
|
||||
* run as inline activity we have only one bridge service (hosted in the main
|
||||
* window context) and only one bridge client (hosted in the same context).
|
||||
* Client should only communicate with the service that holds reference to
|
||||
* the corresponding activity request. Since we can potentially have more than
|
||||
* one inline activity instance we should use unique bridge service name for
|
||||
* every app instance to avoid message collision.
|
||||
*
|
||||
* @type {Client}
|
||||
*/
|
||||
var client;
|
||||
|
||||
/**
|
||||
* Indicates if there is pending activity request.
|
||||
* @type {boolean}
|
||||
*/
|
||||
var hasPendingRequest;
|
||||
|
||||
/**
|
||||
* ActivityClient is a wrapper around bridge client connected to the bridge
|
||||
* service hosted in the context that can handle system messages. It allows
|
||||
* to access "activity" request coming from system message when consumer can't
|
||||
* receive it directly (document in a different URL or even worker).
|
||||
* @type {Object}
|
||||
*/
|
||||
var ActivityClient = {
|
||||
/**
|
||||
* Initialized activity bridge client.
|
||||
* @param {string} appInstanceId Unique identifier of app instance where
|
||||
* client resides in.
|
||||
*/
|
||||
init(appInstanceId) {
|
||||
if (!appInstanceId) {
|
||||
throw new Error('AppInstanceId is required!');
|
||||
}
|
||||
|
||||
client = bridge.client({
|
||||
service: SERVICE_NAME,
|
||||
endpoint: new BroadcastChannel(
|
||||
`${SERVICE_NAME}-channel-${appInstanceId}`
|
||||
),
|
||||
// Disable service response timeouts.
|
||||
timeout: false
|
||||
});
|
||||
hasPendingRequest = false;
|
||||
|
||||
client.on('activity-request', (request) => {
|
||||
// This case should never happen since we use "inline" activities only.
|
||||
if (hasPendingRequest) {
|
||||
this.postError('Concurrent activity requests are not allowed');
|
||||
return;
|
||||
}
|
||||
|
||||
hasPendingRequest = true;
|
||||
this.emit(request.name + '-activity-request', request.data);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Posts results to the activity caller.
|
||||
* @param {*?} result
|
||||
* @returns {Promise} Promise that resolves once operation is completed.
|
||||
*/
|
||||
postResult(result) {
|
||||
if (!hasPendingRequest) {
|
||||
return Promise.reject(
|
||||
new Error('There is no any activity request to post result to!')
|
||||
);
|
||||
}
|
||||
|
||||
hasPendingRequest = false;
|
||||
return client.method('postResult', result);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifies activity caller that exception occurred.
|
||||
* @param {*?} error
|
||||
* @returns {Promise} Promise that resolves once operation is completed.
|
||||
*/
|
||||
postError(error) {
|
||||
if (!hasPendingRequest) {
|
||||
return Promise.reject(
|
||||
new Error('There is no any activity request to post error to!')
|
||||
);
|
||||
}
|
||||
|
||||
hasPendingRequest = false;
|
||||
|
||||
console.error('Activity request was rejected with "%s" reason', error);
|
||||
|
||||
return client.method('postError', error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if client has pending activity request.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasPendingRequest() {
|
||||
return hasPendingRequest;
|
||||
}
|
||||
};
|
||||
|
||||
exports.ActivityClient = Object.freeze(
|
||||
EventDispatcher.mixin(
|
||||
ActivityClient, ['new-activity-request', 'share-activity-request']
|
||||
)
|
||||
);
|
||||
})(window);
|
||||
144
gaia/apps/sms/services/js/activity/activity_shim.js
Executable file
144
gaia/apps/sms/services/js/activity/activity_shim.js
Executable file
|
|
@ -0,0 +1,144 @@
|
|||
/* global BridgeServiceMixin,
|
||||
BroadcastChannel
|
||||
*/
|
||||
/* exported ActivityShim */
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
const priv = Object.freeze({
|
||||
request: Symbol('request'),
|
||||
setMessageHandler: Symbol('setMessageHandler'),
|
||||
|
||||
onActivityRequest: Symbol('onActivityRequest')
|
||||
});
|
||||
|
||||
/**
|
||||
* Name of the service that is responsible for managing activities.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'activity-service';
|
||||
|
||||
/**
|
||||
* Name of the system message that is leveraged by activities.
|
||||
* @type {string}
|
||||
*/
|
||||
const SYSTEM_MESSAGE_NAME = 'activity';
|
||||
|
||||
/**
|
||||
* List of methods exposed by shim.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
const METHODS = Object.freeze(['postResult', 'postError']);
|
||||
|
||||
/**
|
||||
* List of event that can be broadcasted by shim.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
const EVENTS = Object.freeze(['activity-request']);
|
||||
|
||||
/**
|
||||
* ActivityShim is a shim around "activity" system message handling code that
|
||||
* allows to access activity request data by the consumer that can't handle
|
||||
* system messages directly (document hosted in a different URl or worker) via
|
||||
* exposing bridge service with required methods.
|
||||
* @type {Object}
|
||||
*/
|
||||
var ActivityShim = {
|
||||
/**
|
||||
* Reference to currently active activity request.
|
||||
* @type {ActivityRequestHandler}
|
||||
* @private
|
||||
*/
|
||||
[priv.request]: null,
|
||||
|
||||
/**
|
||||
* Reference to mozSetMessageHandler function.
|
||||
* @type {Function}
|
||||
* @private
|
||||
*/
|
||||
[priv.setMessageHandler]: null,
|
||||
|
||||
/**
|
||||
* Initialized activity shim bridge service.
|
||||
* @param {string} appInstanceId Unique identifier of app instance where
|
||||
* shim resides in.
|
||||
*/
|
||||
init(appInstanceId, setMessageHandler) {
|
||||
if (!appInstanceId) {
|
||||
throw new Error('AppInstanceId is required!');
|
||||
}
|
||||
|
||||
this[priv.setMessageHandler] = setMessageHandler;
|
||||
|
||||
// We may consider using exports.parent as endpoint instead in patch for
|
||||
// bug 1223363, once it is unblocked.
|
||||
this.initService(
|
||||
new BroadcastChannel(`${SERVICE_NAME}-channel-${appInstanceId}`)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Method is called by bridge when a new client is connected to the service.
|
||||
* @private
|
||||
*/
|
||||
onConnected() {
|
||||
// We subscribe for the system message only when we have client connected,
|
||||
// to avoid the case when service broadcasts message, but no one listens
|
||||
// for it yet.
|
||||
this[priv.setMessageHandler](
|
||||
SYSTEM_MESSAGE_NAME, this[priv.onActivityRequest].bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Posts activity request result.
|
||||
* @param {*} result Data to post as activity request result.
|
||||
* @private
|
||||
*/
|
||||
postResult(result) {
|
||||
var request = this[priv.request];
|
||||
if (!request) {
|
||||
throw new Error('There is no any activity request to post result to!');
|
||||
}
|
||||
|
||||
request.postResult(result || { success: true });
|
||||
|
||||
this[priv.request] = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Posts activity request error.
|
||||
* @param {*} error Error to post as activity request error.
|
||||
* @private
|
||||
*/
|
||||
postError(error) {
|
||||
var request = this[priv.request];
|
||||
if (!request) {
|
||||
throw new Error('There is no any activity request to post error to!');
|
||||
}
|
||||
|
||||
request.postError(error);
|
||||
|
||||
this[priv.request] = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler that fires once app receives activity request via system message.
|
||||
* @param {ActivityRequestHandler} request Activity request.
|
||||
* @private
|
||||
*/
|
||||
[priv.onActivityRequest](request) {
|
||||
this[priv.request] = request;
|
||||
|
||||
this.broadcast('activity-request', request.source);
|
||||
}
|
||||
};
|
||||
|
||||
exports.ActivityShim = Object.seal(
|
||||
BridgeServiceMixin.mixin(
|
||||
ActivityShim,
|
||||
SERVICE_NAME,
|
||||
{ methods: METHODS, events: EVENTS }
|
||||
)
|
||||
);
|
||||
})(window);
|
||||
110
gaia/apps/sms/services/js/bridge_service_mixin.js
Executable file
110
gaia/apps/sms/services/js/bridge_service_mixin.js
Executable file
|
|
@ -0,0 +1,110 @@
|
|||
/*global bridge,
|
||||
streamService
|
||||
*/
|
||||
/*jshint esnext: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const priv = Object.freeze({
|
||||
methods: Symbol('methods'),
|
||||
streams: Symbol('streams'),
|
||||
events: Symbol('events'),
|
||||
service: Symbol('service')
|
||||
});
|
||||
|
||||
/**
|
||||
* This mixin allows to expose easily a service from an object.
|
||||
*
|
||||
* This needs the following properties to work properly:
|
||||
*
|
||||
* - SERVICE_NAME holds the service name.
|
||||
* - METHODS holds the methods that will be exposed by the service.
|
||||
* - STREAMS holds the methods that handle streams exposed by the service.
|
||||
*
|
||||
* EVENTS, METHODS and STREAMS are all optional.
|
||||
*/
|
||||
var serviceMixin = {
|
||||
/**
|
||||
* Initialize service to listen on the default context and given endpoint.
|
||||
* @param {(Iframe|Worker|MessagePort|BroadcastChannel|Window)} endpoint
|
||||
* The context/thread that service could listen for.
|
||||
*/
|
||||
initService(endpoint) {
|
||||
if (!('bridge' in self) || !('service' in bridge)) {
|
||||
importScripts('/lib/bridge/service.js');
|
||||
}
|
||||
|
||||
var service = bridge.service(this[priv.name]);
|
||||
|
||||
this[priv.methods].forEach((exposedMethod) => {
|
||||
service.method(exposedMethod, this[exposedMethod].bind(this));
|
||||
});
|
||||
|
||||
var streams = this[priv.streams];
|
||||
if (streams.length > 0) {
|
||||
if (!('streamService' in self)) {
|
||||
importScripts('/lib/bridge/plugins/stream/service.js');
|
||||
}
|
||||
|
||||
service.plugin(streamService);
|
||||
|
||||
streams.forEach((exposedStream) => {
|
||||
service.stream(exposedStream, this[exposedStream].bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
if (this.onConnected) {
|
||||
service.on('connected', this.onConnected.bind(this));
|
||||
}
|
||||
|
||||
service.listen();
|
||||
|
||||
endpoint && service.listen(endpoint);
|
||||
|
||||
this[priv.service] = service;
|
||||
},
|
||||
|
||||
/**
|
||||
* Broadcast an event to all listeners.
|
||||
* @param {String} eventName The event name to send.
|
||||
* @param {any} data Data to send with the event.
|
||||
*/
|
||||
broadcast(eventName, data) {
|
||||
if (this[priv.events].indexOf(eventName) < 0) {
|
||||
throw new Error('Event "' + eventName + '" is not allowed!');
|
||||
}
|
||||
|
||||
this[priv.service].broadcast(eventName, data);
|
||||
}
|
||||
};
|
||||
|
||||
exports.BridgeServiceMixin = {
|
||||
mixin(target, name, { methods, streams, events }) {
|
||||
if (!name) {
|
||||
throw new Error(
|
||||
'A service name is mandatory to define a service.'
|
||||
);
|
||||
}
|
||||
|
||||
Object.keys(serviceMixin).forEach(function(method) {
|
||||
if (typeof this[method] !== 'undefined') {
|
||||
throw new Error(
|
||||
'Object to mix into already has "' + method + '" property defined!'
|
||||
);
|
||||
}
|
||||
|
||||
this[method] = serviceMixin[method];
|
||||
}, target);
|
||||
|
||||
target[priv.service] = null;
|
||||
target[priv.name] = name;
|
||||
target[priv.methods] = methods || [];
|
||||
target[priv.streams] = streams || [];
|
||||
target[priv.events] = events || [];
|
||||
|
||||
return target;
|
||||
}
|
||||
};
|
||||
})(self);
|
||||
99
gaia/apps/sms/services/js/conversation/conversation_client.js
Executable file
99
gaia/apps/sms/services/js/conversation/conversation_client.js
Executable file
|
|
@ -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);
|
||||
234
gaia/apps/sms/services/js/conversation/conversation_service.js
Executable file
234
gaia/apps/sms/services/js/conversation/conversation_service.js
Executable file
|
|
@ -0,0 +1,234 @@
|
|||
/*global BridgeServiceMixin,
|
||||
Draft,
|
||||
Drafts,
|
||||
MozMobileMessageClient
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
[
|
||||
['BridgeServiceMixin', '/services/js/bridge_service_mixin.js'],
|
||||
[
|
||||
'MozMobileMessageClient',
|
||||
'/services/js/moz_mobile_message/moz_mobile_message_client.js'
|
||||
],
|
||||
['Drafts', '/services/js/drafts.js']
|
||||
].forEach(([dependencyName, dependencyPath]) => {
|
||||
if (!(dependencyName in self)) {
|
||||
importScripts(dependencyPath);
|
||||
}
|
||||
});
|
||||
|
||||
(function(exports) {
|
||||
const debug = 0 ?
|
||||
(arg1, ...args) => console.log(`[ConversationService] ${arg1}`, ...args):
|
||||
() => {};
|
||||
|
||||
// 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 SERVICE_CONTRACT = Object.freeze({
|
||||
name: 'conversation-service',
|
||||
|
||||
streams: Object.freeze(
|
||||
['getAllConversations', 'getMessagesForConversation']
|
||||
),
|
||||
|
||||
methods: Object.freeze([
|
||||
'deleteConversations', 'deleteMessages', 'markConversationsAs',
|
||||
'getConversationSummary', 'getMessage', 'findConversationFromAddress'
|
||||
]),
|
||||
|
||||
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 = {
|
||||
/**
|
||||
* Initializes our service using the Service mixin's initService.
|
||||
*/
|
||||
init() {
|
||||
this.initService();
|
||||
},
|
||||
|
||||
/**
|
||||
* Stream that returns everything needed to display conversations in the
|
||||
* inbox view.
|
||||
* @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(serviceStream, appInstanceId) {
|
||||
mark('start retrieving conversations');
|
||||
|
||||
var getThreadsStream = MozMobileMessageClient.forApp(
|
||||
appInstanceId
|
||||
).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 automatically by the Bridge library.
|
||||
* @param {Number} conversationId Conversation to retrieve.
|
||||
*/
|
||||
getMessagesForConversation(stream, conversationId) {},
|
||||
|
||||
/**
|
||||
* Returns all information related to a single conversation, to display in
|
||||
* the conversation view.
|
||||
* @param {Number} conversationId Conversation to retrieve.
|
||||
* @returns {ConversationSummary}
|
||||
*/
|
||||
getConversationSummary(conversationId) {},
|
||||
|
||||
/**
|
||||
* Deletes one or several conversations, including their messages.
|
||||
* @param {Array.<Number>} conversationIds ID of the conversations to be
|
||||
* deleted.
|
||||
*/
|
||||
deleteConversations(conversationIds) {},
|
||||
|
||||
/**
|
||||
* Deletes one or several messages.
|
||||
* @param {Array.<Number>} messageIds ID of the messages to be deleted.
|
||||
*/
|
||||
deleteMessages(messageIds) {},
|
||||
|
||||
/**
|
||||
* Marks a conversation as read or unread.
|
||||
* @param {Object} param
|
||||
* @param {Array.<Number>} param.conversations Conversation IDs to be
|
||||
* marked.
|
||||
* @param {Boolean} param.asRead Specifies whether we mark as read of
|
||||
* unread.
|
||||
*/
|
||||
markConversationsAs({conversations, asRead}) {},
|
||||
|
||||
/**
|
||||
* Fetches a message information.
|
||||
* @param {Number} messageId The message ID to retrieve.
|
||||
* @returns {Message} All message information, including contact, delivery,
|
||||
* read reports, size, timestamps, etc.
|
||||
*/
|
||||
getMessage(messageId) {},
|
||||
|
||||
/**
|
||||
* Finds a conversation from an address (that is: a phone number or an
|
||||
* email).
|
||||
* @param {String} address This is either a phone number or an email, to
|
||||
* match an existing conversation from.
|
||||
* @returns {Number?} The id of the conversation, or null if not found.
|
||||
*/
|
||||
findConversationFromAddress(address) {}
|
||||
};
|
||||
|
||||
exports.ConversationService = Object.seal(
|
||||
BridgeServiceMixin.mixin(
|
||||
ConversationService, SERVICE_CONTRACT.name, SERVICE_CONTRACT
|
||||
)
|
||||
);
|
||||
|
||||
// Automatically init service if it's run inside worker.
|
||||
if (!self.document) {
|
||||
ConversationService.init();
|
||||
}
|
||||
})(self);
|
||||
266
gaia/apps/sms/services/js/drafts.js
Executable file
266
gaia/apps/sms/services/js/drafts.js
Executable file
|
|
@ -0,0 +1,266 @@
|
|||
/*global asyncStorage,
|
||||
InterInstanceEventDispatcher,
|
||||
Utils,
|
||||
EventDispatcher
|
||||
*/
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
[
|
||||
['asyncStorage', '/shared/js/async_storage.js'],
|
||||
['EventDispatcher', '/shared/js/event_dispatcher.js'],
|
||||
['Utils', '/views/shared/js/utils.js']
|
||||
].forEach(([dependencyName, dependencyPath]) => {
|
||||
if (!(dependencyName in self)) {
|
||||
importScripts(dependencyPath);
|
||||
}
|
||||
});
|
||||
|
||||
var draftIndex = new Map();
|
||||
var deferredDraftRequest = null;
|
||||
|
||||
/**
|
||||
* Drafts
|
||||
*
|
||||
* A collection of active Draft objects, indexed by thread id.
|
||||
*/
|
||||
var Drafts = {
|
||||
/**
|
||||
* size
|
||||
*
|
||||
* Returns the number of Draft Lists.
|
||||
*
|
||||
* There is one Draft.List per Thread and
|
||||
* one Draft.List for all threadless Drafts.
|
||||
*
|
||||
* @return {Number} Maps to draftIndex size property.
|
||||
*/
|
||||
get size() {
|
||||
return draftIndex.size;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the InterInstanceEventDispatcher and relay the draft
|
||||
* saved/deleted events from another instance.
|
||||
*/
|
||||
init() {
|
||||
InterInstanceEventDispatcher.on('draft-saved', (draft) =>
|
||||
this.request(true).then(() => this.emit('saved', draft))
|
||||
);
|
||||
|
||||
InterInstanceEventDispatcher.on('draft-deleted', (draft) =>
|
||||
this.request(true).then(() => this.emit('deleted', draft))
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Pushes a Draft instance to the Drafts in-memory collection and commits
|
||||
* it to the persistent storage.
|
||||
* @param {Draft} draft Draft instance.
|
||||
* @returns {Drafts} Returns the Drafts object.
|
||||
*/
|
||||
add: function(draft) {
|
||||
if (!draft || !(draft instanceof Draft)) {
|
||||
throw new Error('Draft should be valid Draft instance.');
|
||||
}
|
||||
|
||||
// Remove existing draft.
|
||||
this.delete(draft);
|
||||
|
||||
var drafts = draftIndex.get(draft.threadId) || [];
|
||||
drafts.push(draft);
|
||||
draftIndex.set(draft.threadId, drafts);
|
||||
|
||||
this.emit('saved', draft);
|
||||
InterInstanceEventDispatcher.emit('draft-saved', draft);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a draft record from Drafts in-memory collection.
|
||||
* @param {Draft} draft Draft instance to delete.
|
||||
* @returns {Drafts} Returns the Drafts object.
|
||||
*/
|
||||
delete: function(draft) {
|
||||
if (!draft || !(draft instanceof Draft)) {
|
||||
throw new Error('Draft should be valid Draft instance.');
|
||||
}
|
||||
|
||||
var drafts = draftIndex.get(draft.threadId);
|
||||
if (!drafts || !drafts.length) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Flag to track that draft was really removed.
|
||||
var isDeleted = false;
|
||||
|
||||
// For drafts bound to thread we should remove all drafts we have, but for
|
||||
// thread-less drafts we should remove draft with the same id only.
|
||||
if (draft.threadId) {
|
||||
drafts.length = 0;
|
||||
isDeleted = true;
|
||||
} else {
|
||||
var indexToRemove = drafts.findIndex(
|
||||
(storedDraft) => storedDraft.id === draft.id
|
||||
);
|
||||
|
||||
if (indexToRemove > -1) {
|
||||
isDeleted = true;
|
||||
drafts.splice(indexToRemove, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (isDeleted) {
|
||||
this.emit('deleted', draft);
|
||||
InterInstanceEventDispatcher.emit('draft-deleted', draft);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns draft for the specified thread id or null if it doesn't exist.
|
||||
* @param {Number} threadId Thread id to recall draft for.
|
||||
* @returns {Draft}
|
||||
*/
|
||||
byThreadId: function(threadId) {
|
||||
if (!threadId) {
|
||||
throw new Error('Thread Id is not defined');
|
||||
}
|
||||
|
||||
var threadDrafts = draftIndex.get(threadId);
|
||||
return threadDrafts && threadDrafts.length ?
|
||||
threadDrafts[threadDrafts.length - 1] : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the draft object with the specified id.
|
||||
* @param {Number|String} id Id of the draft to return.
|
||||
* @returns {Draft} Found draft instance or null if nothing is found.
|
||||
*/
|
||||
byDraftId: function(id) {
|
||||
// Ensure a number is used for the comparison, as this value may come from
|
||||
// a dataset property.
|
||||
id = +id;
|
||||
|
||||
for (var draft of this.getAllThreadless()) {
|
||||
if (draft.id === id) {
|
||||
return draft;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns iterator for all available drafts. For the drafts associated with
|
||||
* the thread, only the latest draft is returned.
|
||||
*/
|
||||
getAll: function* () {
|
||||
for (var [threadId, drafts] of draftIndex) {
|
||||
// For drafts with associated thread we consider only the latest draft.
|
||||
yield* threadId ? drafts.slice(-1) : drafts;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns list of all thread less drafts.
|
||||
* @returns {Array.<Draft>}
|
||||
*/
|
||||
getAllThreadless: function () {
|
||||
return draftIndex.get(null) || [];
|
||||
},
|
||||
|
||||
/**
|
||||
* clear
|
||||
*
|
||||
* Delete drafts from the map.
|
||||
*
|
||||
* @return {Drafts} return the Drafts object.
|
||||
*/
|
||||
clear: function() {
|
||||
draftIndex = new Map();
|
||||
deferredDraftRequest = null;
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores drafts that are held in memory to local storage.
|
||||
*/
|
||||
store: function() {
|
||||
var defer = Utils.Promise.defer();
|
||||
|
||||
asyncStorage.setItem('draft index', [...draftIndex], () => {
|
||||
defer.resolve();
|
||||
});
|
||||
|
||||
return defer.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests drafts from asyncStorage or in-memory cache. Result is cached.
|
||||
* @param {boolean?} force Indicates whether we should respect already
|
||||
* cached result or _force_ asyncStorage request once again.
|
||||
* @returns {Promise} Promise that resolves when all drafts are retrieved
|
||||
* from persistent storage.
|
||||
*/
|
||||
request: function(force) {
|
||||
// Loading from storage only happens once or when specifically requested
|
||||
// with force parameter.
|
||||
if (deferredDraftRequest && !force) {
|
||||
return deferredDraftRequest.promise;
|
||||
}
|
||||
|
||||
deferredDraftRequest = Utils.Promise.defer();
|
||||
|
||||
asyncStorage.getItem('draft index', function (records) {
|
||||
// Convert every plain JS draft object into Draft "class" instance.
|
||||
// record[0] is the threadId or null key, record[1] is the array of
|
||||
// draft objects associated with that threadId or null key.
|
||||
records && records.forEach((record) => {
|
||||
record[1] = record[1].map((draft) => new Draft(draft));
|
||||
});
|
||||
draftIndex = new Map(records || []);
|
||||
|
||||
deferredDraftRequest.resolve();
|
||||
});
|
||||
|
||||
return deferredDraftRequest.promise;
|
||||
}
|
||||
};
|
||||
|
||||
exports.Drafts = EventDispatcher.mixin(Drafts, ['saved', 'deleted']);
|
||||
|
||||
/**
|
||||
* Draft
|
||||
*
|
||||
* A message-like object containing unsent
|
||||
* message content to be stored temporarily
|
||||
* in a Drafts collection.
|
||||
*
|
||||
* @param {Object} draft Draft or empty object.
|
||||
*/
|
||||
function Draft(opts) {
|
||||
var draft = opts || {};
|
||||
|
||||
if (draft.id && typeof draft.id !== 'number') {
|
||||
throw new Error('Draft id must be a number');
|
||||
}
|
||||
|
||||
this.id = draft.id || guid();
|
||||
this.recipients = draft.recipients || [];
|
||||
this.content = draft.content || [];
|
||||
this.subject = draft.subject || '';
|
||||
this.timestamp = +draft.timestamp || Date.now();
|
||||
this.threadId = draft.threadId || null;
|
||||
this.type = draft.type;
|
||||
this.isEdited = false;
|
||||
}
|
||||
|
||||
function guid() {
|
||||
return +(Date.now() + '' + (Math.random() * 1000 | 0));
|
||||
}
|
||||
|
||||
exports.Draft = Draft;
|
||||
}(this));
|
||||
715
gaia/apps/sms/services/js/message_manager.js
Executable file
715
gaia/apps/sms/services/js/message_manager.js
Executable file
|
|
@ -0,0 +1,715 @@
|
|||
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
|
||||
/* global EventDispatcher,
|
||||
Promise,
|
||||
Settings,
|
||||
SMIL,
|
||||
Threads,
|
||||
Utils
|
||||
*/
|
||||
|
||||
/*exported MessageManager */
|
||||
|
||||
/**
|
||||
* MessageManager wraps around {@link http://mdn.io/MozMobileMessageManager}
|
||||
* that makes access to SMS/MMS related operations (sending, handling
|
||||
* read/delivery reports etc.) and respective events easier.
|
||||
* @module MessageManager
|
||||
*/
|
||||
'use strict';
|
||||
(function(exports) {
|
||||
var MessageManager = {
|
||||
|
||||
/**
|
||||
* Initializes MessageManager and subscribes for required
|
||||
* {@link http://mdn.io/MozMobileMessageManager} events.
|
||||
* Should be always called before calling any other MessageManager methods.
|
||||
*/
|
||||
init: function mm_init() {
|
||||
this._mozMobileMessage = navigator.mozMobileMessage;
|
||||
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'received', this.onMessageReceived.bind(this)
|
||||
);
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'sending', this.onMessageSending.bind(this)
|
||||
);
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'sent', this.onMessageSent.bind(this)
|
||||
);
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'failed', this.onMessageFailed.bind(this)
|
||||
);
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'readsuccess', this.onReadSuccess.bind(this)
|
||||
);
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'deliverysuccess', this.onDeliverySuccess.bind(this)
|
||||
);
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'deleted', this.onDeleted.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Event occurs when an SMS or MMS enters the send flow.
|
||||
* @param {MozSmsEvent|MozMmsEvent} e Instance of
|
||||
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onMessageSending: function mm_onMessageSending(e) {
|
||||
Threads.registerMessage(e.message);
|
||||
|
||||
this.emit('message-sending', { message: e.message });
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies an event listener to receive "failed" events. These events occur
|
||||
* when sending an MMS or SMS has failed.
|
||||
* @param {MozSmsEvent|MozMmsEvent} e Instance of
|
||||
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onMessageFailed: function mm_onMessageFailed(e) {
|
||||
this.emit('message-failed-to-send', { message: e.message });
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies an event listener to receive "deliverysuccess" events. These
|
||||
* events occur when an SMS or MMS is delivered.
|
||||
* @param {MozSmsEvent|MozMmsEvent} e Instance of
|
||||
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onDeliverySuccess: function mm_onDeliverySuccess(e) {
|
||||
this.emit('message-delivered', { message: e.message });
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies an event listener to receive "read" events. These events
|
||||
* occur when MMS is read by recipient (read status isn't supported by SMS).
|
||||
* @param {MozMmsEvent} e Instance of {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onReadSuccess: function mm_onReadSuccess(e) {
|
||||
this.emit('message-read', { message: e.message });
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies an event listener to receive "sent" events. These events occur
|
||||
* when an SMS or MMS is sent.
|
||||
* @param {MozSmsEvent|MozMmsEvent} e Instance of
|
||||
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onMessageSent: function mm_onMessageSent(e) {
|
||||
this.emit('message-sent', { message: e.message });
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies an event listener to receive "received" events. These events
|
||||
* occur when an SMS or MMS is received. Event handler executed when new SMS
|
||||
* or MMS is received.
|
||||
* @param {MozSmsEvent|MozMmsEvent} e Instance of
|
||||
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onMessageReceived: function mm_onMessageReceived(e) {
|
||||
var message = e.message;
|
||||
|
||||
if (message.messageClass && message.messageClass === 'class-0') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Here we can only have one sender, so deliveryInfo[0].deliveryStatus =>
|
||||
// message status from sender. Ignore 'pending' messages that are received
|
||||
// this means we are in automatic download mode.
|
||||
if (message.delivery === 'not-downloaded' &&
|
||||
message.deliveryInfo[0].deliveryStatus === 'pending') {
|
||||
return;
|
||||
}
|
||||
|
||||
Threads.registerMessage(message);
|
||||
|
||||
this.emit('message-received', { message: message });
|
||||
},
|
||||
|
||||
/**
|
||||
* Event handler executed when message is deleted.
|
||||
* @param {MozSmsEvent|MozMmsEvent} e Instance of
|
||||
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onDeleted: function(e) {
|
||||
// The method verifies that some threads have been deleted.
|
||||
if (e.deletedThreadIds && e.deletedThreadIds.length) {
|
||||
this.emit('threads-deleted', {
|
||||
ids: e.deletedThreadIds
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtains all the treads and takes a set of callback functions as an
|
||||
* argument.
|
||||
* @param {Object} options A set of callback functions as described below:-
|
||||
* @param {function) options.each: callback function invoked for each message.
|
||||
* @param {function} options.end: callback function run when cursor is "done".
|
||||
* @param {function} options.done: callback function run when we iterated
|
||||
* through all the threads or consumer requested to do so.
|
||||
*/
|
||||
getThreads: function mm_getThreads(options) {
|
||||
|
||||
var each = options.each;
|
||||
var end = options.end;
|
||||
var done = options.done;
|
||||
var cursor = null;
|
||||
|
||||
// WORKAROUND for bug 958738. We can remove 'try\catch' block once this bug
|
||||
// is resolved.
|
||||
try {
|
||||
cursor = this._mozMobileMessage.getThreads();
|
||||
} catch(e) {
|
||||
console.error('Error occurred while retrieving threads:', e.name, e);
|
||||
end && end();
|
||||
done && done();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.onsuccess = function onsuccess() {
|
||||
if (this.result) {
|
||||
// Invoking callback on each thread.
|
||||
each && each(this.result);
|
||||
|
||||
this.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoking terminating callbacks.
|
||||
end && end();
|
||||
done && done();
|
||||
};
|
||||
|
||||
cursor.onerror = function onerror() {
|
||||
console.error('Reading the database. Error: ' + this.error.name);
|
||||
done && done();
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Method used to access message having a given id.
|
||||
* @param {number} id The id of the message we wish to get.
|
||||
* @return {DOMRequest<SmsMessage|MmsMessage>}
|
||||
*/
|
||||
getMessage: function mm_getMsg(id) {
|
||||
return this._mozMobileMessage.getMessage(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Method to get the MMS object having a given id.
|
||||
* @param {number} id The id of the MMS we wish to get.
|
||||
* @return {DOMRequest<MmsMessage>}
|
||||
*/
|
||||
retrieveMMS: function mm_retrieveMMS(id) {
|
||||
return this._mozMobileMessage.retrieveMMS(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtains all the messages using a given filter.
|
||||
* @param {Object} options A set of callback functions as described below:-
|
||||
* @param {function} options.each callback function invoked for each message.
|
||||
* @param {function} options.end callback function run when cursor is "done".
|
||||
* @param {function} options.endArgs specify arguments for the "end" callback.
|
||||
* @param {function} options.done callback function invoked when we iterated
|
||||
* through all the messages or consumer requested to do so.
|
||||
* @param {function} options.filter a MobileMessageFilter or similar object.
|
||||
* @param {boolean} invert option to invert the selection.
|
||||
*/
|
||||
getMessages: function mm_getMgs(options) {
|
||||
var each = options.each;
|
||||
var invert = options.invert;
|
||||
var end = options.end;
|
||||
var endArgs = options.endArgs;
|
||||
var done = options.done;
|
||||
var filter = options.filter;
|
||||
var cursor = this._mozMobileMessage.getMessages(filter, !invert);
|
||||
|
||||
cursor.onsuccess = function onsuccess() {
|
||||
if (!this.done) {
|
||||
var shouldContinue = true;
|
||||
if (each) {
|
||||
shouldContinue = each(this.result);
|
||||
}
|
||||
// if each returns false the iteration stops.
|
||||
if (shouldContinue !== false) { // if this is undefined this is fine.
|
||||
this.continue();
|
||||
} else {
|
||||
done && done();
|
||||
}
|
||||
} else {
|
||||
end && end(endArgs);
|
||||
done && done();
|
||||
}
|
||||
};
|
||||
cursor.onerror = function onerror() {
|
||||
var msg = 'Reading the database. Error: ' + this.error.name;
|
||||
console.log(msg);
|
||||
done && done();
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Sanitizes the "serviceId". It works as follows:-
|
||||
* 0 is a valid value so we need to take care to not consider it as a falsy
|
||||
* value. We want to return null for anything that's not a number or a string
|
||||
* containing a number.
|
||||
* @param {number|string} id The service ID.
|
||||
* @private
|
||||
* @return {?number}
|
||||
*/
|
||||
_sanitizeServiceId: function mm_sanitizeServiceId(serviceId) {
|
||||
if (serviceId == null || // null or undefined
|
||||
isNaN(+serviceId)) {
|
||||
serviceId = null;
|
||||
} else {
|
||||
serviceId = +serviceId;
|
||||
}
|
||||
|
||||
return serviceId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets send options for the specified "serviceId".
|
||||
* @param {number|string} id The service ID.
|
||||
* @private
|
||||
* @return {{ serviceId: number|string }}
|
||||
*/
|
||||
_getSendOptionsFromServiceId: function mm_gSOFSI(serviceId) {
|
||||
var sendOpts;
|
||||
|
||||
if (serviceId != null && // not null, not undefined
|
||||
Settings.hasSeveralSim()) {
|
||||
sendOpts = { serviceId: serviceId };
|
||||
}
|
||||
|
||||
return sendOpts;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends an SMS based on the specified parameters object
|
||||
* @param {Object} opts Options object that contains :-
|
||||
* - recipients (string or array of string): contains the list of recipients
|
||||
* for this message.
|
||||
* - content (string): the message's body.
|
||||
* - serviceId (optional long or string): the SIM serviceId we use to send the
|
||||
* message.
|
||||
* - onsuccess (optional function): will be called when one SMS has been
|
||||
* sent successfully, with the request's result as argument. Can be called
|
||||
* several times.
|
||||
* - onerror (optional function): will be called when one SMS transmission
|
||||
* failed, with the error object as argument. Can be called several times.
|
||||
* - oncomplete (optional function): will be called when all messages have
|
||||
* been sent. It's argument will have the following properties:
|
||||
* + hasError (boolean): whether we had at least one error
|
||||
* + return (array): each item is an object with the following properties:
|
||||
* . success (boolean): whether this is a success or an error.
|
||||
* . result (request's result): the request's result object.
|
||||
* . recipient (string): the recipient used for this transmission.
|
||||
*/
|
||||
sendSMS: function mm_send(opts) {
|
||||
var recipients = opts.recipients || [],
|
||||
content = opts.content,
|
||||
serviceId = this._sanitizeServiceId(opts.serviceId),
|
||||
onsuccess = opts.onsuccess,
|
||||
onerror = opts.onerror,
|
||||
oncomplete = opts.oncomplete;
|
||||
|
||||
if (!Array.isArray(recipients)) {
|
||||
recipients = [recipients];
|
||||
}
|
||||
|
||||
// The returned value is not a DOM request!
|
||||
// Instead, It's an array of DOM requests.
|
||||
var i = 0;
|
||||
var requestResult = { hasError: false, return: [] };
|
||||
|
||||
// Getting serviceId field in sendOpts.
|
||||
var sendOpts = this._getSendOptionsFromServiceId(serviceId);
|
||||
|
||||
var requests = this._mozMobileMessage.send(recipients, content, sendOpts);
|
||||
|
||||
var numberOfRequests = requests.length;
|
||||
|
||||
requests.forEach(function(request, idx) {
|
||||
request.onsuccess = function onSuccess(event) {
|
||||
// Callback functions executed message has been successfully sent.
|
||||
onsuccess && onsuccess(event.target.result);
|
||||
|
||||
requestResult.return.push({
|
||||
success: true,
|
||||
result: event.target.result,
|
||||
recipient: recipients[idx]
|
||||
});
|
||||
|
||||
// Last callback, oncomplete, executed at the end of the loop.
|
||||
if (i === numberOfRequests - 1) {
|
||||
oncomplete && oncomplete(requestResult);
|
||||
}
|
||||
i++;
|
||||
};
|
||||
|
||||
request.onerror = function onError(event) {
|
||||
console.error('Error Sending: ' + JSON.stringify(event.target.error));
|
||||
onerror && onerror(event.target.error);
|
||||
|
||||
requestResult.hasError = true;
|
||||
requestResult.return.push({
|
||||
success: false,
|
||||
code: event.target.error,
|
||||
recipient: recipients[idx]
|
||||
});
|
||||
|
||||
if (i === numberOfRequests - 1) {
|
||||
oncomplete && oncomplete(requestResult);
|
||||
}
|
||||
i++;
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends an MMS based on the specified parameters object
|
||||
* @param {Object} opts is the options field with the following properties:
|
||||
* - recipients (string or array of string): recipients for this message
|
||||
* - subject (optional string): subject for this message
|
||||
* - content (array of SMIL slides): this is the content for the message (see
|
||||
* ConversationView for more information)
|
||||
* - serviceId (optional long or string): the SIM that should be used for
|
||||
* sending this message. If this is not the current default configuration
|
||||
* for sending MMS, then we'll first switch the configuration to this
|
||||
* serviceId, and only then send the message. That means that the "sending"
|
||||
* event will come quite late in this case.
|
||||
* - onsuccess (optional func): called only once, even for several recipients,
|
||||
* when the message is successfully sent.
|
||||
* - onerror (optional func): called only once if there is an error.
|
||||
*/
|
||||
sendMMS: function mm_sendMMS(opts) {
|
||||
var request;
|
||||
var recipients = opts.recipients,
|
||||
subject = opts.subject,
|
||||
content = opts.content,
|
||||
serviceId = opts.serviceId = this._sanitizeServiceId(opts.serviceId),
|
||||
onsuccess = opts.onsuccess,
|
||||
onerror = opts.onerror;
|
||||
|
||||
if (!Array.isArray(recipients)) {
|
||||
recipients = [recipients];
|
||||
}
|
||||
|
||||
var message = SMIL.generate(content);
|
||||
|
||||
var sendOpts = this._getSendOptionsFromServiceId(serviceId);
|
||||
|
||||
request = this._mozMobileMessage.sendMMS({
|
||||
receivers: recipients,
|
||||
subject: subject,
|
||||
smil: message.smil,
|
||||
attachments: message.attachments
|
||||
}, sendOpts);
|
||||
|
||||
request.onsuccess = function onSuccess(event) {
|
||||
onsuccess && onsuccess(event.target.result);
|
||||
};
|
||||
|
||||
request.onerror = function onError(event) {
|
||||
onerror && onerror(event.target.error);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Resends message with the given options.
|
||||
* @param {Object} opts Options is the object with the following properties:-
|
||||
* - message (object): the message object containing all the information
|
||||
* depending on the type of the message which can be 'sms' or 'mms'.
|
||||
* - onsuccess (optional function): will be called when the message has been
|
||||
* sent successfully, with the request's result as argument. Can be called
|
||||
* several times.
|
||||
* - onerror (optional function): will be called when the message transmission
|
||||
* failed, with the error object as argument. Can be called several times.
|
||||
*/
|
||||
resendMessage: function mm_resendMessage(opts) {
|
||||
var message = opts.message;
|
||||
|
||||
if (!message) {
|
||||
throw new Error('Message to resend is not defined.');
|
||||
}
|
||||
|
||||
var serviceId = Settings.getServiceIdByIccId(message.iccId);
|
||||
var sendOpts = this._getSendOptionsFromServiceId(serviceId);
|
||||
var onsuccess = opts.onsuccess;
|
||||
var onerror = opts.onerror;
|
||||
var request;
|
||||
|
||||
if (message.type === 'sms') {
|
||||
request = this._mozMobileMessage.send(
|
||||
message.receiver, message.body, sendOpts);
|
||||
}
|
||||
if (message.type === 'mms') {
|
||||
request = this._mozMobileMessage.sendMMS({
|
||||
receivers: message.receivers,
|
||||
subject: message.subject,
|
||||
smil: message.smil,
|
||||
attachments: message.attachments
|
||||
}, sendOpts);
|
||||
}
|
||||
|
||||
request.onsuccess = function onSuccess(evt) {
|
||||
MessageManager.deleteMessages(message.id);
|
||||
onsuccess && onsuccess(evt.target.result);
|
||||
};
|
||||
|
||||
request.onerror = function onError(evt) {
|
||||
MessageManager.deleteMessages(message.id);
|
||||
onerror && onerror(evt.target.error);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes messages using the message id provided.
|
||||
* @param {number} id The ID of the message to be deleted.
|
||||
* @return {Promise}
|
||||
*/
|
||||
deleteMessages: function mm_deleteMessages(id) {
|
||||
// As DOMRequest doesn't have "catch" method we should use "then" with
|
||||
// "null" for the success callback to handle reject case only.
|
||||
return this._mozMobileMessage.delete(id).then(null, (e) => {
|
||||
// TODO: If the messages could not be deleted completely, conversation
|
||||
// view will also update without notification currently. May need more
|
||||
// information for user that the messages were not removed completely.
|
||||
// See bug #1045666 for details.
|
||||
console.error(`Deleting in the database. [${e.name}] ${e.message || ''}`);
|
||||
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Marks a thread as read or unread depending on value of isRead as true or
|
||||
* false respectively.
|
||||
* @param {number|string} threadId The ID of the thread to be marked as read.
|
||||
* @param {boolean} isRead If "true" then message is to be marked as read
|
||||
* otherwise message is to be marked as unread.
|
||||
*/
|
||||
markThreadRead: function mm_markThreadRead(threadId, isRead = true) {
|
||||
// Create filter object to mark all unread messages of the specified thread.
|
||||
var filter = {};
|
||||
filter.threadId = +threadId;
|
||||
filter.read = !isRead;
|
||||
|
||||
var messagesUnreadIDs = [];
|
||||
|
||||
// Building the options field in getMessages() with above filter.
|
||||
var changeStatusOptions = {
|
||||
each: function addUnreadMessage(message) {
|
||||
// First build up array of Unread message ID.
|
||||
messagesUnreadIDs.push(message.id);
|
||||
return true;
|
||||
},
|
||||
filter: filter,
|
||||
invert: true,
|
||||
end: function handleUnread() {
|
||||
// end with marking each of them as read.
|
||||
MessageManager.markMessagesRead(messagesUnreadIDs, isRead);
|
||||
}
|
||||
};
|
||||
MessageManager.getMessages(changeStatusOptions);
|
||||
},
|
||||
|
||||
/**
|
||||
* Marks messages with specified ids as read.
|
||||
* @param {Array<Number>} list List of all the message IDs to be changed to
|
||||
* read.
|
||||
* @param {boolean} isRead Pass true if message is to be marked as read and
|
||||
* false if message is to be marked as unread.
|
||||
*/
|
||||
markMessagesRead: function mm_markMessagesRead(list, isRead = true) {
|
||||
if (!this._mozMobileMessage || !list.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We chain the calls to the API in a way that we make no call to
|
||||
// 'markMessageRead' until a previous call is completed. This way any
|
||||
// other potential call to the API, like the one for getting a message
|
||||
// list, could be done within the calls to mark the messages as read.
|
||||
|
||||
var id = list.pop();
|
||||
// TODO: Third parameter of markMessageRead is return read request.
|
||||
// Here we always return read request for now, but we can let user
|
||||
// decide to return request or not in Bug 971658.
|
||||
var req = this._mozMobileMessage.markMessageRead(id, isRead, true);
|
||||
// isRead == false i.e mark as unread case, marking only one message
|
||||
// as unread is sufficient.
|
||||
req.onsuccess = (function onsuccess() {
|
||||
if (!list.length || !isRead) {
|
||||
return;
|
||||
} else if (isRead) {
|
||||
this.markMessagesRead(list, isRead);
|
||||
}
|
||||
}).bind(this);
|
||||
|
||||
req.onerror = function onerror() {
|
||||
console.error(
|
||||
'Error while marking message %d as read: %s', id, this.error.name
|
||||
);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Method used to get the information necessary to create a multipart SMS for
|
||||
* a given text.
|
||||
* @param {string} text The text used to create the SMS.
|
||||
* @return {Promise<MozSmsSegmentInfo>} Instance of
|
||||
* {@link http://mdn.io/MozSmsSegmentInfo}
|
||||
*/
|
||||
getSegmentInfo: function mm_getSegmentInfo(text) {
|
||||
if (!(this._mozMobileMessage &&
|
||||
this._mozMobileMessage.getSegmentInfoForText)) {
|
||||
return Promise.reject(new Error('mozMobileMessage is unavailable.'));
|
||||
}
|
||||
|
||||
var defer = Utils.Promise.defer();
|
||||
|
||||
var request = this._mozMobileMessage.getSegmentInfoForText(text);
|
||||
request.onsuccess = function onsuccess(e) {
|
||||
defer.resolve(e.target.result);
|
||||
};
|
||||
|
||||
request.onerror = function onerror(e) {
|
||||
defer.reject(e.target.error);
|
||||
};
|
||||
|
||||
return defer.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* This method checks whether message is received from a sender with specified
|
||||
* number or sent to this particular number. (in case of MMS only 1-to-1,
|
||||
* non-group MMS's are counted).
|
||||
* It's used to differentiate from broadcast messages ---> group MMS.
|
||||
* @param {number} number Phone number.
|
||||
* @param {Object} message A composite object having details about message
|
||||
* delivery, type, sender and receiver.
|
||||
* @private
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isMessageBelongTo1to1Conversation:
|
||||
function isMessageBelongTo1to1Conversation(number, message) {
|
||||
var isIncoming = message.delivery === 'received' ||
|
||||
message.delivery === 'not-downloaded';
|
||||
// if it is a received message, it is a candidate
|
||||
// we still need to test the sender in case the user filters with his own
|
||||
// number, because we would get all the received messages in this case.
|
||||
if (isIncoming) {
|
||||
return Utils.probablyMatches(message.sender, number);
|
||||
} else {
|
||||
switch (message.type) {
|
||||
case 'sms':
|
||||
// in case of sent messages and sms, we test if the receiver match the
|
||||
// filter, to filter out other sent messages in the case user is
|
||||
// sending message to himself
|
||||
return Utils.probablyMatches(message.receiver, number);
|
||||
case 'mms':
|
||||
return message.receivers.length === 1 &&
|
||||
Utils.probablyMatches(message.receivers[0], number);
|
||||
default:
|
||||
console.error('Got an unknown message type: ' + message.type);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a SMS/MMS thread from a number.
|
||||
* @param {number} number Phone number to find thread for.
|
||||
* @return {Promise<number>} that is resolved to a threadId or rejected if
|
||||
* not found
|
||||
*/
|
||||
findThreadFromNumber: function mm_findThread(number) {
|
||||
|
||||
function checkCandidate(message) {
|
||||
var isMessageInThread = MessageManager.
|
||||
_isMessageBelongTo1to1Conversation(number, message);
|
||||
if (isMessageInThread) {
|
||||
threadId = message.threadId;
|
||||
return false; // found the message, stop iterating
|
||||
}
|
||||
}
|
||||
|
||||
var threadId = null;
|
||||
var deferred = Utils.Promise.defer();
|
||||
|
||||
MessageManager.getMessages({
|
||||
filter: { numbers: [number] },
|
||||
each: checkCandidate,
|
||||
done: function() {
|
||||
if (threadId == null) {
|
||||
deferred.reject(new Error('No thread found for number: ' + number));
|
||||
return;
|
||||
} else {
|
||||
deferred.resolve(threadId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* This method ensures that threads have been registered in the in-memory
|
||||
* cache. In the case it has, the promise returned resolves immediately. If
|
||||
* not, it loads the last message from the thread and creates a new entry in
|
||||
* cache based on it. It can happen when application loaded from non default
|
||||
* page and Thread cache hasn't been populated.
|
||||
* @param {number|string} threadId The ID of the thread we wish to register.
|
||||
* @return {Promise}
|
||||
*/
|
||||
ensureThreadRegistered(threadId) {
|
||||
var getOneMessageForThread = (threadId) => {
|
||||
var defer = Utils.Promise.defer();
|
||||
this.getMessages({
|
||||
each: function(message) {
|
||||
defer.resolve(message);
|
||||
return false;
|
||||
},
|
||||
done: function() {
|
||||
defer.reject(new Error('found no message'));
|
||||
},
|
||||
filter: { threadId: threadId }
|
||||
});
|
||||
return defer.promise;
|
||||
};
|
||||
|
||||
// Checking whether Thread cache has this thread.
|
||||
if (Threads.has(threadId)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return getOneMessageForThread(threadId).then(
|
||||
(message) => Threads.registerMessage(message)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(exports, 'MessageManager', {
|
||||
get: function () {
|
||||
delete exports.MessageManager;
|
||||
|
||||
exports.MessageManager = EventDispatcher.mixin(MessageManager, [
|
||||
'message-sending', 'message-failed-to-send', 'message-delivered',
|
||||
'message-read', 'message-sent', 'message-received', 'threads-deleted'
|
||||
]);
|
||||
|
||||
return exports.MessageManager;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
})(window);
|
||||
125
gaia/apps/sms/services/js/messaging/messaging_client.js
Executable file
125
gaia/apps/sms/services/js/messaging/messaging_client.js
Executable file
|
|
@ -0,0 +1,125 @@
|
|||
/* global bridge */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Name of the service that is responsible for messaging functionality.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'messaging-service';
|
||||
/**
|
||||
* Disable default client timeout from bridge by setting the timeout to false.
|
||||
* @type {number|boolean}
|
||||
*/
|
||||
const TIMEOUT = false;
|
||||
|
||||
/**
|
||||
* Reference to active bridge client instance.
|
||||
* @type {Client}
|
||||
*/
|
||||
var client;
|
||||
|
||||
/**
|
||||
* Unique identifier of app instance where client resides in.
|
||||
* @type {string}
|
||||
*/
|
||||
var appInstanceId;
|
||||
|
||||
var MessagingClient = {
|
||||
/**
|
||||
* Initializes connection to MessagingService hosted in a SharedWorker.
|
||||
* @param {string} applicationInstanceId Unique identifier of app instance
|
||||
* where client resides in.
|
||||
*/
|
||||
init(applicationInstanceId) {
|
||||
if (!applicationInstanceId) {
|
||||
throw new Error('AppInstanceId is required!');
|
||||
}
|
||||
|
||||
var serviceEndpoint = new SharedWorker(
|
||||
'/services/js/messaging/messaging_service.js'
|
||||
);
|
||||
|
||||
appInstanceId = applicationInstanceId;
|
||||
|
||||
client = bridge.client({
|
||||
service: SERVICE_NAME,
|
||||
endpoint: serviceEndpoint,
|
||||
timeout: TIMEOUT
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends an SMS based on the specified parameter object.
|
||||
* @param {Object} options Options object that contains :-
|
||||
* - recipients (string or array of string): contains the list of
|
||||
* recipients for this message
|
||||
* - content (string): the message's body
|
||||
* - serviceId (optional long or string): the SIM serviceId we use to send
|
||||
* the message
|
||||
* - onsuccess (optional function): will be called when one SMS has been
|
||||
* sent successfully, with the request's result as argument. Can be called
|
||||
* several times.
|
||||
* - onerror (optional function): will be called when one SMS transmission
|
||||
* failed, with the error object as argument. Can be called several times.
|
||||
* - oncomplete (optional function): will be called when all messages have
|
||||
* been sent. Its argument will have the following properties:
|
||||
* + hasError (boolean): whether we had at least one error
|
||||
* + return (array): each item is an object with the following properties:
|
||||
* . success (boolean): whether this is a success or an error
|
||||
* . result (request's result): the request's result object
|
||||
* . recipient (string): the recipient used for this transmission.
|
||||
* @return {Promise}
|
||||
*/
|
||||
sendSMS(options) {
|
||||
return client.method('sendSMS', options, appInstanceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends an MMS based on the specified parameter object.
|
||||
* @param {Object} options Options object is the options field with the
|
||||
* following properties:
|
||||
* - recipients (string or array of string): recipients for this message.
|
||||
* - subject (optional string): subject for this message.
|
||||
* - content (array of SMIL slides): this is the content for the message
|
||||
* (see ConversationView for more information).
|
||||
* - serviceId (optional long or string): the SIM that should be used for
|
||||
* sending this message. If this is not the current default configuration
|
||||
* for sending MMS, then we'll first switch the configuration to this
|
||||
* serviceId, and only then send the message. That means that the
|
||||
* "sending" event will come quite late in this case.
|
||||
* - onsuccess (optional func): called only once, even for several
|
||||
* recipients, when the message is successfully sent.
|
||||
* - onerror (optional func): called only once if there is an error.
|
||||
* @return {Promise}
|
||||
*/
|
||||
sendMMS(options) {
|
||||
return client.method('sendMMS', options, appInstanceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resends message with the given opts.
|
||||
* Takes a formatted message in case you happen to have one.
|
||||
* @param {Object} message The message object containing all the information
|
||||
* depending on the type of the message which can be 'sms' or 'mms'.
|
||||
* @return {Promise}
|
||||
*/
|
||||
resendMessage(message) {
|
||||
return client.method('resendMessage', message, appInstanceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the MMS object having a given id.
|
||||
* @param {number} id The id of the MMS we wish to get.
|
||||
* @return {Promise<MmsMessage>} A Promise object that handles the success
|
||||
* or error of the operation.
|
||||
*/
|
||||
retrieveMMS(id) {
|
||||
return client.method('retrieveMMS', id, appInstanceId);
|
||||
}
|
||||
};
|
||||
|
||||
exports.MessagingClient = Object.freeze(MessagingClient);
|
||||
|
||||
})(window);
|
||||
197
gaia/apps/sms/services/js/messaging/messaging_service.js
Executable file
197
gaia/apps/sms/services/js/messaging/messaging_service.js
Executable file
|
|
@ -0,0 +1,197 @@
|
|||
/* global bridge,
|
||||
BridgeServiceMixin,
|
||||
MozMobileMessageClient,
|
||||
Settings,
|
||||
SMIL
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
if (!('BridgeServiceMixin' in self)) {
|
||||
importScripts('/services/js/bridge_service_mixin.js');
|
||||
}
|
||||
|
||||
if (!('MozMobileMessageClient' in self)) {
|
||||
importScripts('/services/js/moz_mobile_message/moz_mobile_message_client.js');
|
||||
}
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const SERVICE_NAME = 'messaging-service';
|
||||
|
||||
const METHODS = Object.freeze([
|
||||
'sendSMS', 'sendMMS', 'resendMessage', 'retrieveMMS'
|
||||
]);
|
||||
|
||||
function ensureBridge() {
|
||||
if (!('bridge' in self) || !('service' in bridge)) {
|
||||
importScripts('/lib/bridge/service.js');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 0 is a valid value so we need to take care to not consider it as a falsy
|
||||
* value. We want to return null for anything that's not a number or a
|
||||
* string containing a number.
|
||||
*/
|
||||
function sanitizeServiceId(serviceId) {
|
||||
if (serviceId == null || // null or undefined
|
||||
isNaN(+serviceId)) {
|
||||
serviceId = null;
|
||||
} else {
|
||||
serviceId = +serviceId;
|
||||
}
|
||||
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
function getSendOptionsFromServiceId(serviceId) {
|
||||
var sendOpts;
|
||||
|
||||
if (serviceId != null && // not null, not undefined
|
||||
Settings.hasSeveralSim()) {
|
||||
sendOpts = { serviceId: serviceId };
|
||||
}
|
||||
|
||||
return sendOpts;
|
||||
}
|
||||
|
||||
var MessagingService = {
|
||||
/**
|
||||
* Initializes our service using the Service mixin's initService and
|
||||
* requests mozMobileMessage shim as client.
|
||||
*/
|
||||
init() {
|
||||
ensureBridge();
|
||||
|
||||
this.initService();
|
||||
},
|
||||
|
||||
/**
|
||||
* Send sms by using send API shim.
|
||||
* @param {String|String[]} recipients Contains the list of recipients for
|
||||
* this message.
|
||||
* @param {String} content The message's body.
|
||||
* @param {Number|String} serviceId The SIM serviceId we use to send the
|
||||
* message.
|
||||
* @param {appInstanceId} appInstanceId Unique id of the app instance
|
||||
* requesting service.
|
||||
* @returns {Promise} Promise with array of the request result.
|
||||
*/
|
||||
sendSMS({ recipients, content, serviceId }, appInstanceId) {
|
||||
recipients = recipients || [];
|
||||
serviceId = sanitizeServiceId(serviceId);
|
||||
|
||||
if (!Array.isArray(recipients)) {
|
||||
recipients = [recipients];
|
||||
}
|
||||
|
||||
var sendOpts = getSendOptionsFromServiceId(serviceId);
|
||||
var mobileMessageClient = MozMobileMessageClient.forApp(appInstanceId);
|
||||
|
||||
var results = recipients.map((recipient) => {
|
||||
return mobileMessageClient.send(recipient, content, sendOpts).then(
|
||||
(message) => ({ message: message }), (error) => ({ error: error })
|
||||
);
|
||||
});
|
||||
|
||||
return Promise.all(results).then((values) =>
|
||||
values.map((result, idx) => ({
|
||||
success: !result.error,
|
||||
result: result.message,
|
||||
code: result.error,
|
||||
recipient: recipients[idx]
|
||||
}))
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Send mms by using sendMMS API shim.
|
||||
* @param {String|String[]} recipients Recipients for this message.
|
||||
* @param {String} subject Subject for this message.
|
||||
* @param {Object[]} content This is the content for the message (
|
||||
* see ConversationView for more information).
|
||||
* @param {Number|String} serviceId The SIM that should be used for
|
||||
* sending this message. If this is not the current default configuration
|
||||
* for sending MMS, then we'll first switch the configuration to this
|
||||
* serviceId, and only then send the message. That means that the
|
||||
* "sending" event will come quite late in this case.
|
||||
* @param {appInstanceId} appInstanceId Unique id of the app instance
|
||||
* requesting service.
|
||||
* @returns {Promise} DOMRequest promise with success result or error.
|
||||
*/
|
||||
sendMMS({ recipients, content, subject, serviceId }, appInstanceId) {
|
||||
serviceId = sanitizeServiceId(serviceId);
|
||||
|
||||
if (!Array.isArray(recipients)) {
|
||||
recipients = [recipients];
|
||||
}
|
||||
|
||||
var message = SMIL.generate(content);
|
||||
|
||||
var sendOpts = getSendOptionsFromServiceId(serviceId);
|
||||
|
||||
return MozMobileMessageClient.forApp(appInstanceId).sendMMS({
|
||||
receivers: recipients,
|
||||
subject: subject,
|
||||
smil: message.smil,
|
||||
attachments: message.attachments
|
||||
}, sendOpts);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resend a message by send API shim with existing message information.
|
||||
* @param {Message} message Target message that need to be resent.
|
||||
* @param {appInstanceId} appInstanceId Unique id of the app instance
|
||||
* requesting service.
|
||||
* @returns {Promise} DOMRequest promise with success result or error.
|
||||
*/
|
||||
resendMessage(message, appInstanceId) {
|
||||
if (!message) {
|
||||
throw new Error('Message to resend is not defined.');
|
||||
}
|
||||
|
||||
var serviceId = Settings.getServiceIdByIccId(message.iccId);
|
||||
var sendOpts = getSendOptionsFromServiceId(serviceId);
|
||||
var request;
|
||||
|
||||
var mobileMessageClient = MozMobileMessageClient.forApp(appInstanceId);
|
||||
|
||||
if (message.type === 'sms') {
|
||||
request = mobileMessageClient.send(
|
||||
message.receiver, message.body, sendOpts
|
||||
);
|
||||
} else if (message.type === 'mms') {
|
||||
request = mobileMessageClient.sendMMS({
|
||||
receivers: message.receivers,
|
||||
subject: message.subject,
|
||||
smil: message.smil,
|
||||
attachments: message.attachments
|
||||
}, sendOpts);
|
||||
}
|
||||
|
||||
return mobileMessageClient.delete(message.id).then(() => request);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a MMS by retrieveMMS API shim by message ID.
|
||||
* @param {Number} id mms message id for retrieval.
|
||||
* @param {appInstanceId} appInstanceId Unique id of the app instance
|
||||
* requesting service.
|
||||
* @returns {Promise} DOMRequest promise with success result or error.
|
||||
*/
|
||||
retrieveMMS(id, appInstanceId) {
|
||||
return MozMobileMessageClient.forApp(appInstanceId).retrieveMMS(id);
|
||||
}
|
||||
};
|
||||
|
||||
exports.MessagingService = BridgeServiceMixin.mixin(
|
||||
MessagingService,
|
||||
SERVICE_NAME,
|
||||
{ methods: METHODS }
|
||||
);
|
||||
|
||||
if (!self.document) {
|
||||
MessagingService.init();
|
||||
}
|
||||
|
||||
})(self);
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/* global bridge,
|
||||
BroadcastChannel */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Name of the service that is responsible for messaging functionality.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'moz-mobile-connections-shim';
|
||||
|
||||
/**
|
||||
* Disable default client timeout from bridge by setting the timeout to false.
|
||||
* @type {number|boolean}
|
||||
*/
|
||||
const TIMEOUT = false;
|
||||
|
||||
/**
|
||||
* Reference to active bridge client instance.
|
||||
*
|
||||
* @type {Client}
|
||||
*/
|
||||
var client;
|
||||
|
||||
var MozMobileConnectionsClient = {
|
||||
/**
|
||||
* Initializes connection to mozMobileConnectionsShim hosted in iframe.
|
||||
* @param {string} appInstanceId Unique identifier of app instance
|
||||
* where client resides in.
|
||||
*/
|
||||
init(appInstanceId) {
|
||||
if (!appInstanceId) {
|
||||
throw new Error('AppInstanceId is required!');
|
||||
}
|
||||
|
||||
var broadcastChannelName = `${SERVICE_NAME}-channel-${appInstanceId}`;
|
||||
|
||||
client = bridge.client({
|
||||
service: SERVICE_NAME,
|
||||
endpoint: new BroadcastChannel(broadcastChannelName),
|
||||
timeout: TIMEOUT
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Switch the default MMS and data to SIM that matches the given iccId.
|
||||
* @param {string} iccId Id that could map to the target SIM.
|
||||
* @returns {Promise} Promise of shim's method that returned when sim
|
||||
* switching is ready or failed.
|
||||
*/
|
||||
switchMmsSimHandler(iccId) {
|
||||
return client.method('switchMmsSimHandler', iccId);
|
||||
}
|
||||
};
|
||||
|
||||
exports.MozMobileConnectionsClient = Object.freeze(
|
||||
MozMobileConnectionsClient
|
||||
);
|
||||
|
||||
})(window);
|
||||
106
gaia/apps/sms/services/js/moz_mobile_connections/moz_mobile_connections_shim.js
Executable file
106
gaia/apps/sms/services/js/moz_mobile_connections/moz_mobile_connections_shim.js
Executable file
|
|
@ -0,0 +1,106 @@
|
|||
/* global BridgeServiceMixin,
|
||||
BroadcastChannel,
|
||||
MozSettingsShim
|
||||
*/
|
||||
|
||||
/* exported MozMobileConnectionsShim */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Name of the service for mozMobileConnections API shim.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'moz-mobile-connections-shim';
|
||||
|
||||
/**
|
||||
* Array of method names that need to be exposed for API shim.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
const METHODS = Object.freeze(['getServiceIdByIccId', 'switchMmsSimHandler']);
|
||||
|
||||
var mozMobileConnections = null;
|
||||
|
||||
function setMmsSimServiceId(id) {
|
||||
// DSDS: mms & data are both necessary for connection switch.
|
||||
MozSettingsShim.set({
|
||||
'ril.mms.defaultServiceId': id,
|
||||
'ril.data.defaultServiceId': id
|
||||
});
|
||||
}
|
||||
|
||||
var MozMobileConnectionsShim = {
|
||||
init(appInstanceId, conns) {
|
||||
if (!conns) {
|
||||
console.error('Invalid mozMobileConnections for shim initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
mozMobileConnections = conns;
|
||||
|
||||
this.initService(
|
||||
new BroadcastChannel(`${SERVICE_NAME}-channel-${appInstanceId}`)
|
||||
);
|
||||
},
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* Shim for returning the service id that matches the icc id.
|
||||
* @param {string} iccId Id that could map to the target SIM.
|
||||
* @returns {?number} matched service id or null if unmatched.
|
||||
*/
|
||||
getServiceIdByIccId(iccId) {
|
||||
var index = Array.prototype.findIndex.call(mozMobileConnections, (conn) =>
|
||||
iccId === conn.iccId
|
||||
);
|
||||
|
||||
return index > -1 ? index : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Switching the default mms/data service id by icc id.
|
||||
* @param {string} iccId Id that could map to the target SIM.
|
||||
* @returns {Promise} Promise that returned when sim switching is ready or
|
||||
* failed.
|
||||
*/
|
||||
switchMmsSimHandler(iccId) {
|
||||
var serviceId = this.getServiceIdByIccId(iccId);
|
||||
|
||||
if (serviceId === null) {
|
||||
return Promise.reject('NoSimCardError');
|
||||
}
|
||||
|
||||
var conn = mozMobileConnections[serviceId];
|
||||
|
||||
if (conn.data.state === 'registered') {
|
||||
// Call resolve directly if state is registered already
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
// Listen to MobileConnections datachange to make sure we can start
|
||||
// to retrieve mms only when data.state is registered. But we can't
|
||||
// guarantee datachange event will work in other device.
|
||||
conn.addEventListener('datachange', function onDataChange() {
|
||||
if (conn.data.state === 'registered') {
|
||||
conn.removeEventListener('datachange', onDataChange);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
setMmsSimServiceId(serviceId);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.MozMobileConnectionsShim = Object.seal(
|
||||
BridgeServiceMixin.mixin(
|
||||
MozMobileConnectionsShim,
|
||||
SERVICE_NAME,
|
||||
{ methods: METHODS }
|
||||
)
|
||||
);
|
||||
|
||||
}(this));
|
||||
97
gaia/apps/sms/services/js/moz_mobile_message/moz_mobile_message_client.js
Executable file
97
gaia/apps/sms/services/js/moz_mobile_message/moz_mobile_message_client.js
Executable file
|
|
@ -0,0 +1,97 @@
|
|||
/*global bridge,
|
||||
BroadcastChannel,
|
||||
streamClient
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
const debug = 0 ?
|
||||
(arg1, ...args) => console.log(`[MozMobileMessageClient] ${arg1}`, ...args):
|
||||
() => {};
|
||||
|
||||
const priv = {
|
||||
bridgeClient: Symbol('bridgeClient')
|
||||
};
|
||||
|
||||
/**
|
||||
* Disable default client timeout from bridge by setting the timeout to false.
|
||||
* @type {number|boolean}
|
||||
*/
|
||||
const TIMEOUT = false;
|
||||
|
||||
function MobileMessageClient(endpoint) {
|
||||
if (!('bridge' in self) || !('client' in bridge)) {
|
||||
importScripts('/lib/bridge/client.js');
|
||||
}
|
||||
|
||||
if (!('streamClient' in self)) {
|
||||
importScripts('/lib/bridge/plugins/stream/client.js');
|
||||
}
|
||||
|
||||
this[priv.bridgeClient] = bridge.client({
|
||||
service: 'moz-mobile-message-shim',
|
||||
endpoint: endpoint,
|
||||
timeout: TIMEOUT
|
||||
}).plugin(streamClient);
|
||||
}
|
||||
|
||||
MobileMessageClient.prototype = {
|
||||
retrieveMMS(id) {
|
||||
return this[priv.bridgeClient].method('retrieveMMS', id);
|
||||
},
|
||||
|
||||
getThreads() {
|
||||
return this[priv.bridgeClient].stream('getThreads');
|
||||
},
|
||||
|
||||
send(recipient, content, sendOpts) {
|
||||
return this[priv.bridgeClient].method(
|
||||
'send', recipient, content, sendOpts
|
||||
);
|
||||
},
|
||||
|
||||
sendMMS(dataOpts, sendOpts) {
|
||||
return this[priv.bridgeClient].method('sendMMS', dataOpts, sendOpts);
|
||||
},
|
||||
|
||||
delete(id) {
|
||||
return this[priv.bridgeClient].method('delete', id);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* List of MobileMessageClient mapped to specific application id.
|
||||
* @type {Map.<string, MobileMessageClient>}
|
||||
*/
|
||||
const mobileMessageClients = new Map();
|
||||
|
||||
exports.MozMobileMessageClient = Object.freeze({
|
||||
forApp(appInstanceId) {
|
||||
if (!appInstanceId) {
|
||||
throw new Error('AppInstanceId is required!');
|
||||
}
|
||||
|
||||
var mobileMessageClient = mobileMessageClients.get(appInstanceId);
|
||||
|
||||
if (!mobileMessageClient) {
|
||||
mobileMessageClient = new MobileMessageClient(
|
||||
new BroadcastChannel(
|
||||
'moz-mobile-message-shim-channel-' + appInstanceId
|
||||
)
|
||||
);
|
||||
|
||||
mobileMessageClients.set(appInstanceId, mobileMessageClient);
|
||||
|
||||
debug(
|
||||
'Create MobileMessageClient for app instance %s', appInstanceId
|
||||
);
|
||||
}
|
||||
|
||||
return mobileMessageClient;
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
mobileMessageClients.clear();
|
||||
}
|
||||
});
|
||||
})(self);
|
||||
276
gaia/apps/sms/services/js/moz_mobile_message/moz_mobile_message_shim.js
Executable file
276
gaia/apps/sms/services/js/moz_mobile_message/moz_mobile_message_shim.js
Executable file
|
|
@ -0,0 +1,276 @@
|
|||
/* global BridgeServiceMixin,
|
||||
BroadcastChannel
|
||||
*/
|
||||
|
||||
/* exported MozMobileMessageShim */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
const debug = 0 ?
|
||||
(arg1, ...args) => console.log(`[MozMobileMessageShim] ${arg1}`, ...args):
|
||||
() => {};
|
||||
|
||||
const mark = 0 ?
|
||||
(...args) => exports.performance.mark(`[MozMobileMessageShim] ${args}`):
|
||||
() => {};
|
||||
|
||||
/**
|
||||
* Name of the service for mozMobileMessage API shim.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'moz-mobile-message-shim';
|
||||
|
||||
/**
|
||||
* Event set that key is corresponding to mozMobileMessage events and value is
|
||||
* the name that broadcasts to client side.
|
||||
* @type {Object.<string, string>}
|
||||
*/
|
||||
const EVENTS = Object.freeze({
|
||||
'received': 'message-received',
|
||||
'sending': 'message-sending',
|
||||
'sent': 'message-sent',
|
||||
'failed': 'message-failed-to-send',
|
||||
'deleted': 'threads-deleted',
|
||||
'readsuccess': 'message-read',
|
||||
'deliverysuccess': 'message-delivered'
|
||||
});
|
||||
|
||||
/**
|
||||
* Array of method names that need to be exposed for API shim.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
const METHODS = Object.freeze(['getMessage', 'retrieveMMS', 'send', 'sendMMS',
|
||||
'delete', 'markMessageRead', 'getSegmentInfoForText']);
|
||||
|
||||
/**
|
||||
* Array of stream names that need to return data (messages/threads) in chunk.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
const STREAMS = Object.freeze(['getThreads', 'getMessages']);
|
||||
|
||||
var mozMobileMessage = null;
|
||||
|
||||
/**
|
||||
* Clones DOM thread object to plain JS object, so that it's possible to pass it
|
||||
* via postMessage. Should be removed once bug 1172794 is landed.
|
||||
* @param {DOMMozMobileMessageThread} thread DOM Message thread instance.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function cloneThread(thread) {
|
||||
// See nsIDOMMozMobileMessageThread.idl file at:
|
||||
// http://mxr.mozilla.org/mozilla-central/source/dom/mobilemessage/interfaces
|
||||
return {
|
||||
id: thread.id,
|
||||
body: thread.body,
|
||||
participants: thread.participants,
|
||||
timestamp: thread.timestamp,
|
||||
unreadCount: thread.unreadCount,
|
||||
lastMessageType: thread.lastMessageType
|
||||
};
|
||||
}
|
||||
|
||||
var MozMobileMessageShim = {
|
||||
init(appInstanceId, mobileMessage) {
|
||||
if (!mobileMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
function capitalize(str) {
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
var broadcastChannelName = `${SERVICE_NAME}-channel-${appInstanceId}`;
|
||||
mozMobileMessage = mobileMessage;
|
||||
this.initService(new BroadcastChannel(broadcastChannelName));
|
||||
|
||||
Object.keys(EVENTS).forEach((event) => {
|
||||
mozMobileMessage.addEventListener(
|
||||
event,
|
||||
this['on' + capitalize(event)].bind(this)
|
||||
);
|
||||
});
|
||||
|
||||
debug(
|
||||
'Listen incoming connections on "%s" broadcast channel',
|
||||
broadcastChannelName
|
||||
);
|
||||
},
|
||||
|
||||
/* Events */
|
||||
|
||||
onSending(e) {
|
||||
this.broadcast('message-sending', { message: e.message });
|
||||
},
|
||||
|
||||
onFailed(e) {
|
||||
this.broadcast('message-failed-to-send', { message: e.message });
|
||||
},
|
||||
|
||||
onDeliverysuccess(e) {
|
||||
this.broadcast('message-delivered', { message: e.message });
|
||||
},
|
||||
|
||||
onReadsuccess(e) {
|
||||
this.broadcast('message-read', { message: e.message });
|
||||
},
|
||||
|
||||
onSent(e) {
|
||||
this.broadcast('message-sent', { message: e.message });
|
||||
},
|
||||
|
||||
onReceived(e) {
|
||||
this.broadcast('message-received', { message: e.message });
|
||||
},
|
||||
|
||||
onDeleted(e) {
|
||||
if (e.deletedThreadIds && e.deletedThreadIds.length) {
|
||||
this.broadcast('threads-deleted', {
|
||||
ids: e.deletedThreadIds
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/* Methods */
|
||||
|
||||
getMessage(id) {
|
||||
return mozMobileMessage.getMessage(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Call platform retrieveMMS API to retrieve the MMS by ID.
|
||||
* @param {Number} id MMS message id.
|
||||
* @returns {Promise.<void|Error>} return void if MMS is retrieved
|
||||
* successfully or error while failed.
|
||||
*/
|
||||
retrieveMMS(id) {
|
||||
// Return void instead of message to avoid clone error issue. Remove once
|
||||
// bug 1172794 is resolved.
|
||||
return mozMobileMessage.retrieveMMS(id).then(() => {});
|
||||
},
|
||||
|
||||
send(recipient, content, sendOpts) {
|
||||
return mozMobileMessage.send(recipient, content, sendOpts);
|
||||
},
|
||||
|
||||
sendMMS(dataOpts, sendOpts) {
|
||||
return mozMobileMessage.sendMMS(dataOpts, sendOpts);
|
||||
},
|
||||
|
||||
delete(id) {
|
||||
return mozMobileMessage.delete(id);
|
||||
},
|
||||
|
||||
markMessageRead(id, isRead, sendReadReport) {
|
||||
return mozMobileMessage.markMessageRead(id, isRead, sendReadReport);
|
||||
},
|
||||
|
||||
getSegmentInfoForText(text) {
|
||||
return mozMobileMessage.getSegmentInfoForText(text);
|
||||
},
|
||||
|
||||
/* Streams */
|
||||
|
||||
/**
|
||||
* Stream wrapper for getThreads API for returning threads in chunk.
|
||||
* @param {ServiceStream} stream Channel for returning thread.
|
||||
*/
|
||||
getThreads(stream) {
|
||||
mark('start retrieving conversations');
|
||||
|
||||
var cursor = null;
|
||||
var isStreamCancelled = false;
|
||||
|
||||
stream.cancel = (reason) => {
|
||||
debug('getThreads stream is cancelled: %s', reason);
|
||||
isStreamCancelled = true;
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// WORKAROUND for bug 958738. We can remove 'try\catch' block once this bug
|
||||
// is resolved
|
||||
try {
|
||||
cursor = mozMobileMessage.getThreads();
|
||||
} catch(e) {
|
||||
console.error('Error occurred while retrieving threads:', e);
|
||||
// Pass error object once the following issue is fixed:
|
||||
// https://github.com/gaia-components/threads/issues/74
|
||||
stream.abort(`[${e.name}] ${e.message || ''}`);
|
||||
return;
|
||||
}
|
||||
|
||||
var index = 0;
|
||||
cursor.onsuccess = function onsuccess() {
|
||||
var result = this.result;
|
||||
|
||||
if (result && !isStreamCancelled) {
|
||||
mark(++index, ' conversation retrieved');
|
||||
stream.write(cloneThread(result));
|
||||
this.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
mark('all conversations retrieved');
|
||||
|
||||
stream.close();
|
||||
};
|
||||
|
||||
cursor.onerror = function onerror() {
|
||||
console.error('Error occurred while reading the database', this.error);
|
||||
|
||||
// Pass error object once the following issue is fixed:
|
||||
// https://github.com/gaia-components/threads/issues/74
|
||||
stream.abort(`[${this.error.name}] ${this.error.message || ''}`);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Stream wrapper for getMessages API for returning messages in chunk.
|
||||
* @param {ServiceStream} stream Channel for returning message.
|
||||
* @param {*} options Options for getMessages API.
|
||||
*/
|
||||
getMessages(stream, options) {
|
||||
/*
|
||||
options {
|
||||
filter: a MobileMessageFilter or similar object
|
||||
invert: option to invert the selection
|
||||
}
|
||||
|
||||
*/
|
||||
var invert = options.invert;
|
||||
var filter = options.filter;
|
||||
var cursor = mozMobileMessage.getMessages(filter, !invert);
|
||||
|
||||
cursor.onsuccess = function onsuccess() {
|
||||
if (!this.done) {
|
||||
stream.write(this.result);
|
||||
this.continue();
|
||||
} else {
|
||||
stream.close();
|
||||
}
|
||||
};
|
||||
|
||||
cursor.onerror = function onerror() {
|
||||
var msg = 'Reading the database. Error: ' + this.error.name;
|
||||
console.error(msg);
|
||||
stream.abort();
|
||||
};
|
||||
|
||||
stream.cancel = function() {
|
||||
stream.close();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
exports.MozMobileMessageShim = Object.seal(
|
||||
BridgeServiceMixin.mixin(
|
||||
MozMobileMessageShim,
|
||||
SERVICE_NAME, {
|
||||
methods: METHODS,
|
||||
streams: STREAMS,
|
||||
events: Object.keys(EVENTS).map((key) => EVENTS[key])
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
}(this));
|
||||
59
gaia/apps/sms/services/js/moz_settings/moz_settings_shim.js
Executable file
59
gaia/apps/sms/services/js/moz_settings/moz_settings_shim.js
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
/* global BridgeServiceMixin */
|
||||
|
||||
/* exported MozSettingsShim */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Name of the service for mozSettings API shim.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'moz-settings-shim';
|
||||
|
||||
/**
|
||||
* Array of method names that need to be exposed for API shim.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
const METHODS = Object.freeze(['get', 'set']);
|
||||
|
||||
var mozSettings = null;
|
||||
|
||||
var MozSettingsShim = {
|
||||
init(settings) {
|
||||
if (!settings) {
|
||||
console.error('Invalid settings for shim initialization');
|
||||
return;
|
||||
}
|
||||
mozSettings = settings;
|
||||
|
||||
this.initService();
|
||||
},
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* Shim for mozSettings.createLock().get API.
|
||||
* @param {String} key Key name for Settings.
|
||||
* @returns {Promise} Promise that return the settings value from settings DB.
|
||||
*/
|
||||
get(key) {
|
||||
return mozSettings.createLock().get(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shim for mozSettings.createLock().set API.
|
||||
* @param {Object} settings Settings key and value for settings. It can have
|
||||
* several key/value pairs inside.
|
||||
* @returns {Promise} Promise that return saved result.
|
||||
*/
|
||||
set(settings) {
|
||||
return mozSettings.createLock().set(settings);
|
||||
}
|
||||
};
|
||||
|
||||
exports.MozSettingsShim = Object.seal(
|
||||
BridgeServiceMixin.mixin(MozSettingsShim, SERVICE_NAME, { methods: METHODS })
|
||||
);
|
||||
|
||||
}(this));
|
||||
177
gaia/apps/sms/services/js/threads.js
Executable file
177
gaia/apps/sms/services/js/threads.js
Executable file
|
|
@ -0,0 +1,177 @@
|
|||
/*global Drafts */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var threads = new Map();
|
||||
var messageMap = new Map();
|
||||
|
||||
function Thread(thread) {
|
||||
var length = Thread.FIELDS.length;
|
||||
var key;
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
key = Thread.FIELDS[i];
|
||||
this[key] = thread[key];
|
||||
}
|
||||
|
||||
this.messages = new Map();
|
||||
}
|
||||
|
||||
Thread.prototype.getDraft = function() {
|
||||
return this.isDraft ?
|
||||
Drafts.byDraftId(this.id) : Drafts.byThreadId(this.id);
|
||||
};
|
||||
|
||||
Thread.FIELDS = [
|
||||
'body', 'id', 'lastMessageSubject', 'lastMessageType',
|
||||
'participants', 'timestamp', 'unreadCount', 'isDraft'
|
||||
];
|
||||
|
||||
Thread.fromMessage = function(record, options) {
|
||||
var participants = [];
|
||||
|
||||
if (typeof record.delivery !== 'undefined') {
|
||||
if (record.delivery === 'received' ||
|
||||
record.delivery === 'not-downloaded') {
|
||||
participants = [record.sender];
|
||||
} else {
|
||||
participants = record.receivers || [record.receiver];
|
||||
}
|
||||
}
|
||||
|
||||
return new Thread({
|
||||
id: record.threadId,
|
||||
participants: participants,
|
||||
body: record.body,
|
||||
timestamp: record.timestamp,
|
||||
unreadCount: (options && options.unread) ? 1 : 0,
|
||||
lastMessageType: record.type || 'sms',
|
||||
isDraft: false
|
||||
});
|
||||
};
|
||||
|
||||
Thread.fromDraft = function(draft, options) {
|
||||
var participants = draft.recipients && draft.recipients.length ?
|
||||
draft.recipients : [''];
|
||||
|
||||
var body = draft.content && draft.content.length ?
|
||||
draft.content.find(function(content) {
|
||||
if (typeof content === 'string') {
|
||||
return true;
|
||||
}
|
||||
}) : '';
|
||||
|
||||
return new Thread({
|
||||
id: draft.threadId || draft.id,
|
||||
participants: participants,
|
||||
body: body,
|
||||
timestamp: new Date(draft.timestamp),
|
||||
unreadCount: (options && options.unread) ? 1 : 0,
|
||||
lastMessageType: draft.type || 'sms',
|
||||
isDraft: true
|
||||
});
|
||||
};
|
||||
|
||||
Thread.create = function(record, options) {
|
||||
if (record instanceof Thread) {
|
||||
return record;
|
||||
}
|
||||
return record.delivery ?
|
||||
Thread.fromMessage(record, options) :
|
||||
Thread.fromDraft(record, options);
|
||||
};
|
||||
|
||||
var Threads = exports.Threads = {
|
||||
registerMessage: function(message) {
|
||||
var threadId = +message.threadId;
|
||||
var messageId = +message.id;
|
||||
|
||||
if (!this.has(threadId)) {
|
||||
this.set(threadId, Thread.create(message));
|
||||
}
|
||||
this.get(threadId).messages.set(messageId, message);
|
||||
messageMap.set(messageId, threadId);
|
||||
},
|
||||
unregisterMessage: function(id) {
|
||||
id = +id;
|
||||
var threadId = messageMap.get(id);
|
||||
|
||||
if (this.has(threadId)) {
|
||||
this.get(threadId).messages.delete(id);
|
||||
}
|
||||
messageMap.delete(id);
|
||||
},
|
||||
set: function(id, thread) {
|
||||
var old, length, key;
|
||||
id = +id;
|
||||
if (threads.has(id)) {
|
||||
// Updates the reference
|
||||
old = threads.get(id);
|
||||
length = Thread.FIELDS.length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
key = Thread.FIELDS[i];
|
||||
old[key] = thread[key];
|
||||
}
|
||||
|
||||
return threads;
|
||||
}
|
||||
return threads.set(id, new Thread(thread));
|
||||
},
|
||||
get: function(id) {
|
||||
return threads.get(+id);
|
||||
},
|
||||
has: function(id) {
|
||||
return threads.has(+id);
|
||||
},
|
||||
delete: function(id) {
|
||||
id = +id;
|
||||
|
||||
var thread = this.get(id);
|
||||
|
||||
if (!thread) {
|
||||
return;
|
||||
}
|
||||
|
||||
threads.delete(id);
|
||||
|
||||
var draft = thread.getDraft();
|
||||
if (draft) {
|
||||
Drafts.delete(draft).store();
|
||||
}
|
||||
},
|
||||
clear: function() {
|
||||
threads = new Map();
|
||||
},
|
||||
forEach: function(callback) {
|
||||
threads.forEach(function(v, k) {
|
||||
callback(v, k);
|
||||
});
|
||||
},
|
||||
keys: function() {
|
||||
return threads.keys();
|
||||
},
|
||||
get size() {
|
||||
// support: gecko 18 - size might be a function
|
||||
if (typeof threads.size === 'function') {
|
||||
return +threads.size();
|
||||
}
|
||||
return +threads.size;
|
||||
}
|
||||
};
|
||||
|
||||
Threads.Messages = {
|
||||
get(id) {
|
||||
id = +id;
|
||||
var threadId = messageMap.get(id);
|
||||
|
||||
if (Threads.has(threadId)) {
|
||||
return Threads.get(threadId).messages.get(id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
exports.Thread = Thread;
|
||||
}(this));
|
||||
Loading…
Reference in a new issue