From 1d8c1dfeb6fd045da9f7805c7a23feec85b45c10 Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Mon, 31 Aug 2015 16:29:29 -0700 Subject: [PATCH 01/21] Added queue service and enqueue function signature --- app/services/queue/queue-service.js | 22 ++++++++++++++++++++++ app/services/queue/queue.js | 6 ++++++ 2 files changed, 28 insertions(+) create mode 100644 app/services/queue/queue-service.js create mode 100644 app/services/queue/queue.js diff --git a/app/services/queue/queue-service.js b/app/services/queue/queue-service.js new file mode 100644 index 00000000..78665bd4 --- /dev/null +++ b/app/services/queue/queue-service.js @@ -0,0 +1,22 @@ +export let serviceName = 'queue'; +export default /*@ngInject*/ function queue( + state +) { + let PAYLOAD_TYPES = { + message: 'message', + profile: 'profile' + }; + + let enqueue = function enqueue(channelId, payloadType, payload) { + + }; + + let dequeue = function dequeue() { + + }; + + return { + enqueue, + dequeue + }; +} diff --git a/app/services/queue/queue.js b/app/services/queue/queue.js new file mode 100644 index 00000000..4d36ebd4 --- /dev/null +++ b/app/services/queue/queue.js @@ -0,0 +1,6 @@ +import angular from 'angular'; + +import service, { serviceName } from './queue-service'; + +export default angular.module('toc.services.queue', []) + .factory(serviceName, service); From 2b1d76ac80a7b4cd3bdc59070613e6e77ff4f91c Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Mon, 31 Aug 2015 17:09:17 -0700 Subject: [PATCH 02/21] Renamed to buffer service --- app/services/buffer/buffer-service.js | 23 +++++++++++++++++++++++ app/services/buffer/buffer.js | 6 ++++++ app/services/queue/queue-service.js | 22 ---------------------- app/services/queue/queue.js | 6 ------ 4 files changed, 29 insertions(+), 28 deletions(-) create mode 100644 app/services/buffer/buffer-service.js create mode 100644 app/services/buffer/buffer.js delete mode 100644 app/services/queue/queue-service.js delete mode 100644 app/services/queue/queue.js diff --git a/app/services/buffer/buffer-service.js b/app/services/buffer/buffer-service.js new file mode 100644 index 00000000..2ee57474 --- /dev/null +++ b/app/services/buffer/buffer-service.js @@ -0,0 +1,23 @@ +export let serviceName = 'buffer'; +export default /*@ngInject*/ function buffer( + state +) { + let PAYLOAD_TYPES = { + message: 'message', + profile: 'profile' + }; + + let add = function add(contactId, channelId, payloadType, payload) { + + }; + + //send all messages in buffer + //call immediately if status is 1, otherwise set up listener for status + let flush = function flush() { + + }; + + return { + add + }; +} diff --git a/app/services/buffer/buffer.js b/app/services/buffer/buffer.js new file mode 100644 index 00000000..5bbb19d4 --- /dev/null +++ b/app/services/buffer/buffer.js @@ -0,0 +1,6 @@ +import angular from 'angular'; + +import service, { serviceName } from './buffer-service'; + +export default angular.module('toc.services.buffer', []) + .factory(serviceName, service); diff --git a/app/services/queue/queue-service.js b/app/services/queue/queue-service.js deleted file mode 100644 index 78665bd4..00000000 --- a/app/services/queue/queue-service.js +++ /dev/null @@ -1,22 +0,0 @@ -export let serviceName = 'queue'; -export default /*@ngInject*/ function queue( - state -) { - let PAYLOAD_TYPES = { - message: 'message', - profile: 'profile' - }; - - let enqueue = function enqueue(channelId, payloadType, payload) { - - }; - - let dequeue = function dequeue() { - - }; - - return { - enqueue, - dequeue - }; -} diff --git a/app/services/queue/queue.js b/app/services/queue/queue.js deleted file mode 100644 index 4d36ebd4..00000000 --- a/app/services/queue/queue.js +++ /dev/null @@ -1,6 +0,0 @@ -import angular from 'angular'; - -import service, { serviceName } from './queue-service'; - -export default angular.module('toc.services.queue', []) - .factory(serviceName, service); From 1e6b242ffef3215042cdb3df42a969f36ab419c1 Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Tue, 1 Sep 2015 00:01:34 -0700 Subject: [PATCH 03/21] Added a bunch more rough ideas on how to do this --- app/services/buffer/buffer-service.js | 20 ++++++++++++++++++-- app/services/contacts/contacts-service.js | 3 +++ app/services/messages/messages-service.js | 9 +++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/services/buffer/buffer-service.js b/app/services/buffer/buffer-service.js index 2ee57474..8af15877 100644 --- a/app/services/buffer/buffer-service.js +++ b/app/services/buffer/buffer-service.js @@ -7,8 +7,8 @@ export default /*@ngInject*/ function buffer( profile: 'profile' }; - let add = function add(contactId, channelId, payloadType, payload) { - + let addMessage = function addMessage(channelId, messageId) { + }; //send all messages in buffer @@ -17,6 +17,22 @@ export default /*@ngInject*/ function buffer( }; + let initialize = function initialize() { + let messagesCursor = state.cloud.buffer; + + R.forEach((channelId) => { + let messageChannelCursor = messagesCursor.select(channelId); + + let startSendingMessage = + + state.addListener(messageChannelCursor, ) + })(R.keys(messagesCursor.get())); + + R.pipe( + R.map + )(messageChannels); + }; + return { add }; diff --git a/app/services/contacts/contacts-service.js b/app/services/contacts/contacts-service.js index 2ccc7a65..97ec2c78 100644 --- a/app/services/contacts/contacts-service.js +++ b/app/services/contacts/contacts-service.js @@ -7,6 +7,9 @@ export default /*@ngInject*/ function contacts( R, state ) { + //TODO: add sentLatestProfile flag to each contact + // reset when profile updates + let invite = function inviteContact(contactId) { let userInfo = state.cloud.identity.get('userInfo'); let contactChannel = channels.createContactChannel(userInfo.id, contactId); diff --git a/app/services/messages/messages-service.js b/app/services/messages/messages-service.js index 9006b811..9040a867 100644 --- a/app/services/messages/messages-service.js +++ b/app/services/messages/messages-service.js @@ -1,5 +1,14 @@ export let serviceName = 'messages'; export default /*@ngInject*/ function messages( + state ) { + let saveReceivedMessage = saveReceivedMessage() { + + }; + + let saveSentMessage = saveSentMessage(channelId, messageContent) { + return state.save() + }; + return {}; } From 56d51f2e7bc54db94546789771dda4298b93f546 Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Tue, 1 Sep 2015 00:43:11 -0700 Subject: [PATCH 04/21] Added saving message to buffer --- app/services/messages/messages-service.js | 54 +++++++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/app/services/messages/messages-service.js b/app/services/messages/messages-service.js index 9040a867..925d296e 100644 --- a/app/services/messages/messages-service.js +++ b/app/services/messages/messages-service.js @@ -1,14 +1,60 @@ export let serviceName = 'messages'; export default /*@ngInject*/ function messages( - state + $q, + network, + time, + state, + R ) { let saveReceivedMessage = saveReceivedMessage() { }; - let saveSentMessage = saveSentMessage(channelId, messageContent) { - return state.save() + let saveSendingMessage = saveSendingMessage(channelId, messageContent) { + if (!messageContent) { + return $q.reject('messages: message must not be empty'); + } + + let userId = state.cloud.identity.get(['userInfo', 'id']); + let logicalClock = state.cloud.channels.get([channelId, 'logicalClock']); + let newLogicalClock = logicalClock + 1; + let sentTime = time.getTime(); + let contactIds = state.cloud.channels.get([channelId, 'contactIds']); + + let messageId = `${sentTime}-${userId}`; + + let messageInfo = { + id: messageId, + senderId: userId, + sentTime: sentTime, + logicalClock: newLogicalClock, + content: messageContent + }; + + let bumpLogicalClock = () => state.save( + state.cloud.channels, + [channelId, 'logicalClock'], + newLogicalClock + ); + + let saveMessageInfo = () => state.save( + state.cloud.messages, + [channelId, 'messageInfo'], + messageInfo + ); + + let addToMessageBuffer = () => $q.all(R.map((contactId) => state.save( + state.cloud.buffer, + ['messages', contactId, messageId], + { messageId, channelId, contactId } + ))(contactIds)); + + return bumpLogicalClock() + .then(saveMessageInfo) + .then(addToMessageBuffer); }; - return {}; + return { + saveSendingMessage + }; } From 1e934cd3350d66a66ac9fd92aafc7621508d9197 Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Tue, 1 Sep 2015 02:25:12 -0700 Subject: [PATCH 05/21] Added periodic sending of buffered messages --- app/services/buffer/buffer-service.js | 64 ++++++++++++----- app/services/messages/messages-service.js | 20 +++--- app/services/network/network-service.js | 46 +++++------- app/views/channel/channel-controller.js | 88 ++++++++++++----------- app/views/channel/channel.html | 9 +-- 5 files changed, 121 insertions(+), 106 deletions(-) diff --git a/app/services/buffer/buffer-service.js b/app/services/buffer/buffer-service.js index 8af15877..c08a5e8e 100644 --- a/app/services/buffer/buffer-service.js +++ b/app/services/buffer/buffer-service.js @@ -1,39 +1,65 @@ export let serviceName = 'buffer'; export default /*@ngInject*/ function buffer( + $interval, state ) { - let PAYLOAD_TYPES = { - message: 'message', - profile: 'profile' + let network; + + let sendAttempts = { + messages: undefined, + profiles: undefined }; - let addMessage = function addMessage(channelId, messageId) { + let addMessage = function addMessage(messageId, channelId) { + if (sendAttempts.messages[messageId]) { + return; + } + return state.save( + state.cloud.buffer, + ['messages', messageId], + { messageId, channelId } + ) + .then(() => { + network.sendMessage(channelId, messageId); + sendAttempts.messages[messageId] = + $interval(() => network.sendMessage(channelId, messageId), 20000); + }); }; - //send all messages in buffer - //call immediately if status is 1, otherwise set up listener for status - let flush = function flush() { + let removeMessage = function removeMessage(messageId) { + if (!sendAttempts.messages[messageId]) { + return; + } + return state.remove( + state.cloud.buffer, + ['messages', messageId] + ) + .then(() => { + $interval.cancel(sendAttempts.messages[messageId]); + sendAttempts.messages[messageId] = undefined; + }); }; - let initialize = function initialize() { - let messagesCursor = state.cloud.buffer; - - R.forEach((channelId) => { - let messageChannelCursor = messagesCursor.select(channelId); + let initialize = function initialize(networkService) { + network = networkService; - let startSendingMessage = + let messageBuffersCursor = state.cloud.buffer.select(['messages']); + let messageBuffers = messageBuffersCursor.get(); - state.addListener(messageChannelCursor, ) - })(R.keys(messagesCursor.get())); + sendAttempts.messages = R.mapObj((messageBuffer) => { + let channelId = messageBuffer.channelId; + let messageId = messageBuffer.messageId; - R.pipe( - R.map - )(messageChannels); + network.sendMessage(channelId, messageId); + return $interval(() => network.sendMessage(channelId, messageId), 20000); + })(messageBuffers); }; return { - add + addMessage, + removeMessage, + initialize }; } diff --git a/app/services/messages/messages-service.js b/app/services/messages/messages-service.js index 925d296e..d890d164 100644 --- a/app/services/messages/messages-service.js +++ b/app/services/messages/messages-service.js @@ -1,6 +1,8 @@ export let serviceName = 'messages'; export default /*@ngInject*/ function messages( $q, + buffer, + cryptography, network, time, state, @@ -15,17 +17,19 @@ export default /*@ngInject*/ function messages( return $q.reject('messages: message must not be empty'); } - let userId = state.cloud.identity.get(['userInfo', 'id']); + let senderId = state.cloud.identity.get(['userInfo', 'id']); let logicalClock = state.cloud.channels.get([channelId, 'logicalClock']); let newLogicalClock = logicalClock + 1; let sentTime = time.getTime(); - let contactIds = state.cloud.channels.get([channelId, 'contactIds']); - let messageId = `${sentTime}-${userId}`; + let messageIdBase = cryptography.getSha256(`${channelId}-${senderId}`) + .substr(0, 32); + + let messageId = `${sentTime}-${messageIdBase}`; let messageInfo = { id: messageId, - senderId: userId, + senderId: senderId, sentTime: sentTime, logicalClock: newLogicalClock, content: messageContent @@ -43,15 +47,9 @@ export default /*@ngInject*/ function messages( messageInfo ); - let addToMessageBuffer = () => $q.all(R.map((contactId) => state.save( - state.cloud.buffer, - ['messages', contactId, messageId], - { messageId, channelId, contactId } - ))(contactIds)); - return bumpLogicalClock() .then(saveMessageInfo) - .then(addToMessageBuffer); + .then(() => buffer.addMessage(messageId, channelId)); }; return { diff --git a/app/services/network/network-service.js b/app/services/network/network-service.js index 3f3b7932..fd0ce0db 100644 --- a/app/services/network/network-service.js +++ b/app/services/network/network-service.js @@ -3,6 +3,7 @@ export default /*@ngInject*/ function network( $interval, $q, $window, + buffer, channels, notifications, R, @@ -307,41 +308,32 @@ export default /*@ngInject*/ function network( ); }; - let sendMessage = function sendMessage(channelInfo, messageContent, - logicalClock) { - if (!messageContent || !logicalClock) { - return $q.reject('Invalid message format.'); + let sendMessage = function sendMessage(channelId, messageId) { + let messageCursor = state.cloud.messages.select([channelId, messageId]); + let channelCursor = state.cloud.messages.select([channelId]); + + let messageInfo = messageCursor.get(['messageInfo']); + let channelInfo = messageCursor.get(['channelInfo']); + + if (!messageInfo) { + return $q.reject('network: message doesn\'t exist'); } let handleMessageAck = (ack) => { - let sentTime = ack.s; let receivedTime = ack.r; - let userId = state.cloud.identity.get().userInfo.id; - let messageId = sentTime + '-' + userId; - - let message = { - id: messageId, - //TODO: find main contact userId from sender userId for multi-signon - senderId: userId, - receivedTime: receivedTime, - sentTime: sentTime, - logicalClock: logicalClock, - content: messageContent - }; - let messagesCursor = state.cloud.messages.select([channelInfo.id]); - return state.save( - messagesCursor, - [messageId, 'messageInfo'], - message - ); + messagesCursor, + [messageId, 'receivedTime'], + receivedTime + ) + .then(() => buffer.removeMessage(messageId)); }; let contactId = channelInfo.contactIds[0]; let previousContactStatusId = state.cloud.contacts - .select([contactId]).get(['statusId']); + .get([contactId, 'statusId']); if (previousContactStatusId === undefined) { return $q.when(); @@ -349,10 +341,10 @@ export default /*@ngInject*/ function network( let payload = { m: { - c: messageContent, - l: logicalClock + c: messageInfo.content, + l: messageInfo.logicalClock }, - t: Date.now() + t: messageInfo.sentTime }; return send(channelInfo, payload) diff --git a/app/views/channel/channel-controller.js b/app/views/channel/channel-controller.js index 5bb25cdd..6839cb67 100644 --- a/app/views/channel/channel-controller.js +++ b/app/views/channel/channel-controller.js @@ -6,6 +6,7 @@ export default /*@ngInject*/ function ChannelController( $ionicScrollDelegate, identity, network, + messages, state ) { this.channelId = $stateParams.channelId; @@ -32,48 +33,51 @@ export default /*@ngInject*/ function ChannelController( }; this.message = ''; - //TODO: add to offline message queue instead of blocking further input - this.send = () => { - const MAX_ATTEMPTS = 3; - let attemptCount = 0; - let recursivelySendMessage = () => { - let logicalClock = channelCursor.get(['logicalClock']); - - return network.sendMessage( - channelCursor.get(['channelInfo']), - this.message, - logicalClock + 1 - ) - .then(() => { - let currentLogicalClock = channelCursor - .get(['logicalClock']); - - return state.save( - channelCursor, - ['logicalClock'], - currentLogicalClock + 1 - ); - }) - .catch((error) => { - if (error !== 'timeout') { - return $q.reject(error); - } - - attemptCount++; - if (attemptCount === MAX_ATTEMPTS) { - return $q.reject('Message sending has timed out.'); - } - return recursivelySendMessage(); - }); - }; + this.send = () => messages.saveSendingMessage(this.channelId, this.message); - this.sending = recursivelySendMessage() - .then(() => { - this.message = ''; - return $q.when(); - }); - - return this.sending; - }; + //TODO: add to offline message queue instead of blocking further input + // this.send = () => { + // const MAX_ATTEMPTS = 3; + // let attemptCount = 0; + // let recursivelySendMessage = () => { + // let logicalClock = channelCursor.get(['logicalClock']); + // + // return network.sendMessage( + // channelCursor.get(['channelInfo']), + // this.message, + // logicalClock + 1 + // ) + // .then(() => { + // let currentLogicalClock = channelCursor + // .get(['logicalClock']); + // + // return state.save( + // channelCursor, + // ['logicalClock'], + // currentLogicalClock + 1 + // ); + // }) + // .catch((error) => { + // if (error !== 'timeout') { + // return $q.reject(error); + // } + // + // attemptCount++; + // if (attemptCount === MAX_ATTEMPTS) { + // return $q.reject('Message sending has timed out.'); + // } + // + // return recursivelySendMessage(); + // }); + // }; + // + // this.sending = recursivelySendMessage() + // .then(() => { + // this.message = ''; + // return $q.when(); + // }); + // + // return this.sending; + // }; } diff --git a/app/views/channel/channel.html b/app/views/channel/channel.html index efa447c2..099f6885 100644 --- a/app/views/channel/channel.html +++ b/app/views/channel/channel.html @@ -14,7 +14,6 @@
-
+ From c726b5e916ddd4c126e09c7ccab8d8f3a22625a2 Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Tue, 1 Sep 2015 03:06:20 -0700 Subject: [PATCH 06/21] Fixed messages saving cursor path errors --- app/services/buffer/buffer-service.js | 12 ++++++++---- app/services/messages/messages-service.js | 8 +++++--- app/services/network/network-service.js | 8 ++++---- app/services/services.js | 2 ++ app/services/session/session-service.js | 2 ++ app/services/state/state-service.js | 2 ++ app/views/channel/channel.html | 10 ++++++---- app/views/channel/channel.scss | 2 +- 8 files changed, 30 insertions(+), 16 deletions(-) diff --git a/app/services/buffer/buffer-service.js b/app/services/buffer/buffer-service.js index c08a5e8e..69f1fc6f 100644 --- a/app/services/buffer/buffer-service.js +++ b/app/services/buffer/buffer-service.js @@ -1,7 +1,9 @@ export let serviceName = 'buffer'; export default /*@ngInject*/ function buffer( $interval, - state + $q, + state, + R ) { let network; @@ -45,8 +47,8 @@ export default /*@ngInject*/ function buffer( let initialize = function initialize(networkService) { network = networkService; - let messageBuffersCursor = state.cloud.buffer.select(['messages']); - let messageBuffers = messageBuffersCursor.get(); + let bufferedMessagesCursor = state.cloud.buffer.select(['messages']); + let bufferedMessage = bufferedMessagesCursor.get(); sendAttempts.messages = R.mapObj((messageBuffer) => { let channelId = messageBuffer.channelId; @@ -54,7 +56,9 @@ export default /*@ngInject*/ function buffer( network.sendMessage(channelId, messageId); return $interval(() => network.sendMessage(channelId, messageId), 20000); - })(messageBuffers); + })(bufferedMessage); + + return $q.when(); }; return { diff --git a/app/services/messages/messages-service.js b/app/services/messages/messages-service.js index d890d164..71227bdf 100644 --- a/app/services/messages/messages-service.js +++ b/app/services/messages/messages-service.js @@ -8,11 +8,13 @@ export default /*@ngInject*/ function messages( state, R ) { - let saveReceivedMessage = saveReceivedMessage() { + let saveReceivedMessage = function saveReceivedMessage() { }; - let saveSendingMessage = saveSendingMessage(channelId, messageContent) { + let saveSendingMessage = function saveSendingMessage( + channelId, messageContent + ) { if (!messageContent) { return $q.reject('messages: message must not be empty'); } @@ -43,7 +45,7 @@ export default /*@ngInject*/ function messages( let saveMessageInfo = () => state.save( state.cloud.messages, - [channelId, 'messageInfo'], + [channelId, messageId, 'messageInfo'], messageInfo ); diff --git a/app/services/network/network-service.js b/app/services/network/network-service.js index fd0ce0db..a70d11d7 100644 --- a/app/services/network/network-service.js +++ b/app/services/network/network-service.js @@ -310,10 +310,10 @@ export default /*@ngInject*/ function network( let sendMessage = function sendMessage(channelId, messageId) { let messageCursor = state.cloud.messages.select([channelId, messageId]); - let channelCursor = state.cloud.messages.select([channelId]); + let channelCursor = state.cloud.channels.select([channelId]); let messageInfo = messageCursor.get(['messageInfo']); - let channelInfo = messageCursor.get(['channelInfo']); + let channelInfo = channelCursor.get(['channelInfo']); if (!messageInfo) { return $q.reject('network: message doesn\'t exist'); @@ -323,8 +323,8 @@ export default /*@ngInject*/ function network( let receivedTime = ack.r; return state.save( - messagesCursor, - [messageId, 'receivedTime'], + messageCursor, + ['receivedTime'], receivedTime ) .then(() => buffer.removeMessage(messageId)); diff --git a/app/services/services.js b/app/services/services.js index c288e754..d5837572 100644 --- a/app/services/services.js +++ b/app/services/services.js @@ -1,5 +1,6 @@ import angular from 'angular'; +import buffer from './buffer/buffer'; import channels from './channels/channels'; import contacts from './contacts/contacts'; import cryptography from './cryptography/cryptography'; @@ -16,6 +17,7 @@ import storage from './storage/storage'; import time from './time/time'; export default angular.module('toc.services', [ + buffer.name, channels.name, contacts.name, cryptography.name, diff --git a/app/services/session/session-service.js b/app/services/session/session-service.js index e0e9f3a9..1bdc3226 100644 --- a/app/services/session/session-service.js +++ b/app/services/session/session-service.js @@ -4,6 +4,7 @@ export default /*@ngInject*/ function session( $log, $timeout, $window, + buffer, channels, contacts, devices, @@ -34,6 +35,7 @@ export default /*@ngInject*/ function session( .then(() => devices.initialize(destroy)) .then(() => channels.initialize(network.listen)) .then(() => status.initialize()) + .then(() => buffer.initialize(network)) .then(() => time.initialize()) .then(() => navigation.initialize()) .then(() => preparingPrivateSession.resolve('session: private ready')); diff --git a/app/services/state/state-service.js b/app/services/state/state-service.js index 656453cd..4fec5c61 100644 --- a/app/services/state/state-service.js +++ b/app/services/state/state-service.js @@ -69,6 +69,8 @@ export default /*@ngInject*/ function state( stateService.cloudUnencrypted.state = stateService.cloudUnencrypted .cursor.select(['state']); + stateService.cloud.buffer = stateService.cloud.cursor + .select(['buffer']); stateService.cloud.identity = stateService.cloud.cursor .select(['identity']); stateService.cloud.contacts = stateService.cloud.cursor diff --git a/app/views/channel/channel.html b/app/views/channel/channel.html index 099f6885..1722f76e 100644 --- a/app/views/channel/channel.html +++ b/app/views/channel/channel.html @@ -10,10 +10,11 @@ +
- +
-
+ + diff --git a/app/views/channel/channel.scss b/app/views/channel/channel.scss index 2d7474a9..d576804c 100644 --- a/app/views/channel/channel.scss +++ b/app/views/channel/channel.scss @@ -8,7 +8,7 @@ } .toc-message-input-form { - display: none; + // display: none; } .toc-message-input-area { From 330c8dbfe4d05a30d8b68541083b4c7b65973b4e Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Tue, 1 Sep 2015 03:30:00 -0700 Subject: [PATCH 07/21] Adapted message handling for new shorter id Added message box clear after sending --- app/services/messages/messages-service.js | 79 ++++++++++++++++++++++- app/services/network/network-service.js | 77 ++-------------------- app/views/channel/channel-controller.js | 5 +- 3 files changed, 86 insertions(+), 75 deletions(-) diff --git a/app/services/messages/messages-service.js b/app/services/messages/messages-service.js index 71227bdf..407685f3 100644 --- a/app/services/messages/messages-service.js +++ b/app/services/messages/messages-service.js @@ -3,13 +3,85 @@ export default /*@ngInject*/ function messages( $q, buffer, cryptography, - network, time, state, + notifications, R ) { - let saveReceivedMessage = function saveReceivedMessage() { + let saveReceivedMessage = function saveReceivedMessage( + messagePayload, messageMetadata + ) { + let messageContent = messagePayload.c; + let receivedLogicalClock = messagePayload.l; + let sentTime = messageMetadata.sentTime; + let receivedTime = messageMetadata.receivedTime; + let senderId = messageMetadata.senderId; + let channelId = messageMetadata.channelId; + + let messageIdBase = cryptography.getSha256(`${channelId}-${senderId}`) + .substr(0, 32); + + let messageId = `${sentTime}-${messageIdBase}`; + + let messageInfo = { + id: messageId, + //TODO: find main contact endpoint id from sender userId + // when using multi-endpoint contacts + senderId: senderId, + sentTime: sentTime, + logicalClock: receivedLogicalClock, + content: messageContent + }; + + let channelCursor = state.cloud.channels.select([channelId]); + + let messagesCursor = state.cloud.messages.select([channelId]); + + let existingLogicalClock = channelCursor.get('logicalClock'); + + let currentLogicalClock = receivedLogicalClock >= existingLogicalClock ? + receivedLogicalClock : existingLogicalClock; + + return state.save( + channelCursor, + ['logicalClock'], + currentLogicalClock + 1 + ) + .then(() => state.save( + messagesCursor, + [messageId, 'messageInfo'], + messageInfo + )) + .then(() => state.save( + messagesCursor, + [messageId, 'receivedTime'], + receivedTime + )) + .then(() => state.save( + channelCursor, + ['latestMessageId'], + messageId + )) + .then(() => { + let activeViewId = + state.cloud.navigation.get(['activeViewId']); + if (activeViewId === channelId && + channelCursor.get(['viewingLatest'])) { + return $q.when(); + } + + let updatingUnreadPointer = + !channelCursor.get(['unreadMessageId']) ? + state.save( + channelCursor, + ['unreadMessageId'], + messageId + ) : + $q.when(); + return updatingUnreadPointer + .then(() => notifications.notify(channelId)); + }); }; let saveSendingMessage = function saveSendingMessage( @@ -55,6 +127,7 @@ export default /*@ngInject*/ function messages( }; return { - saveSendingMessage + saveSendingMessage, + saveReceivedMessage }; } diff --git a/app/services/network/network-service.js b/app/services/network/network-service.js index a70d11d7..7beebe94 100644 --- a/app/services/network/network-service.js +++ b/app/services/network/network-service.js @@ -6,8 +6,10 @@ export default /*@ngInject*/ function network( buffer, channels, notifications, + messages, R, state, + time, telehash ) { let activeSession; @@ -87,74 +89,7 @@ export default /*@ngInject*/ function network( }; let handleMessage = function handleMessage(messagePayload, messageMetadata) { - let messageContent = messagePayload.c; - let receivedLogicalClock = messagePayload.l; - let sentTime = messageMetadata.sentTime; - let receivedTime = messageMetadata.receivedTime; - let senderId = messageMetadata.senderId; - let channelId = messageMetadata.channelId; - - let messageId = sentTime + '-' + senderId; - - let message = { - id: messageId, - //TODO: find main contact endpoint id from sender userId - // when using multi-endpoint contacts - senderId: senderId, - receivedTime: receivedTime, - sentTime: sentTime, - logicalClock: receivedLogicalClock, - content: messageContent - }; - - let channelCursor = state.cloud.channels.select( - [channelId] - ); - - let messagesCursor = state.cloud.messages.select( - [channelId] - ); - - let existingLogicalClock = channelCursor.get('logicalClock'); - - let currentLogicalClock = receivedLogicalClock >= existingLogicalClock ? - receivedLogicalClock : existingLogicalClock; - - return state.save( - channelCursor, - ['logicalClock'], - currentLogicalClock + 1 - ) - .then(() => state.save( - messagesCursor, - [messageId, 'messageInfo'], - message - )) - .then(() => state.save( - channelCursor, - ['latestMessageId'], - messageId - )) - .then(() => { - let activeViewId = - state.cloud.navigation.get(['activeViewId']); - if (activeViewId === channelId && - channelCursor.get(['viewingLatest'])) { - return $q.when(); - } - - let updatingUnreadPointer = - !channelCursor.get(['unreadMessageId']) ? - state.save( - channelCursor, - ['unreadMessageId'], - messageId - ) : - $q.when(); - - return updatingUnreadPointer - .then(() => notifications.notify(channelId)); - }); + return messages.saveReceivedMessage(messagePayload, messageMetadata); }; let listen = function listen(channelInfo, session = activeSession) { @@ -166,7 +101,7 @@ export default /*@ngInject*/ function network( let senderId = packet.from.hashname; let channelId = channel.type.substr(1); //removes leading underscore - let receivedTime = Date.now(); + let receivedTime = time.getTime(); let sentTime = packet.js.t; let ackPayload = packet.js.a; @@ -279,7 +214,7 @@ export default /*@ngInject*/ function network( let payload = { i: userInfo, - t: Date.now() + t: time.getTime() }; return send(inviteChannel, payload); @@ -299,7 +234,7 @@ export default /*@ngInject*/ function network( let payload = { s: statusId, - t: Date.now() + t: time.getTime() }; return send(contactChannel, payload) diff --git a/app/views/channel/channel-controller.js b/app/views/channel/channel-controller.js index 6839cb67..66682c57 100644 --- a/app/views/channel/channel-controller.js +++ b/app/views/channel/channel-controller.js @@ -34,7 +34,10 @@ export default /*@ngInject*/ function ChannelController( this.message = ''; - this.send = () => messages.saveSendingMessage(this.channelId, this.message); + this.send = () => messages.saveSendingMessage(this.channelId, this.message) + .then(() => { + this.message = ''; + }); //TODO: add to offline message queue instead of blocking further input // this.send = () => { From 2f2eb62bc90e1071cc587e7df2de4f097a975894 Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Tue, 1 Sep 2015 06:22:01 -0700 Subject: [PATCH 08/21] Refactored message handling to ignore duplicates Fixed list scrolling sometimes not working Updated baobab to v2 rc1 --- .../message-list/message-list-directive.js | 111 ++++++++-------- .../notification-overlay-directive.js | 4 +- .../update-profile-modal-directive.js | 5 +- app/config.js | 4 +- app/services/devices/devices-service.js | 7 +- app/services/messages/messages-service.js | 121 +++++++++++------- app/services/state/state-service.js | 26 ++-- app/views/channel/channel-controller.js | 12 +- package.json | 2 +- 9 files changed, 159 insertions(+), 133 deletions(-) diff --git a/app/components/message-list/message-list-directive.js b/app/components/message-list/message-list-directive.js index d85cb284..85d896c0 100644 --- a/app/components/message-list/message-list-directive.js +++ b/app/components/message-list/message-list-directive.js @@ -2,12 +2,12 @@ import template from './message-list.html!text'; export let directiveName = 'tocMessageList'; export default /*@ngInject*/ function tocMessageList( - $interval, $ionicScrollDelegate, + $interval, navigation, notifications, - R, - state + state, + R ) { return { restrict: 'E', @@ -15,36 +15,14 @@ export default /*@ngInject*/ function tocMessageList( scope: { channelId: '@' }, - controllerAs: 'messageList', - controller: /*@ngInject*/ function MessageListController( - $scope, - $state, - identity, - time, - messages - ) { - //TODO: refactor shared functionality into messages service - this.getAvatar = identity.getAvatar; - this.channelId = $scope.channelId; + link: function link(scope, element, attributes) { + //workaround for dynamic delegate-handle on ion-content not working + let scrollDelegate = R.find((delegateInstance) => { + return element.parent().parent()[0] === delegateInstance.element; + })($ionicScrollDelegate._instances); - let messagesCursor = state.cloud.messages.select([this.channelId]); - let channelCursor = state.cloud.channels.select([this.channelId]); - - let userCursor = state.cloud.identity.select(['userInfo']); - let contactCursor = state.cloud.contacts.select([ - channelCursor.get(['channelInfo', 'contactIds'])[0], - 'userInfo' - ]); - - let updateUser = () => { - this.userInfo = userCursor.get(); - }; - let updateContact = () => { - this.contactInfo = contactCursor.get(); - }; - - state.addListener(userCursor, updateUser, $scope); - state.addListener(contactCursor, updateContact, $scope); + let messagesCursor = state.cloud.messages.select([scope.channelId]); + let channelCursor = state.cloud.channels.select([scope.channelId]); let viewingLatestCursor = channelCursor.select(['viewingLatest']); let scrollToLatest = () => { @@ -52,14 +30,14 @@ export default /*@ngInject*/ function tocMessageList( return; } - if (!navigation.isActiveView(this.channelId)) { + if (!navigation.isActiveView(scope.channelId)) { return; } - $ionicScrollDelegate.scrollBottom(true); + scrollDelegate.scrollBottom(true); - if (!notifications.isDismissed(this.channelId)) { - notifications.dismiss(this.channelId); + if (!notifications.isDismissed(scope.channelId)) { + notifications.dismiss(scope.channelId); } if (channelCursor.get(['unreadMessageId'])) { @@ -67,14 +45,14 @@ export default /*@ngInject*/ function tocMessageList( } }; - state.addListener(viewingLatestCursor, scrollToLatest, $scope); + state.addListener(viewingLatestCursor, scrollToLatest, scope); let updateMessageListPosition = () => { - if (!navigation.isActiveView(this.channelId)) { + if (!navigation.isActiveView(scope.channelId)) { return; } - let scrollView = $ionicScrollDelegate.getScrollView(); + let scrollView = scrollDelegate.getScrollView(); if (scrollView.__scrollTop < scrollView.__maxScrollTop) { return; @@ -89,17 +67,17 @@ export default /*@ngInject*/ function tocMessageList( } }; - state.addListener(messagesCursor, updateMessageListPosition, $scope, { + state.addListener(messagesCursor, updateMessageListPosition, scope, { skipInitialize: true }); $interval(() => { - if (!navigation.isActiveView(this.channelId)) { + if (!navigation.isActiveView(scope.channelId)) { return; } //Updates unread messages if scrolled to bottom //TODO: write a more robust version that moves unread marker granularly - let scrollView = $ionicScrollDelegate.getScrollView(); + let scrollView = scrollDelegate.getScrollView(); //Don't do anything if not at scrolled to bottom if (scrollView.__scrollTop < scrollView.__maxScrollTop) { @@ -119,25 +97,48 @@ export default /*@ngInject*/ function tocMessageList( state.save(channelCursor, ['unreadMessageId'], null); } - if (!notifications.isDismissed(this.channelId)) { - notifications.dismiss(this.channelId); + if (!notifications.isDismissed(scope.channelId)) { + notifications.dismiss(scope.channelId); } }, 5000); + }, + controllerAs: 'messageList', + controller: /*@ngInject*/ function MessageListController( + $scope, + $state, + identity, + messages, + time + ) { + //TODO: refactor shared functionality into messages service + this.getAvatar = identity.getAvatar; + this.channelId = $scope.channelId; + + let messagesCursor = state.cloud.messages.select([this.channelId]); + let channelCursor = state.cloud.channels.select([this.channelId]); + + let userCursor = state.cloud.identity.select(['userInfo']); + let contactCursor = state.cloud.contacts.select([ + channelCursor.get(['channelInfo', 'contactIds'])[0], + 'userInfo' + ]); + + let updateUser = () => { + this.userInfo = userCursor.get(); + }; + let updateContact = () => { + this.contactInfo = contactCursor.get(); + }; + + state.addListener(userCursor, updateUser, $scope); + state.addListener(contactCursor, updateContact, $scope); + + let updateMessages = () => { this.messages = R.pipe( R.values, - R.sort((message1, message2) => { - let logicalClockDiff = - message1.messageInfo.logicalClock - - message2.messageInfo.logicalClock; - - if (logicalClockDiff === 0) { - return message1.messageInfo.id > message2.messageInfo.id ? 1 : -1; - } - - return logicalClockDiff; - }) + R.sort(messages.compareMessages) )(messagesCursor.get());; }; diff --git a/app/components/notification-overlay/notification-overlay-directive.js b/app/components/notification-overlay/notification-overlay-directive.js index 573130e5..6863fa66 100644 --- a/app/components/notification-overlay/notification-overlay-directive.js +++ b/app/components/notification-overlay/notification-overlay-directive.js @@ -36,10 +36,10 @@ export default /*@ngInject*/ function tocNotificationOverlay() { let updateActiveNotification = (event, notificationId) => { if (!notificationId) { - if (!event.data.data.notificationInfo) { + if (!event.data.currentData.notificationInfo) { return; } - notificationId = event.data.data.notificationInfo.id; + notificationId = event.data.currentData.notificationInfo.id; } if (notifications.isDismissed(notificationId)) { diff --git a/app/components/update-profile-modal/update-profile-modal-directive.js b/app/components/update-profile-modal/update-profile-modal-directive.js index 90a3168d..28f9f737 100644 --- a/app/components/update-profile-modal/update-profile-modal-directive.js +++ b/app/components/update-profile-modal/update-profile-modal-directive.js @@ -13,11 +13,12 @@ export default /*@ngInject*/ function tocUpdateProfileModal() { $scope, $q, identity, - state + state, + R ) { this.removeModal = $scope.removeModal; - this.userInfo = state.cloud.identity.get(['userInfo']); + this.userInfo = R.clone(state.cloud.identity.get(['userInfo'])); this.getNewAvatar = function getNewAvatar() { return identity.getAvatar(this.userInfo); diff --git a/app/config.js b/app/config.js index ced582c5..efb11d81 100644 --- a/app/config.js +++ b/app/config.js @@ -19,7 +19,7 @@ System.config({ "angular/bower-angular": "github:angular/bower-angular@1.4.4", "babel": "npm:babel-core@5.8.20", "babel-runtime": "npm:babel-runtime@5.8.20", - "baobab": "npm:baobab@1.1.2", + "baobab": "npm:baobab@2.0.0-rc1", "core-js": "npm:core-js@0.9.18", "driftyco/ionic": "github:driftyco/ionic@1.1.0", "moment": "npm:moment@2.10.6", @@ -79,7 +79,7 @@ System.config({ "npm:babel-runtime@5.8.20": { "process": "github:jspm/nodelibs-process@0.1.1" }, - "npm:baobab@1.1.2": { + "npm:baobab@2.0.0-rc1": { "emmett": "npm:emmett@3.1.1" }, "npm:browserify-zlib@0.1.4": { diff --git a/app/services/devices/devices-service.js b/app/services/devices/devices-service.js index 3bd8c317..1dd7fd89 100644 --- a/app/services/devices/devices-service.js +++ b/app/services/devices/devices-service.js @@ -17,8 +17,9 @@ export default /*@ngInject*/ function devices( let disconnectOtherDevices = function disconnectOtherDevices() { let localDeviceId = state.local.devices.get(['deviceInfo', 'id']); - let cloudDevices = state.cloud.devices.get() || {}; - cloudDevices[localDeviceId] = {}; + let existingCloudDevices = state.cloud.devices.get() || {}; + + let cloudDevices = R.assoc(localDeviceId, {})(existingCloudDevices); let devicesDisconnecting = R.pipe( R.keys, @@ -42,7 +43,7 @@ export default /*@ngInject*/ function devices( .select([localDeviceId, 'disconnect']); let handleDisconnects = function handleDisconnects(event) { - if (event.data.data !== 1) { + if (event.data.currentData !== 1) { return; } diff --git a/app/services/messages/messages-service.js b/app/services/messages/messages-service.js index 407685f3..8df4e06d 100644 --- a/app/services/messages/messages-service.js +++ b/app/services/messages/messages-service.js @@ -1,6 +1,7 @@ export let serviceName = 'messages'; export default /*@ngInject*/ function messages( $q, + $log, buffer, cryptography, time, @@ -8,21 +9,42 @@ export default /*@ngInject*/ function messages( notifications, R ) { + let compareMessages = function compareMessages(message1, message2) { + let logicalClockDiff = + message1.messageInfo.logicalClock - + message2.messageInfo.logicalClock; + + if (logicalClockDiff === 0) { + //tiebreaker + //id is in format sentTime-(hash(channelId-senderId)) + return message1.messageInfo.id > message2.messageInfo.id ? 1 : -1; + } + + // >1 message1 is later + // <1 message1 is earlier + return logicalClockDiff; + }; + let saveReceivedMessage = function saveReceivedMessage( messagePayload, messageMetadata ) { - let messageContent = messagePayload.c; - let receivedLogicalClock = messagePayload.l; let sentTime = messageMetadata.sentTime; - let receivedTime = messageMetadata.receivedTime; let senderId = messageMetadata.senderId; let channelId = messageMetadata.channelId; let messageIdBase = cryptography.getSha256(`${channelId}-${senderId}`) .substr(0, 32); - let messageId = `${sentTime}-${messageIdBase}`; + let existingMessage = state.cloud.messages.get([channelId, messageId]); + if (existingMessage) { + return $q.when(); + } + + let receivedTime = messageMetadata.receivedTime; + let messageContent = messagePayload.c; + let receivedLogicalClock = messagePayload.l; + let messageInfo = { id: messageId, //TODO: find main contact endpoint id from sender userId @@ -34,54 +56,62 @@ export default /*@ngInject*/ function messages( }; let channelCursor = state.cloud.channels.select([channelId]); - let messagesCursor = state.cloud.messages.select([channelId]); - + let messageCursor = messagesCursor.select([messageId]); let existingLogicalClock = channelCursor.get('logicalClock'); let currentLogicalClock = receivedLogicalClock >= existingLogicalClock ? receivedLogicalClock : existingLogicalClock; - return state.save( - channelCursor, - ['logicalClock'], - currentLogicalClock + 1 - ) - .then(() => state.save( - messagesCursor, - [messageId, 'messageInfo'], - messageInfo - )) - .then(() => state.save( - messagesCursor, - [messageId, 'receivedTime'], - receivedTime - )) - .then(() => state.save( - channelCursor, - ['latestMessageId'], - messageId - )) - .then(() => { - let activeViewId = - state.cloud.navigation.get(['activeViewId']); - if (activeViewId === channelId && - channelCursor.get(['viewingLatest'])) { + let newLogicalClock = currentLogicalClock + 1; + + let updateLatestMessageId = () => { + let latestMessageId = channelCursor.get(['latestMessageId']); + let latestMessage = messagesCursor.get([latestMessageId]); + let message = {messageInfo}; + + //do nothing if message is earlier than latest message + if (compareMessages(message, latestMessage) < 0) { + return $q.when(); + } + + return state.save(channelCursor, ['latestMessageId'], messageId); + }; + + let notifyMessage = () => { + //dont notify or update unread pointer if viewing latest message + let activeViewId = state.cloud.navigation.get(['activeViewId']); + if (activeViewId === channelId && channelCursor.get(['viewingLatest'])) { + return $q.when(); + } + + let updateUnreadPointer = () => { + let unreadMessageId = channelCursor.get(['unreadMessageId']); + if (!unreadMessageId) { return $q.when(); } - let updatingUnreadPointer = - !channelCursor.get(['unreadMessageId']) ? - state.save( - channelCursor, - ['unreadMessageId'], - messageId - ) : - $q.when(); - - return updatingUnreadPointer - .then(() => notifications.notify(channelId)); - }); + let unreadMessage = messagesCursor.get([unreadMessageId]); + let message = {messageInfo}; + + //do nothing if message is later than unread message + if (compareMessages(message, unreadMessage) > 0) { + return $q.when(); + } + + return state.save(channelCursor, ['latestMessageId'], messageId); + } + + return updateUnreadPointer() + .then(() => notifications.notify(channelId)); + }; + + return state.save(channelCursor, ['logicalClock'], newLogicalClock) + .then(() => state.save(messageCursor, ['messageInfo'], messageInfo)) + .then(() => state.save(messageCursor, ['receivedTime'], receivedTime)) + .then(updateLatestMessageId) + .then(notifyMessage) + .catch($log.error); }; let saveSendingMessage = function saveSendingMessage( @@ -98,7 +128,6 @@ export default /*@ngInject*/ function messages( let messageIdBase = cryptography.getSha256(`${channelId}-${senderId}`) .substr(0, 32); - let messageId = `${sentTime}-${messageIdBase}`; let messageInfo = { @@ -123,10 +152,12 @@ export default /*@ngInject*/ function messages( return bumpLogicalClock() .then(saveMessageInfo) - .then(() => buffer.addMessage(messageId, channelId)); + .then(() => buffer.addMessage(messageId, channelId)) + .catch($log.error); }; return { + compareMessages, saveSendingMessage, saveReceivedMessage }; diff --git a/app/services/state/state-service.js b/app/services/state/state-service.js index 4fec5c61..0454af25 100644 --- a/app/services/state/state-service.js +++ b/app/services/state/state-service.js @@ -18,11 +18,14 @@ export default /*@ngInject*/ function state( $window.baobab = Baobab; stateService.tree = new Baobab({ - memory: {}, - local: {}, - cloudUnencrypted: {}, - cloud: {} - }); + memory: {}, + local: {}, + cloudUnencrypted: {}, + cloud: {} + }, + { + // immutable: false + }); //TODO: test baobab event batching and tweak manual commit timing stateService.tree.on('update', @@ -96,11 +99,6 @@ export default /*@ngInject*/ function state( function saveVolatile(cursor, relativePath, object) { return $q.when() .then(() => { - if (cursor.get() === undefined) { - cursor.tree.set(cursor.path, {}); - cursor.tree.commit(); - } - cursor.set(relativePath, object); return object; }); @@ -114,14 +112,6 @@ export default /*@ngInject*/ function state( return store.storeObject(storageKey, object) .then(object => { - //FIXME: workaround for setting nonexistant cursors - // this can't be very performant - // migrating to baobab v2 should make this obsolete - if (cursor.get() === undefined) { - cursor.tree.set(cursor.path, {}); - cursor.tree.commit(); - } - cursor.set(relativePath, object); return object; }); diff --git a/app/views/channel/channel-controller.js b/app/views/channel/channel-controller.js index 66682c57..fc108539 100644 --- a/app/views/channel/channel-controller.js +++ b/app/views/channel/channel-controller.js @@ -29,15 +29,17 @@ export default /*@ngInject*/ function ChannelController( state.addListener(contactCursor, updateContact, $scope); this.viewLatest = () => { - $ionicScrollDelegate.scrollBottom(true); + $ionicScrollDelegate.$getByHandle(this.channelId).scrollBottom(true); }; this.message = ''; - this.send = () => messages.saveSendingMessage(this.channelId, this.message) - .then(() => { - this.message = ''; - }); + this.send = () => { + let message = this.message; + this.message = ''; + + return messages.saveSendingMessage(this.channelId, message); + }; //TODO: add to offline message queue instead of blocking further input // this.send = () => { diff --git a/package.json b/package.json index ff4c6a44..2c497e6f 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "angular": "github:angular/bower-angular@^1.4.0", "angular/bower-angular": "github:angular/bower-angular@^1.4.0", - "baobab": "npm:baobab@^1.1.2", + "baobab": "npm:baobab@2.0.0-rc1", "driftyco/ionic": "github:driftyco/ionic@1.1.0", "moment": "npm:moment@^2.10.6", "ng-cordova": "npm:ng-cordova@^0.1.17-alpha", From bac55b88f08a714b722ff0a2d6980c49935a4b8d Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Tue, 1 Sep 2015 19:02:49 -0700 Subject: [PATCH 09/21] Added visual indicator for sent messages Fixed some issues with auto-scrolling Added placeholders for profile buffering --- .../message-list/message-list-directive.js | 28 +++++++++++++------ app/components/message-list/message-list.html | 13 ++++++--- app/components/message-list/message-list.scss | 14 ++++++++++ app/services/buffer/buffer-service.js | 18 ++++++++++++ app/services/contacts/contacts-service.js | 3 -- app/services/messages/messages-service.js | 12 ++++---- app/views/channel/channel-controller.js | 2 +- 7 files changed, 69 insertions(+), 21 deletions(-) diff --git a/app/components/message-list/message-list-directive.js b/app/components/message-list/message-list-directive.js index 85d896c0..a776743d 100644 --- a/app/components/message-list/message-list-directive.js +++ b/app/components/message-list/message-list-directive.js @@ -53,18 +53,25 @@ export default /*@ngInject*/ function tocMessageList( } let scrollView = scrollDelegate.getScrollView(); + let scrollTop = scrollDelegate.getScrollPosition().top; + let scrollMax = scrollView.getScrollMax().top; - if (scrollView.__scrollTop < scrollView.__maxScrollTop) { + if (scrollTop < scrollMax) { return; } - if (!channelCursor.get(['viewingLatest'])) { - state.save(channelCursor, ['viewingLatest'], true); - } - if (channelCursor.get(['unreadMessageId'])) { state.save(channelCursor, ['unreadMessageId'], null); } + + if (!channelCursor.get(['viewingLatest'])) { + //changing viewingLatest will trigger scroll + // don't need to trigger manually + return state.save(channelCursor, ['viewingLatest'], true); + } + + //otherwise scroll to bottom to see latest message + scrollDelegate.scrollBottom(true); }; state.addListener(messagesCursor, updateMessageListPosition, scope, { @@ -78,9 +85,10 @@ export default /*@ngInject*/ function tocMessageList( //Updates unread messages if scrolled to bottom //TODO: write a more robust version that moves unread marker granularly let scrollView = scrollDelegate.getScrollView(); - - //Don't do anything if not at scrolled to bottom - if (scrollView.__scrollTop < scrollView.__maxScrollTop) { + let scrollTop = scrollDelegate.getScrollPosition().top; + let scrollMax = scrollView.getScrollMax().top; + //Don't do anything if not scrolled to bottom + if (scrollTop < scrollMax) { if (!channelCursor.get(['viewingLatest'])) { return; } @@ -179,6 +187,10 @@ export default /*@ngInject*/ function tocMessageList( ); }; + this.isMessageSending = (message) => { + return message.receivedTime === undefined; + }; + this.isDateSeparator = (message) => { let messageIndex = this.messages.indexOf(message); let previousMessage = this.messages[messageIndex - 1]; diff --git a/app/components/message-list/message-list.html b/app/components/message-list/message-list.html index 5d50f88c..6ad72ba5 100644 --- a/app/components/message-list/message-list.html +++ b/app/components/message-list/message-list.html @@ -1,14 +1,14 @@ Avatar for {{messageList.getUserInfo(message).email}} -
+
{{::messageList.getDatestamp(message)}}
@@ -17,9 +17,14 @@

{{messageList.getUserInfo(message).displayName || 'Anonymous'}}

{{::message.messageInfo.content}}

-
{{::messageList.getTimestamp(message)}}
+
+ ... +
diff --git a/app/components/message-list/message-list.scss b/app/components/message-list/message-list.scss index 70840ea7..ce777bb6 100644 --- a/app/components/message-list/message-list.scss +++ b/app/components/message-list/message-list.scss @@ -52,6 +52,20 @@ font-size: 12px; width: 100%; } + .toc-message-sending-indicator { + position: absolute; + bottom: 0px; + left: 0px; + text-align: right; + color: $dark; + // font-size: 12px; + padding-left: 16px; + padding-right: 16px; + width: 100%; + // .spinner svg { + // height: 20px; + // } + } } .toc-message-avatar { diff --git a/app/services/buffer/buffer-service.js b/app/services/buffer/buffer-service.js index 69f1fc6f..fbd5fb22 100644 --- a/app/services/buffer/buffer-service.js +++ b/app/services/buffer/buffer-service.js @@ -9,6 +9,7 @@ export default /*@ngInject*/ function buffer( let sendAttempts = { messages: undefined, + invites: undefined, profiles: undefined }; @@ -44,6 +45,22 @@ export default /*@ngInject*/ function buffer( }); }; + let addInvite = function addInvite(contactId) { + + }; + + let removeInvite = function removeInvite(contactId) { + + }; + + let addProfile = function addProfile(channelId) { + + }; + + let removeProfile = function removeProfile(channelId) { + + }; + let initialize = function initialize(networkService) { network = networkService; @@ -54,6 +71,7 @@ export default /*@ngInject*/ function buffer( let channelId = messageBuffer.channelId; let messageId = messageBuffer.messageId; + //TODO: stagger the initial send and retry intervals network.sendMessage(channelId, messageId); return $interval(() => network.sendMessage(channelId, messageId), 20000); })(bufferedMessage); diff --git a/app/services/contacts/contacts-service.js b/app/services/contacts/contacts-service.js index 97ec2c78..2ccc7a65 100644 --- a/app/services/contacts/contacts-service.js +++ b/app/services/contacts/contacts-service.js @@ -7,9 +7,6 @@ export default /*@ngInject*/ function contacts( R, state ) { - //TODO: add sentLatestProfile flag to each contact - // reset when profile updates - let invite = function inviteContact(contactId) { let userInfo = state.cloud.identity.get('userInfo'); let contactChannel = channels.createContactChannel(userInfo.id, contactId); diff --git a/app/services/messages/messages-service.js b/app/services/messages/messages-service.js index 8df4e06d..e855e522 100644 --- a/app/services/messages/messages-service.js +++ b/app/services/messages/messages-service.js @@ -67,12 +67,14 @@ export default /*@ngInject*/ function messages( let updateLatestMessageId = () => { let latestMessageId = channelCursor.get(['latestMessageId']); - let latestMessage = messagesCursor.get([latestMessageId]); - let message = {messageInfo}; + if (latestMessageId) { + let latestMessage = messagesCursor.get([latestMessageId]); + let message = {messageInfo}; - //do nothing if message is earlier than latest message - if (compareMessages(message, latestMessage) < 0) { - return $q.when(); + //do nothing if message is earlier than latest message + if (compareMessages(message, latestMessage) < 0) { + return $q.when(); + } } return state.save(channelCursor, ['latestMessageId'], messageId); diff --git a/app/views/channel/channel-controller.js b/app/views/channel/channel-controller.js index fc108539..11e98fff 100644 --- a/app/views/channel/channel-controller.js +++ b/app/views/channel/channel-controller.js @@ -29,7 +29,7 @@ export default /*@ngInject*/ function ChannelController( state.addListener(contactCursor, updateContact, $scope); this.viewLatest = () => { - $ionicScrollDelegate.$getByHandle(this.channelId).scrollBottom(true); + $ionicScrollDelegate.scrollBottom(true); }; this.message = ''; From 2695b3481b85086d7a8ed37471f9694dcdcd7e52 Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Tue, 1 Sep 2015 20:18:03 -0700 Subject: [PATCH 10/21] Added new unread-marker styling Slightly lighten font-color for user's own messages Fixed blank user card message when only 1 notification --- .../message-list/message-list-directive.js | 13 ++++- app/components/message-list/message-list.html | 18 ++++--- app/components/message-list/message-list.scss | 50 +++++++++++++++---- .../user-card/user-card-directive.js | 5 +- app/services/messages/messages-service.js | 20 ++++---- 5 files changed, 76 insertions(+), 30 deletions(-) diff --git a/app/components/message-list/message-list-directive.js b/app/components/message-list/message-list-directive.js index a776743d..fe5d12ef 100644 --- a/app/components/message-list/message-list-directive.js +++ b/app/components/message-list/message-list-directive.js @@ -119,6 +119,7 @@ export default /*@ngInject*/ function tocMessageList( time ) { //TODO: refactor shared functionality into messages service + //TODO: memoize what can be memoized this.getAvatar = identity.getAvatar; this.channelId = $scope.channelId; @@ -141,8 +142,6 @@ export default /*@ngInject*/ function tocMessageList( state.addListener(userCursor, updateUser, $scope); state.addListener(contactCursor, updateContact, $scope); - - let updateMessages = () => { this.messages = R.pipe( R.values, @@ -159,6 +158,16 @@ export default /*@ngInject*/ function tocMessageList( return unreadMessageId === message.messageInfo.id; }; + this.isByUser = (message) => { + if (!message) { + return; + } + + let senderId = message.messageInfo.senderId; + + return this.userInfo.id === senderId; + }; + this.isSenderSeparator = (message) => { let messageIndex = this.messages.indexOf(message); let previousMessage = this.messages[messageIndex - 1]; diff --git a/app/components/message-list/message-list.html b/app/components/message-list/message-list.html index 6ad72ba5..fb3424b8 100644 --- a/app/components/message-list/message-list.html +++ b/app/components/message-list/message-list.html @@ -1,10 +1,12 @@ + ng-class="{ + 'toc-message-avatar': messageList.isSenderSeparator(message), + 'toc-message-simple': !messageList.isSenderSeparator(message), + 'toc-message-user': messageList.isByUser(message), + 'toc-message-contact': !messageList.isByUser(message) + }"> Avatar for {{messageList.getUserInfo(message).email}} @@ -13,9 +15,13 @@ {{::messageList.getDatestamp(message)}}
+ class="toc-message-unread"> +
+
-

{{messageList.getUserInfo(message).displayName || 'Anonymous'}}

+

+ {{messageList.getUserInfo(message).displayName || 'Anonymous'}} +

{{::message.messageInfo.content}}

:last-child { box-shadow: unset; - margin-bottom: 16px; + margin-bottom: 20px; } } } @@ -15,6 +15,7 @@ background-color: transparent; padding-right: 72px; min-height: unset; + .toc-message-content { white-space: normal; } @@ -23,24 +24,35 @@ right: 16px; top: 16px; } - .toc-message-unread-marker { + .toc-message-unread { position: absolute; - top: 0px; + bottom: 0px; left: 0px; - height: 3px; + padding-left: 72px; + padding-right: 72px; + height: 20px; text-align: left; - color: $assertive; - //FIXME: pick a less jarring unread marker style - // box-shadow: 0 1px 2px $assertive inset; font-size: 10px; - width: 110%; + width: 100%; + z-index: -1; + } + .toc-message-unread-marker { + background-size: 100% 5px; + background-repeat: no-repeat; + background-image: radial-gradient( + ellipse 50% 40%, + $energized, + $light + ); + height: 100%; + width: 100%; } .toc-message-timestamp { position: absolute; bottom: 0px; left: 0px; text-align: right; - color: $dark; + color: $royal; font-size: 12px; padding-left: 16px; padding-right: 16px; @@ -57,7 +69,7 @@ bottom: 0px; left: 0px; text-align: right; - color: $dark; + color: $energized; // font-size: 12px; padding-left: 16px; padding-right: 16px; @@ -87,3 +99,21 @@ display: none; } } + +.toc-message-user { + h2 { + color: lighten($dark, 20); + } + p { + color: lighten($dark, 20); + } +} + +.toc-message-contact { + h2 { + color: $dark; + } + p { + color: $dark; + } +} diff --git a/app/components/user-card/user-card-directive.js b/app/components/user-card/user-card-directive.js index 80e97d4e..1e381074 100644 --- a/app/components/user-card/user-card-directive.js +++ b/app/components/user-card/user-card-directive.js @@ -51,8 +51,11 @@ export default /*@ngInject*/ function tocUserCard( return; } + let hasMultipleNotifications = this.notificationCount > 1; + let notificationEnding = hasMultipleNotifications ? 's' : ''; + this.message = this.notificationCount + ' new notification' + - this.notificationCount > 1 ? 's' : ''; + notificationEnding; }; state.addListener(notificationsCursor, updateSummary, $scope); diff --git a/app/services/messages/messages-service.js b/app/services/messages/messages-service.js index e855e522..5be81986 100644 --- a/app/services/messages/messages-service.js +++ b/app/services/messages/messages-service.js @@ -89,19 +89,17 @@ export default /*@ngInject*/ function messages( let updateUnreadPointer = () => { let unreadMessageId = channelCursor.get(['unreadMessageId']); - if (!unreadMessageId) { - return $q.when(); - } - - let unreadMessage = messagesCursor.get([unreadMessageId]); - let message = {messageInfo}; - - //do nothing if message is later than unread message - if (compareMessages(message, unreadMessage) > 0) { - return $q.when(); + if (unreadMessageId) { + let unreadMessage = messagesCursor.get([unreadMessageId]); + let message = {messageInfo}; + + //do nothing if message is later than unread message + if (compareMessages(message, unreadMessage) > 0) { + return $q.when(); + } } - return state.save(channelCursor, ['latestMessageId'], messageId); + return state.save(channelCursor, ['unreadMessageId'], messageId); } return updateUnreadPointer() From 89c0210de796b46122cde54f51731769b86bc161 Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Wed, 2 Sep 2015 03:28:52 -0700 Subject: [PATCH 11/21] Added buffering functions for profiles --- app/services/buffer/buffer-service.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/services/buffer/buffer-service.js b/app/services/buffer/buffer-service.js index fbd5fb22..28aa1fb7 100644 --- a/app/services/buffer/buffer-service.js +++ b/app/services/buffer/buffer-service.js @@ -54,11 +54,35 @@ export default /*@ngInject*/ function buffer( }; let addProfile = function addProfile(channelId) { + if (sendAttempts.profiles[channelId]) { + return; + } + return state.save( + state.cloud.buffer, + ['profiles', channelId], + {channelId} + ) + .then(() => { + network.sendProfile(channelId); + sendAttempts.profiles[channelId] = + $interval(() => network.sendProfile(channelId), 20000); + }); }; let removeProfile = function removeProfile(channelId) { + if (!sendAttempts.profiles[channelId]) { + return; + } + return state.remove( + state.cloud.buffer, + ['profiles', channelId] + ) + .then(() => { + $interval.cancel(sendAttempts.profiles[channelId]); + sendAttempts.profiles[channelId] = undefined; + }); }; let initialize = function initialize(networkService) { From 65e90c230f7e3ab37edbab01d94dcf9e11e239e7 Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Wed, 2 Sep 2015 22:53:23 -0700 Subject: [PATCH 12/21] Implemented buffered invite sending --- app/services/contacts/contacts-service.js | 29 +++++++++++++++++++++++ app/services/network/network-service.js | 26 ++++++++++---------- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/app/services/contacts/contacts-service.js b/app/services/contacts/contacts-service.js index 2ccc7a65..fe34c802 100644 --- a/app/services/contacts/contacts-service.js +++ b/app/services/contacts/contacts-service.js @@ -1,12 +1,41 @@ export let serviceName = 'contacts'; export default /*@ngInject*/ function contacts( $q, + buffer, channels, identity, network, R, state ) { + let saveSendingInvite = function saveSendingInvite(contactId) { + let userInfo = state.cloud.identity.get('userInfo'); + let contactChannel = channels.createContactChannel(userInfo.id, contactId); + let existingContact = state.cloud.contacts.get([contactId, 'userInfo']); + + let contact = existingContact || { + id: contactId + }; + + return buffer.addProfile(contactChannel.id) + .then(() => state.save( + state.cloud.contacts, + [contactId, 'userInfo'], + contact + )) + .then(() => state.save( + state.cloud.channels, + [contactChannel.id, 'channelInfo'], + contactChannel + )) + .then(() => channels.initializeChannel(contactChannel)) + .then(() => network.listen(contactChannel)); + }; + + let saveSendingProfile = function saveSendingProfile(channelId) { + + }; + let invite = function inviteContact(contactId) { let userInfo = state.cloud.identity.get('userInfo'); let contactChannel = channels.createContactChannel(userInfo.id, contactId); diff --git a/app/services/network/network-service.js b/app/services/network/network-service.js index 7beebe94..22c02d03 100644 --- a/app/services/network/network-service.js +++ b/app/services/network/network-service.js @@ -22,21 +22,21 @@ export default /*@ngInject*/ function network( throw new Error('network: no active session'); }; - let handleInvite = function handleInvite(invitePayload) { - let contactInfo = invitePayload; + let handleProfile = function handleProfile(profilePayload) { + let contactInfo = profilePayload; let userId = state.cloud.identity.get(['userInfo']).id; let channel = channels.createContactChannel(userId, contactInfo.id); - // if channel already exists, this invite packet indicates acceptance + // if channel already exists, this profile packet indicates acceptance let existingChannel = state.cloud.channels .get([channel.id, 'channelInfo']); let statusId = 1; //online - let receivedInvite = !existingChannel; + let receivedProfile = !existingChannel; return state.save( state.cloud.contacts, @@ -54,7 +54,7 @@ export default /*@ngInject*/ function network( channel )) .then(() => { - if (receivedInvite) { + if (receivedProfile) { return state.save( state.cloud.channels, [channel.id, 'receivedInvite'], @@ -105,7 +105,7 @@ export default /*@ngInject*/ function network( let sentTime = packet.js.t; let ackPayload = packet.js.a; - let invitePayload = packet.js.i; + let profilePayload = packet.js.p; let statusPayload = packet.js.s; let messagePayload = packet.js.m; @@ -124,8 +124,8 @@ export default /*@ngInject*/ function network( if (ackPayload) { return $q.when(); - } else if (invitePayload) { - return handleInvite(invitePayload); + } else if (profilePayload) { + return handleProfile(profilePayload); } else if (statusPayload !== undefined) { return handleStatus(statusPayload, senderId); } else if (messagePayload) { @@ -206,18 +206,18 @@ export default /*@ngInject*/ function network( ); }; - let sendInvite = function sendInvite(contactId, userInfo) { - let inviteChannel = { + let sendProfile = function sendProfile(contactId, userInfo) { + let profileChannel = { id: channels.INVITE_CHANNEL_ID, contactIds: [contactId] }; let payload = { - i: userInfo, + p: userInfo, t: time.getTime() }; - return send(inviteChannel, payload); + return send(profileChannel, payload); }; let sendStatus = function sendStatus(contactId, statusId) { @@ -364,7 +364,7 @@ export default /*@ngInject*/ function network( let networkService = { listen, send, - sendInvite, + sendProfile, sendStatus, sendMessage, initialize, From ccd93f088b078e032250f7d8b3efa44681809192 Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Thu, 3 Sep 2015 06:18:17 -0700 Subject: [PATCH 13/21] Implemented profile update buffers --- .../begin-conversation-modal-directive.js | 26 ++-- .../update-profile-modal-directive.js | 4 +- app/services/buffer/buffer-service.js | 111 +++++++++++------- app/services/contacts/contacts-service.js | 65 +++++++--- app/services/network/network-service.js | 63 ++-------- 5 files changed, 139 insertions(+), 130 deletions(-) diff --git a/app/components/begin-conversation-modal/begin-conversation-modal-directive.js b/app/components/begin-conversation-modal/begin-conversation-modal-directive.js index 125c65a3..09fde561 100644 --- a/app/components/begin-conversation-modal/begin-conversation-modal-directive.js +++ b/app/components/begin-conversation-modal/begin-conversation-modal-directive.js @@ -30,19 +30,19 @@ export default /*@ngInject*/ function tocBeginConversationModal() { let initializeSentInvite = (contactChannel) => { return state.save( - state.cloud.channels, - [contactChannel.id, 'sentInvite'], - true - ) - .then(() => state.save( - state.cloud.contacts, - [this.contactId, 'statusId'], - 0 - )) - .then(() => { - this.removeModal(); - return $q.when(); - }); + state.cloud.channels, + [contactChannel.id, 'sentInvite'], + true + ) + .then(() => state.save( + state.cloud.contacts, + [this.contactId, 'statusId'], + 0 + )) + .then(() => { + this.removeModal(); + return $q.when(); + }); }; this.inviteMethod = 'enter'; diff --git a/app/components/update-profile-modal/update-profile-modal-directive.js b/app/components/update-profile-modal/update-profile-modal-directive.js index 28f9f737..0e07fd3b 100644 --- a/app/components/update-profile-modal/update-profile-modal-directive.js +++ b/app/components/update-profile-modal/update-profile-modal-directive.js @@ -17,8 +17,8 @@ export default /*@ngInject*/ function tocUpdateProfileModal() { R ) { this.removeModal = $scope.removeModal; - - this.userInfo = R.clone(state.cloud.identity.get(['userInfo'])); + let userInfo = state.cloud.identity.get(['userInfo']); + this.userInfo = R.assoc('version', userInfo.version + 1, userInfo); this.getNewAvatar = function getNewAvatar() { return identity.getAvatar(this.userInfo); diff --git a/app/services/buffer/buffer-service.js b/app/services/buffer/buffer-service.js index 28aa1fb7..709d9126 100644 --- a/app/services/buffer/buffer-service.js +++ b/app/services/buffer/buffer-service.js @@ -13,36 +13,49 @@ export default /*@ngInject*/ function buffer( profiles: undefined }; + let bufferedMessagesCursor = state.cloud.buffer.select(['messages']); + let bufferedProfilesCursor = state.cloud.buffer.select(['profiles']); + let bufferedInvitesCursor = state.cloud.buffer.select(['invites']); + + let sendMessage = function sendMessage(messageId, channelId) { + let messageCursor = state.cloud.messages.select([channelId, messageId]); + let messageInfo = messageCursor.get(['messageInfo']); + let channelInfo = state.cloud.channels.get([channelId, 'channelInfo']); + + let handleMessageAck = (ack) => { + let receivedTime = ack.r; + + return state.save(messageCursor, ['receivedTime'], receivedTime) + .then(() => removeMessage(messageInfo.id)); + }; + + return network.sendMessage(channelInfo, messageInfo) + .then(handleMessageAck); + }; + let addMessage = function addMessage(messageId, channelId) { if (sendAttempts.messages[messageId]) { - return; + return $q.when(); } + sendMessage(messageId, channelId); + sendAttempts.messages[messageId] = + $interval(() => sendMessage(messageId, channelId), 20000); return state.save( - state.cloud.buffer, - ['messages', messageId], - { messageId, channelId } - ) - .then(() => { - network.sendMessage(channelId, messageId); - sendAttempts.messages[messageId] = - $interval(() => network.sendMessage(channelId, messageId), 20000); - }); + bufferedMessagesCursor, + [messageId], + { messageId, channelId } + ); }; let removeMessage = function removeMessage(messageId) { if (!sendAttempts.messages[messageId]) { - return; + return $q.when(); } - return state.remove( - state.cloud.buffer, - ['messages', messageId] - ) - .then(() => { - $interval.cancel(sendAttempts.messages[messageId]); - sendAttempts.messages[messageId] = undefined; - }); + $interval.cancel(sendAttempts.messages[messageId]); + sendAttempts.messages[messageId] = undefined; + return state.remove(bufferedMessagesCursor, [messageId]); }; let addInvite = function addInvite(contactId) { @@ -53,36 +66,48 @@ export default /*@ngInject*/ function buffer( }; + let sendProfile = function sendProfile(channelId, userInfo) { + let channelInfo = state.cloud.channels.get([channelId, 'channelInfo']); + + let handleProfileAck = (ack) => { + return buffer.removeProfile(channelInfo.id, userInfo.version); + }; + + return network.sendProfile(channelInfo, userInfo) + .then(handleProfileAck); + }; + let addProfile = function addProfile(channelId) { - if (sendAttempts.profiles[channelId]) { - return; + let userInfo = state.cloud.identity.get(['userInfo']); + let profileVersion = userInfo.version; + let existingBufferedProfile = sendAttempts.profiles[channelId]; + + if (existingBufferedProfile.version >= profileVersion) { + return $q.when(); } + sendProfile(channelId, userInfo); + sendAttempts.profiles[channelId] = + $interval(() => sendProfile(channelId, userInfo), 20000); + return state.save( - state.cloud.buffer, - ['profiles', channelId], - {channelId} - ) - .then(() => { - network.sendProfile(channelId); - sendAttempts.profiles[channelId] = - $interval(() => network.sendProfile(channelId), 20000); - }); + bufferedProfilesCursor, + [channelId], + {channelId, profileVersion} + ); }; - let removeProfile = function removeProfile(channelId) { - if (!sendAttempts.profiles[channelId]) { - return; + let removeProfile = function removeProfile(channelId, profileVersion) { + let existingBufferedProfile = sendAttempts.profiles[channelId]; + + if (existingBufferedProfile.version < profileVersion) { + return $q.when(); } - return state.remove( - state.cloud.buffer, - ['profiles', channelId] - ) - .then(() => { - $interval.cancel(sendAttempts.profiles[channelId]); - sendAttempts.profiles[channelId] = undefined; - }); + $interval.cancel(sendAttempts.profiles[channelId]); + sendAttempts.profiles[channelId] = undefined; + + return state.remove(bufferedProfilesCursor, [channelId]); }; let initialize = function initialize(networkService) { @@ -96,8 +121,8 @@ export default /*@ngInject*/ function buffer( let messageId = messageBuffer.messageId; //TODO: stagger the initial send and retry intervals - network.sendMessage(channelId, messageId); - return $interval(() => network.sendMessage(channelId, messageId), 20000); + sendMessage(messageId, channelId); + return $interval(() => sendMessage(messageId, channelId), 20000); })(bufferedMessage); return $q.when(); diff --git a/app/services/contacts/contacts-service.js b/app/services/contacts/contacts-service.js index fe34c802..d1a32f07 100644 --- a/app/services/contacts/contacts-service.js +++ b/app/services/contacts/contacts-service.js @@ -8,32 +8,61 @@ export default /*@ngInject*/ function contacts( R, state ) { + let saveReceivedProfile = function saveReceivedProfile( + profilePayload, channelId + ) { + let contactInfo = profilePayload; + let contactCursor = state.cloud.contacts.select([contactInfo.id]); + let existingContactInfo = contactCursor.get(['userInfo']); + + if (existingContactInfo.version > contactInfo.version) { + return $q.when(); + } + + return state.save(contactCursor, ['userInfo'], contactInfo); + + // if (channelId !== channels.INVITE_CHANNEL_ID) { + // + // } + // + // return state.save(contactCursor, ['userInfo'], contactInfo) + // .then(() => state.save(contactCursor, ['statusId'], 1)) + // .then(() => state.save( + // state.cloud.channels, + // [channel.id, 'channelInfo'], + // channel + // )) + // + // if (!existingContactInfo) { + // + // } + // // otherwise it's a sent invite + }; + + let saveAcceptingInvite = function saveAcceptingInvite(channelId) { + + }; + let saveSendingInvite = function saveSendingInvite(contactId) { - let userInfo = state.cloud.identity.get('userInfo'); let contactChannel = channels.createContactChannel(userInfo.id, contactId); - let existingContact = state.cloud.contacts.get([contactId, 'userInfo']); + let contactCursor = state.cloud.channels.select([contactId]); + let channelCursor = state.cloud.channels.select([contactChannel.id]); - let contact = existingContact || { + let contactInfo = { + version: 0, id: contactId }; - - return buffer.addProfile(contactChannel.id) - .then(() => state.save( - state.cloud.contacts, - [contactId, 'userInfo'], - contact - )) - .then(() => state.save( - state.cloud.channels, - [contactChannel.id, 'channelInfo'], - contactChannel - )) - .then(() => channels.initializeChannel(contactChannel)) - .then(() => network.listen(contactChannel)); + + return buffer.addInvite(contactId) + .then(() => state.save(contactCursor, ['userInfo'], contactInfo)) + .then(() => state.save(channelCursor, ['channelInfo'], contactChannel)) + .then(() => state.save(channelCursor, ['sendingInvite'], true)); + // .then(() => channels.initializeChannel(contactChannel)) + // .then(() => network.listen(contactChannel)); }; let saveSendingProfile = function saveSendingProfile(channelId) { - + return buffer.addProfile(channelId); }; let invite = function inviteContact(contactId) { diff --git a/app/services/network/network-service.js b/app/services/network/network-service.js index 22c02d03..0d3f68ca 100644 --- a/app/services/network/network-service.js +++ b/app/services/network/network-service.js @@ -3,7 +3,6 @@ export default /*@ngInject*/ function network( $interval, $q, $window, - buffer, channels, notifications, messages, @@ -22,12 +21,10 @@ export default /*@ngInject*/ function network( throw new Error('network: no active session'); }; - let handleProfile = function handleProfile(profilePayload) { + let handleProfile = function handleProfile(profilePayload, channelId) { let contactInfo = profilePayload; - let userId = - state.cloud.identity.get(['userInfo']).id; - + let userId = state.cloud.identity.get(['userInfo']).id; let channel = channels.createContactChannel(userId, contactInfo.id); // if channel already exists, this profile packet indicates acceptance @@ -38,21 +35,7 @@ export default /*@ngInject*/ function network( let receivedProfile = !existingChannel; - return state.save( - state.cloud.contacts, - [contactInfo.id, 'userInfo'], - contactInfo - ) - .then(() => state.save( - state.cloud.contacts, - [contactInfo.id, 'statusId'], - statusId - )) - .then(() => state.save( - state.cloud.channels, - [channel.id, 'channelInfo'], - channel - )) + return .then(() => { if (receivedProfile) { return state.save( @@ -125,7 +108,7 @@ export default /*@ngInject*/ function network( if (ackPayload) { return $q.when(); } else if (profilePayload) { - return handleProfile(profilePayload); + return handleProfile(profilePayload, channelId); } else if (statusPayload !== undefined) { return handleStatus(statusPayload, senderId); } else if (messagePayload) { @@ -206,18 +189,13 @@ export default /*@ngInject*/ function network( ); }; - let sendProfile = function sendProfile(contactId, userInfo) { - let profileChannel = { - id: channels.INVITE_CHANNEL_ID, - contactIds: [contactId] - }; - + let sendProfile = function sendProfile(channelInfo, userInfo) { let payload = { p: userInfo, t: time.getTime() }; - return send(profileChannel, payload); + return send(channelInfo, payload); }; let sendStatus = function sendStatus(contactId, statusId) { @@ -243,28 +221,7 @@ export default /*@ngInject*/ function network( ); }; - let sendMessage = function sendMessage(channelId, messageId) { - let messageCursor = state.cloud.messages.select([channelId, messageId]); - let channelCursor = state.cloud.channels.select([channelId]); - - let messageInfo = messageCursor.get(['messageInfo']); - let channelInfo = channelCursor.get(['channelInfo']); - - if (!messageInfo) { - return $q.reject('network: message doesn\'t exist'); - } - - let handleMessageAck = (ack) => { - let receivedTime = ack.r; - - return state.save( - messageCursor, - ['receivedTime'], - receivedTime - ) - .then(() => buffer.removeMessage(messageId)); - }; - + let sendMessage = function sendMessage(channelInfo, messageInfo) { let contactId = channelInfo.contactIds[0]; let previousContactStatusId = state.cloud.contacts @@ -283,10 +240,7 @@ export default /*@ngInject*/ function network( }; return send(channelInfo, payload) - .then(handleMessageAck) - .catch((error) => - handleSendTimeout(error, contactId, previousContactStatusId) - ); + .then(handleMessageAck); }; let initialize = function initializeNetwork() { @@ -296,6 +250,7 @@ export default /*@ngInject*/ function network( let saveUserInfo = (networkInfo) => { let userInfo = { + version: 1, id: networkInfo.id }; From 9112726a95447e994bfcaa8712be87f1bee8ec4e Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Thu, 3 Sep 2015 22:36:09 -0700 Subject: [PATCH 14/21] Completed invite and profile buffering logic --- app/services/buffer/buffer-service.js | 107 +++++++++++------ app/services/contacts/contacts-service.js | 137 ++++++++-------------- app/services/network/network-service.js | 48 +++----- 3 files changed, 143 insertions(+), 149 deletions(-) diff --git a/app/services/buffer/buffer-service.js b/app/services/buffer/buffer-service.js index 709d9126..95658b0b 100644 --- a/app/services/buffer/buffer-service.js +++ b/app/services/buffer/buffer-service.js @@ -13,10 +13,6 @@ export default /*@ngInject*/ function buffer( profiles: undefined }; - let bufferedMessagesCursor = state.cloud.buffer.select(['messages']); - let bufferedProfilesCursor = state.cloud.buffer.select(['profiles']); - let bufferedInvitesCursor = state.cloud.buffer.select(['invites']); - let sendMessage = function sendMessage(messageId, channelId) { let messageCursor = state.cloud.messages.select([channelId, messageId]); let messageInfo = messageCursor.get(['messageInfo']); @@ -42,8 +38,8 @@ export default /*@ngInject*/ function buffer( sendAttempts.messages[messageId] = $interval(() => sendMessage(messageId, channelId), 20000); return state.save( - bufferedMessagesCursor, - [messageId], + state.cloud.buffer, + ['messages', messageId], { messageId, channelId } ); }; @@ -55,22 +51,70 @@ export default /*@ngInject*/ function buffer( $interval.cancel(sendAttempts.messages[messageId]); sendAttempts.messages[messageId] = undefined; - return state.remove(bufferedMessagesCursor, [messageId]); + return state.remove(state.cloud.buffer, ['messages', messageId]); }; - let addInvite = function addInvite(contactId) { + let sendInvite = function sendInvite(channelId, userInfo) { + let channelCursor = state.cloud.channels.select([channelId]) + let existingChannel = channelCursor.get(); + let channelInfo = existingChannel.channelInfo; + + let handleInviteAck = (ack) => { + let initializeChannel = () => { + if (existingChannel.inviteStatus !== 'accepting') { + return $q.when(); + } + + return state.remove(channelCursor, ['inviteStatus']) + .then(() => channels.initializeChannel(channelInfo)) + .then(() => network.listen(channelInfo)); + }; + return removeInvite(channelInfo.id) + .then(initializeChannel); + }; + + let inviteChannelInfo = { + id: channels.INVITE_CHANNEL_ID, + contactIds: channelInfo.contactIds + }; + + return network.sendInvite(inviteChannelInfo, userInfo) + .then(handleInviteAck); }; - let removeInvite = function removeInvite(contactId) { + let addInvite = function addInvite(channelId) { + let userInfo = state.cloud.identity.get(['userInfo']); + let existingBufferedInvite = sendAttempts.invites[channelId]; + + let removeExistingInvite = existingBufferedInvite ? + removeInvite(channelId) : $q.when(); + + return removeExistingInvite + .then(() => { + sendInvite(channelId, userInfo); + sendAttempts.invites[channelId] = + $interval(() => sendInvite(channelId, userInfo), 20000); + }) + .then(() => state.save( + state.cloud.buffer, + ['invites', channelId], + {channelId} + )); + }; + let removeInvite = function removeInvite(channelId) { + $interval.cancel(sendAttempts.invites[channelId]); + sendAttempts.invites[channelId] = undefined; + + return state.remove(state.cloud.buffer, ['invites', channelId]); }; let sendProfile = function sendProfile(channelId, userInfo) { let channelInfo = state.cloud.channels.get([channelId, 'channelInfo']); let handleProfileAck = (ack) => { - return buffer.removeProfile(channelInfo.id, userInfo.version); + return removeProfile(channelInfo.id); }; return network.sendProfile(channelInfo, userInfo) @@ -79,42 +123,35 @@ export default /*@ngInject*/ function buffer( let addProfile = function addProfile(channelId) { let userInfo = state.cloud.identity.get(['userInfo']); - let profileVersion = userInfo.version; let existingBufferedProfile = sendAttempts.profiles[channelId]; - if (existingBufferedProfile.version >= profileVersion) { - return $q.when(); - } - - sendProfile(channelId, userInfo); - sendAttempts.profiles[channelId] = - $interval(() => sendProfile(channelId, userInfo), 20000); - - return state.save( - bufferedProfilesCursor, - [channelId], - {channelId, profileVersion} - ); + let removeExistingProfile = existingBufferedProfile ? + removeProfile(channelId) : $q.when(); + + return removeExistingProfile + .then(() => { + sendProfile(channelId, userInfo); + sendAttempts.profiles[channelId] = + $interval(() => sendProfile(channelId, userInfo), 20000); + }) + .then(() => state.save( + state.cloud.buffer, + ['profiles', channelId], + {channelId} + )); }; - let removeProfile = function removeProfile(channelId, profileVersion) { - let existingBufferedProfile = sendAttempts.profiles[channelId]; - - if (existingBufferedProfile.version < profileVersion) { - return $q.when(); - } - + let removeProfile = function removeProfile(channelId) { $interval.cancel(sendAttempts.profiles[channelId]); sendAttempts.profiles[channelId] = undefined; - return state.remove(bufferedProfilesCursor, [channelId]); + return state.remove(state.cloud.buffer, ['profiles', channelId]); }; let initialize = function initialize(networkService) { network = networkService; - let bufferedMessagesCursor = state.cloud.buffer.select(['messages']); - let bufferedMessage = bufferedMessagesCursor.get(); + let bufferedMessages = state.cloud.buffer.get(['messages']); sendAttempts.messages = R.mapObj((messageBuffer) => { let channelId = messageBuffer.channelId; @@ -123,7 +160,7 @@ export default /*@ngInject*/ function buffer( //TODO: stagger the initial send and retry intervals sendMessage(messageId, channelId); return $interval(() => sendMessage(messageId, channelId), 20000); - })(bufferedMessage); + })(bufferedMessages); return $q.when(); }; diff --git a/app/services/contacts/contacts-service.js b/app/services/contacts/contacts-service.js index d1a32f07..0213e72c 100644 --- a/app/services/contacts/contacts-service.js +++ b/app/services/contacts/contacts-service.js @@ -4,14 +4,10 @@ export default /*@ngInject*/ function contacts( buffer, channels, identity, - network, R, state ) { - let saveReceivedProfile = function saveReceivedProfile( - profilePayload, channelId - ) { - let contactInfo = profilePayload; + let saveContactInfo = function saveContactInfo(contactInfo) { let contactCursor = state.cloud.contacts.select([contactInfo.id]); let existingContactInfo = contactCursor.get(['userInfo']); @@ -20,116 +16,87 @@ export default /*@ngInject*/ function contacts( } return state.save(contactCursor, ['userInfo'], contactInfo); - - // if (channelId !== channels.INVITE_CHANNEL_ID) { - // - // } - // - // return state.save(contactCursor, ['userInfo'], contactInfo) - // .then(() => state.save(contactCursor, ['statusId'], 1)) - // .then(() => state.save( - // state.cloud.channels, - // [channel.id, 'channelInfo'], - // channel - // )) - // - // if (!existingContactInfo) { - // - // } - // // otherwise it's a sent invite - }; - - let saveAcceptingInvite = function saveAcceptingInvite(channelId) { - }; - let saveSendingInvite = function saveSendingInvite(contactId) { - let contactChannel = channels.createContactChannel(userInfo.id, contactId); - let contactCursor = state.cloud.channels.select([contactId]); - let channelCursor = state.cloud.channels.select([contactChannel.id]); - - let contactInfo = { - version: 0, - id: contactId - }; - - return buffer.addInvite(contactId) - .then(() => state.save(contactCursor, ['userInfo'], contactInfo)) - .then(() => state.save(channelCursor, ['channelInfo'], contactChannel)) - .then(() => state.save(channelCursor, ['sendingInvite'], true)); - // .then(() => channels.initializeChannel(contactChannel)) - // .then(() => network.listen(contactChannel)); + let saveReceivedProfile = function saveReceivedProfile(profilePayload) { + let contactInfo = profilePayload; + return saveContactInfo(contactInfo); }; let saveSendingProfile = function saveSendingProfile(channelId) { return buffer.addProfile(channelId); }; - let invite = function inviteContact(contactId) { - let userInfo = state.cloud.identity.get('userInfo'); - let contactChannel = channels.createContactChannel(userInfo.id, contactId); + let saveReceivedInvite = function saveReceivedInvite(invitePayload) { + let contactInfo = invitePayload; - let existingContact = state.cloud.contacts - .get([contactId, 'userInfo']); + let userId = state.cloud.identity.get(['userInfo']).id; + let newChannelInfo = channels.createContactChannel(userId, contactInfo.id); + let contactCursor = state.cloud.contacts.select([contactInfo.id]); + let channelCursor = state.cloud.channels.select([newChannelInfo.id]); - let contact = existingContact || { - id: contactId, - displayName: 'Invite sent' - }; + let existingChannel = channelCursor.get(); - const MAX_ATTEMPTS = 3; - let attemptCount = 0; + if (existingChannel && existingChannel.inviteStatus === 'received') { + return saveContactInfo(contactInfo) + .then(() => state.remove(channelCursor, ['inviteStatus'])) + .then(() => channels.initializeChannel(existingChannel.channelInfo)) + .then(() => network.listen(existingChannel.channelInfo)); + } - let recursivelySendInvite = () => { - return network.sendInvite(contactId, userInfo) - .catch((error) => { - if (error !== 'timeout') { - return $q.reject(error); - } + return saveContactInfo(contactInfo) + .then(() => state.save(contactCursor, ['statusId'], 1)) + .then(() => state.save(channelCursor, ['channelInfo'], newChannelInfo)) + .then(() => state.save(channelCursor, ['inviteStatus'], 'received')); + }; - attemptCount++; - if (attemptCount === MAX_ATTEMPTS) { - return $q.reject('Invite request has timed out.'); - } + let saveAcceptingInvite = function saveAcceptingInvite(channelId) { + let channelCursor = state.cloud.channels.select([channelId]); + let channelInfo = channelCursor.get(['channelInfo']); - return recursivelySendInvite(); - }); + return state.save(channelCursor, ['inviteStatus'], 'accepting') + .then(() => buffer.addInvite(channelId)); + }; + + let saveSendingInvite = function saveSendingInvite(contactId) { + let contactChannel = channels.createContactChannel(userInfo.id, contactId); + let contactCursor = state.cloud.contacts.select([contactId]); + let channelCursor = state.cloud.channels.select([contactChannel.id]); + + let contactInfo = { + version: 0, + id: contactId }; - return recursivelySendInvite() - .then(() => state.save( - state.cloud.contacts, - [contactId, 'userInfo'], - contact - )) - .then(() => state.save( - state.cloud.channels, - [contactChannel.id, 'channelInfo'], - contactChannel - )) - .then(() => channels.initializeChannel(contactChannel)) - .then(() => network.listen(contactChannel)) - .then(() => contactChannel); + return state.save(contactCursor, ['userInfo'], contactInfo) + .then(() => state.save(channelCursor, ['channelInfo'], contactChannel)) + .then(() => state.save(channelCursor, ['inviteStatus'], 'sending')) + .then(() => buffer.addInvite(channelId)); }; let initialize = function initializeContacts() { - let contactsCursor = state.cloud.contacts; + let allContacts = state.cloud.contacts.get(); - R.pipe( + let settingContactsToOffline = R.pipe( R.values, R.reject(R.propEq('statusId', 0)), - R.forEach((contact) => state.save( + R.map((contact) => state.save( contactsCursor, [contact.userInfo.id, 'statusId'], 0 )) - )(contactsCursor.get()); + )(allContacts); - return $q.when(); + return $q.all(settingContactsToOffline); }; return { - invite, + saveContactInfo, + saveReceivedProfile, + saveSendingProfile, + saveReceivedInvite, + saveAcceptingInvite, + saveSendingInvite, initialize }; } diff --git a/app/services/network/network-service.js b/app/services/network/network-service.js index 0d3f68ca..6d28624b 100644 --- a/app/services/network/network-service.js +++ b/app/services/network/network-service.js @@ -4,6 +4,7 @@ export default /*@ngInject*/ function network( $q, $window, channels, + contacts, notifications, messages, R, @@ -21,35 +22,12 @@ export default /*@ngInject*/ function network( throw new Error('network: no active session'); }; - let handleProfile = function handleProfile(profilePayload, channelId) { - let contactInfo = profilePayload; - - let userId = state.cloud.identity.get(['userInfo']).id; - let channel = channels.createContactChannel(userId, contactInfo.id); - - // if channel already exists, this profile packet indicates acceptance - let existingChannel = state.cloud.channels - .get([channel.id, 'channelInfo']); - - let statusId = 1; //online - - let receivedProfile = !existingChannel; - - return - .then(() => { - if (receivedProfile) { - return state.save( - state.cloud.channels, - [channel.id, 'receivedInvite'], - true - ); - } + let handleInvite = function handleInvite(invitePayload) { + return contacts.saveReceivedInvite(invitePayload); + }; - return state.remove( - state.cloud.channels, - [channel.id, 'sentInvite'] - ); - }); + let handleProfile = function handleProfile(profilePayload) { + return contacts.saveReceivedProfile(profilePayload); }; let handleStatus = function handleStatus(statusPayload, contactId) { @@ -88,6 +66,7 @@ export default /*@ngInject*/ function network( let sentTime = packet.js.t; let ackPayload = packet.js.a; + let invitePayload = packet.js.i; let profilePayload = packet.js.p; let statusPayload = packet.js.s; let messagePayload = packet.js.m; @@ -107,8 +86,10 @@ export default /*@ngInject*/ function network( if (ackPayload) { return $q.when(); + } else if (invitePayload) { + return handleInvite(invitePayload); } else if (profilePayload) { - return handleProfile(profilePayload, channelId); + return handleProfile(profilePayload); } else if (statusPayload !== undefined) { return handleStatus(statusPayload, senderId); } else if (messagePayload) { @@ -189,6 +170,15 @@ export default /*@ngInject*/ function network( ); }; + let sendInvite = function sendInvite(channelInfo, userInfo) { + let payload = { + i: userInfo, + t: time.getTime() + }; + + return send(channelInfo, payload); + }; + let sendProfile = function sendProfile(channelInfo, userInfo) { let payload = { p: userInfo, From b4794e5c174d637779455641383a11aec90f5666 Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Fri, 4 Sep 2015 01:51:57 -0700 Subject: [PATCH 15/21] Added buffered profile updates on save Added some logging for buffered sends --- .../update-profile-modal-directive.js | 6 +++++- app/services/buffer/buffer-service.js | 20 +++++++++++++++++-- app/services/contacts/contacts-service.js | 13 ++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/components/update-profile-modal/update-profile-modal-directive.js b/app/components/update-profile-modal/update-profile-modal-directive.js index 0e07fd3b..34a2e9f9 100644 --- a/app/components/update-profile-modal/update-profile-modal-directive.js +++ b/app/components/update-profile-modal/update-profile-modal-directive.js @@ -12,6 +12,8 @@ export default /*@ngInject*/ function tocUpdateProfileModal() { controller: /*@ngInject*/ function UpdateProfileModalController( $scope, $q, + $log, + contacts, identity, state, R @@ -30,10 +32,12 @@ export default /*@ngInject*/ function tocUpdateProfileModal() { ['userInfo'], this.userInfo ) + .then(() => contacts.saveProfileUpdates()) .then(() => { this.removeModal(); return $q.when(); - }); + }) + .catch($log.error); }; } }; diff --git a/app/services/buffer/buffer-service.js b/app/services/buffer/buffer-service.js index 95658b0b..bac1d16e 100644 --- a/app/services/buffer/buffer-service.js +++ b/app/services/buffer/buffer-service.js @@ -2,11 +2,12 @@ export let serviceName = 'buffer'; export default /*@ngInject*/ function buffer( $interval, $q, + $log, state, R ) { let network; - + //TODO: don't anything in buffer if contact status is offline let sendAttempts = { messages: undefined, invites: undefined, @@ -25,6 +26,7 @@ export default /*@ngInject*/ function buffer( .then(() => removeMessage(messageInfo.id)); }; + $log.debug(`sending message ${messageInfo.content} to ${channelId}`); return network.sendMessage(channelInfo, messageInfo) .then(handleMessageAck); }; @@ -79,6 +81,7 @@ export default /*@ngInject*/ function buffer( contactIds: channelInfo.contactIds }; + $log.debug(`sending invite v${userInfo.version} to ${channelId}`); return network.sendInvite(inviteChannelInfo, userInfo) .then(handleInviteAck); }; @@ -117,6 +120,7 @@ export default /*@ngInject*/ function buffer( return removeProfile(channelInfo.id); }; + $log.debug(`sending profile v${userInfo.version} to ${channelId}`); return network.sendProfile(channelInfo, userInfo) .then(handleProfileAck); }; @@ -162,12 +166,24 @@ export default /*@ngInject*/ function buffer( return $interval(() => sendMessage(messageId, channelId), 20000); })(bufferedMessages); + let bufferedProfiles = state.cloud.buffer.get(['profiles']); + let userInfo = state.cloud.identity.get(['userInfo']); + + sendAttempts.profiles = R.mapObj((profileBuffer) => { + let channelId = profileBuffer.channelId; + + //TODO: stagger the initial send and retry intervals + sendProfile(channelId, userInfo); + return $interval(() => sendProfile(channelId, userInfo), 20000); + })(bufferedProfiles); + return $q.when(); }; return { addMessage, - removeMessage, + addProfile, + addInvite, initialize }; } diff --git a/app/services/contacts/contacts-service.js b/app/services/contacts/contacts-service.js index 0213e72c..7fc5ac57 100644 --- a/app/services/contacts/contacts-service.js +++ b/app/services/contacts/contacts-service.js @@ -74,6 +74,18 @@ export default /*@ngInject*/ function contacts( .then(() => buffer.addInvite(channelId)); }; + let saveProfileUpdates = function saveProfileUpdates() { + let userInfo = state.cloud.identity.get('userInfo'); + let existingChannels = state.cloud.channels.get(); + + let savingSendingProfiles = R.pipe( + R.values, + R.map((channel) => saveSendingProfile(channel.channelInfo.id)) + ) (existingChannels); + + return $q.all(savingSendingProfiles); + }; + let initialize = function initializeContacts() { let allContacts = state.cloud.contacts.get(); @@ -97,6 +109,7 @@ export default /*@ngInject*/ function contacts( saveReceivedInvite, saveAcceptingInvite, saveSendingInvite, + saveProfileUpdates, initialize }; } From 409f4dfcb4fefa7c00a50783391fb60b8c15f27c Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Fri, 4 Sep 2015 02:20:39 -0700 Subject: [PATCH 16/21] Fixed buffered message indicator not clearing --- app/components/message-list/message-list.html | 8 ++++---- app/components/message-list/message-list.scss | 14 +++----------- app/services/network/network-service.js | 3 +-- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/app/components/message-list/message-list.html b/app/components/message-list/message-list.html index fb3424b8..7c290ad5 100644 --- a/app/components/message-list/message-list.html +++ b/app/components/message-list/message-list.html @@ -10,15 +10,15 @@ Avatar for {{messageList.getUserInfo(message).email}} -
- {{::messageList.getDatestamp(message)}} -
+
+ {{::messageList.getDatestamp(message)}} +

{{messageList.getUserInfo(message).displayName || 'Anonymous'}}

diff --git a/app/components/message-list/message-list.scss b/app/components/message-list/message-list.scss index 383c404c..16e7af7d 100644 --- a/app/components/message-list/message-list.scss +++ b/app/components/message-list/message-list.scss @@ -101,19 +101,11 @@ } .toc-message-user { - h2 { - color: lighten($dark, 20); - } - p { - color: lighten($dark, 20); + > :not(.toc-message-datestamp){ + opacity: 0.8; } } .toc-message-contact { - h2 { - color: $dark; - } - p { - color: $dark; - } + } diff --git a/app/services/network/network-service.js b/app/services/network/network-service.js index 6d28624b..e20fea56 100644 --- a/app/services/network/network-service.js +++ b/app/services/network/network-service.js @@ -229,8 +229,7 @@ export default /*@ngInject*/ function network( t: messageInfo.sentTime }; - return send(channelInfo, payload) - .then(handleMessageAck); + return send(channelInfo, payload); }; let initialize = function initializeNetwork() { From 56ae46a151a198875f9810bf4c0ae3206c423845 Mon Sep 17 00:00:00 2001 From: Lewis Liu Date: Fri, 4 Sep 2015 04:34:38 -0700 Subject: [PATCH 17/21] Refined invite sending status updates behavior --- .../begin-conversation-modal-directive.js | 29 ++++---------- .../channel-card/channel-card-directive.js | 38 +++++++++++++++++-- app/components/channel-card/channel-card.html | 7 ++-- app/components/channel-card/channel-card.scss | 13 ++++++- .../channel-list/channel-list-directive.js | 7 +++- app/services/buffer/buffer-service.js | 20 ++++++++-- app/services/channels/channels-service.js | 6 ++- app/services/contacts/contacts-service.js | 17 ++++++--- app/services/devices/devices-service.js | 8 +++- app/services/messages/messages-service.js | 7 +++- app/services/network/network-service.js | 1 + .../notifications/notifications-service.js | 8 +++- app/services/session/session-service.js | 13 +++++-- app/services/status/status-service.js | 19 +++++----- 14 files changed, 133 insertions(+), 60 deletions(-) diff --git a/app/components/begin-conversation-modal/begin-conversation-modal-directive.js b/app/components/begin-conversation-modal/begin-conversation-modal-directive.js index 09fde561..deefcd19 100644 --- a/app/components/begin-conversation-modal/begin-conversation-modal-directive.js +++ b/app/components/begin-conversation-modal/begin-conversation-modal-directive.js @@ -28,23 +28,6 @@ export default /*@ngInject*/ function tocBeginConversationModal() { this.contactId = ''; - let initializeSentInvite = (contactChannel) => { - return state.save( - state.cloud.channels, - [contactChannel.id, 'sentInvite'], - true - ) - .then(() => state.save( - state.cloud.contacts, - [this.contactId, 'statusId'], - 0 - )) - .then(() => { - this.removeModal(); - return $q.when(); - }); - }; - this.inviteMethod = 'enter'; this.inviteMethods = { @@ -75,10 +58,11 @@ export default /*@ngInject*/ function tocBeginConversationModal() { return; } - return contacts.invite(this.contactId) - .then(initializeSentInvite) + return contacts.saveSendingInvite(this.contactId) .then(() => { + this.removeModal(); this.contactId = ''; + return $q.when(); }); } } @@ -102,9 +86,12 @@ export default /*@ngInject*/ function tocBeginConversationModal() { return $q.reject(`${contactId} is not a valid Toc ID.`); } - return contacts.invite(contactId); + return contacts.saveSendingInvite(contactId); + }) + .then(() => { + this.removeModal(); + return $q.when(); }) - .then(initializeSentInvite) .catch((error) => $log.error(error)); } diff --git a/app/components/channel-card/channel-card-directive.js b/app/components/channel-card/channel-card-directive.js index ddb0c665..b7eb87c6 100644 --- a/app/components/channel-card/channel-card-directive.js +++ b/app/components/channel-card/channel-card-directive.js @@ -25,16 +25,46 @@ export default /*@ngInject*/ function tocChannelCard() { this.contact = contactCursor.get( this.channel.channelInfo.contactIds[0] ); + updateStatus(); + }; + + let updateStatus = () => { + if (this.contact.statusId === -1) { + this.status = 'toc-contact-status-pending'; + return; + } + + if (this.contact.statusId === 0) { + this.status = 'toc-contact-status-offline'; + return; + } + + if (this.contact.statusId === 1) { + this.status = 'toc-contact-status-online'; + return; + } + + this.status = 'toc-contact-status-unknown'; }; let updateQuote = () => { - if (this.channel.sentInvite) { - this.quote = `ID: ${this.channel.channelInfo.contactIds[0]}`; + if (this.channel.inviteStatus === 'accepting') { + this.quote = `Accepting invite`; + return; + } + + if (this.channel.inviteStatus === 'sending') { + this.quote = `Sending invite`; + return; + } + + if (this.channel.inviteStatus === 'sent') { + this.quote = `Sent invite`; return; } - if (this.channel.receivedInvite) { - this.quote = 'New invite'; + if (this.channel.inviteStatus === 'received') { + this.quote = 'Received invite'; return; } diff --git a/app/components/channel-card/channel-card.html b/app/components/channel-card/channel-card.html index 3c517873..8b8b16ca 100644 --- a/app/components/channel-card/channel-card.html +++ b/app/components/channel-card/channel-card.html @@ -1,12 +1,11 @@
+ 'toc-channel-pending': channelCard.contact.statusId === -1 + }">
+ ng-class="channelCard.status">

{{channelCard.contact.userInfo.displayName || 'Anonymous'}}