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:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,5 @@
;(function(exports){'use strict';const SERVICE_NAME='activity-service';var client;var hasPendingRequest;var ActivityClient={init(appInstanceId){if(!appInstanceId){throw new Error('AppInstanceId is required!');}
client=bridge.client({service:SERVICE_NAME,endpoint:new BroadcastChannel(`${SERVICE_NAME}-channel-${appInstanceId}`),timeout:false});hasPendingRequest=false;client.on('activity-request',(request)=>{if(hasPendingRequest){this.postError('Concurrent activity requests are not allowed');return;}
hasPendingRequest=true;this.emit(request.name+'-activity-request',request.data);});},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);},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);},hasPendingRequest(){return hasPendingRequest;}};exports.ActivityClient=Object.freeze(EventDispatcher.mixin(ActivityClient,['new-activity-request','share-activity-request']));})(window);

View 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);

View 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);

View file

@ -0,0 +1,2 @@
;(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')};const SERVICE_NAME='conversation-service';var ConversationClient={[priv.client]:null,[priv.appInstanceId]:null,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;},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);

View 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);

View file

@ -0,0 +1,12 @@
;;;(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;var Drafts={get size(){return draftIndex.size;},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)));},add:function(draft){if(!draft||!(draft instanceof Draft)){throw new Error('Draft should be valid Draft instance.');}
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;},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;}
var isDeleted=false;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;},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;},byDraftId:function(id){id=+id;for(var draft of this.getAllThreadless()){if(draft.id===id){return draft;}}
return null;},getAll:function*(){for(var[threadId,drafts]of draftIndex){yield*threadId?drafts.slice(-1):drafts;}},getAllThreadless:function(){return draftIndex.get(null)||[];},clear:function(){draftIndex=new Map();deferredDraftRequest=null;return this;},store:function(){var defer=Utils.Promise.defer();asyncStorage.setItem('draft index',[...draftIndex],()=>{defer.resolve();});return defer.promise;},request:function(force){if(deferredDraftRequest&&!force){return deferredDraftRequest.promise;}
deferredDraftRequest=Utils.Promise.defer();asyncStorage.getItem('draft index',function(records){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']);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));

View file

@ -0,0 +1,19 @@
;;;'use strict';(function(exports){var MessageManager={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));},onMessageSending:function mm_onMessageSending(e){Threads.registerMessage(e.message);this.emit('message-sending',{message:e.message});},onMessageFailed:function mm_onMessageFailed(e){this.emit('message-failed-to-send',{message:e.message});},onDeliverySuccess:function mm_onDeliverySuccess(e){this.emit('message-delivered',{message:e.message});},onReadSuccess:function mm_onReadSuccess(e){this.emit('message-read',{message:e.message});},onMessageSent:function mm_onMessageSent(e){this.emit('message-sent',{message:e.message});},onMessageReceived:function mm_onMessageReceived(e){var message=e.message;if(message.messageClass&&message.messageClass==='class-0'){return;}
if(message.delivery==='not-downloaded'&&message.deliveryInfo[0].deliveryStatus==='pending'){return;}
Threads.registerMessage(message);this.emit('message-received',{message:message});},onDeleted:function(e){if(e.deletedThreadIds&&e.deletedThreadIds.length){this.emit('threads-deleted',{ids:e.deletedThreadIds});}},getThreads:function mm_getThreads(options){var each=options.each;var end=options.end;var done=options.done;var cursor=null;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){each&&each(this.result);this.continue();return;}
end&&end();done&&done();};cursor.onerror=function onerror(){console.error('Reading the database. Error: '+this.error.name);done&&done();};},getMessage:function mm_getMsg(id){return this._mozMobileMessage.getMessage(id);},retrieveMMS:function mm_retrieveMMS(id){return this._mozMobileMessage.retrieveMMS(id);},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(shouldContinue!==false){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();};},_sanitizeServiceId:function mm_sanitizeServiceId(serviceId){if(serviceId==null||isNaN(+serviceId)){serviceId=null;}else{serviceId=+serviceId;}
return serviceId;},_getSendOptionsFromServiceId:function mm_gSOFSI(serviceId){var sendOpts;if(serviceId!=null&&Settings.hasSeveralSim()){sendOpts={serviceId:serviceId};}
return sendOpts;},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];}
var i=0;var requestResult={hasError:false,return:[]};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){onsuccess&&onsuccess(event.target.result);requestResult.return.push({success:true,result:event.target.result,recipient:recipients[idx]});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++;};});},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);};},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);};},deleteMessages:function mm_deleteMessages(id){return this._mozMobileMessage.delete(id).then(null,(e)=>{console.error(`Deleting in the database. [${e.name}] ${e.message || ''}`);return null;});},markThreadRead:function mm_markThreadRead(threadId,isRead=true){var filter={};filter.threadId=+threadId;filter.read=!isRead;var messagesUnreadIDs=[];var changeStatusOptions={each:function addUnreadMessage(message){messagesUnreadIDs.push(message.id);return true;},filter:filter,invert:true,end:function handleUnread(){MessageManager.markMessagesRead(messagesUnreadIDs,isRead);}};MessageManager.getMessages(changeStatusOptions);},markMessagesRead:function mm_markMessagesRead(list,isRead=true){if(!this._mozMobileMessage||!list.length){return;}
var id=list.pop();var req=this._mozMobileMessage.markMessageRead(id,isRead,true);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);};},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;},_isMessageBelongTo1to1Conversation:function isMessageBelongTo1to1Conversation(number,message){var isIncoming=message.delivery==='received'||message.delivery==='not-downloaded';if(isIncoming){return Utils.probablyMatches(message.sender,number);}else{switch(message.type){case'sms':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;}}},findThreadFromNumber:function mm_findThread(number){function checkCandidate(message){var isMessageInThread=MessageManager._isMessageBelongTo1to1Conversation(number,message);if(isMessageInThread){threadId=message.threadId;return false;}}
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;},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;};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);

View 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);

View 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);

View file

@ -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);

View 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));

View 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);

View 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));

View 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));

View file

@ -0,0 +1,12 @@
;;;(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)){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(){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));