First pass of working invitations

Change-Id: I62e37bfa0d7d6156b622df5723acb3ae38cd3348
diff --git a/src/invitation-manager.js b/src/invitation-manager.js
index e1a26a4..1323a53 100644
--- a/src/invitation-manager.js
+++ b/src/invitation-manager.js
@@ -6,6 +6,7 @@
 
 var $ = require('./util/jquery');
 var defineClass = require('./util/define-class');
+var debug = require('./debug');
 
 // TODO(rosswang): generalize this
 var ESC = {
@@ -31,42 +32,24 @@
   });
 }
 
-function invitationKey(owner, recipient) {
-  return ['invitations', escapeUsername(owner), escapeUsername(recipient)];
+function invitationKey(recipient, owner, tripId) {
+  return [
+    'invitations',
+    escapeUsername(recipient),
+    escapeUsername(owner),
+    tripId
+  ];
 }
 
 var InvitationManager = defineClass({
   publics: {
-    accept: function(owner) {
-    },
-
-    decline: function(owner) {
-      return Promise.all([
-        this.syncbasePromise,
-        this.prereqs
-      ]).then(function(args) {
-        var syncbase = args[0];
-        var username = args[1].identity.username;
-
-        return syncbase.delete(invitationKey(owner, username));
-      });
-    },
-
-    getActiveInvite: function() {
-      return this.activeInvite;
-    },
-
-    invite: function(username) {
+    invite: function(recipient, owner, tripId) {
       var self = this;
 
       return this.groupManagerPromise.then(function(gm) {
-        return gm.joinSyncGroup(username, 'invitations').then(function() {
-          return self.prereqs;
-        }).then(function(prereqs) {
-          var owner = self.activeInvite || prereqs.identity.username;
-
-          return gm.syncbaseWrapper.put(invitationKey(owner, username),
-            prereqs.identity.username);
+        return gm.joinSyncGroup(recipient, 'invitations').then(function() {
+          return gm.syncbaseWrapper.put(invitationKey(recipient, owner, tripId),
+            self.username);
         });
       });
     },
@@ -77,55 +60,103 @@
   },
 
   privates: {
+    invitation: defineClass.innerClass({
+      publics: {
+        accept: function() {
+          var self = this;
+
+          return this.outer.groupManagerPromise.then(function(gm) {
+            return gm.joinSyncGroup(self.owner, 'trip').then(function() {
+              return self.decline();
+            });
+          });
+        },
+
+        decline: function() {
+          var self = this;
+
+          var username = this.outer.username;
+          return this.outer.syncbasePromise.then(function(syncbase) {
+            return syncbase.delete(invitationKey(
+              username, self.owner, self.tripId));
+          });
+        },
+      },
+
+      constants: [ 'owner', 'tripId', 'sender' ],
+
+      events: {
+        onDismiss: 'memory once'
+      },
+
+      init: function(owner, tripId, sender, callbacks) {
+        this.owner = owner;
+        this.tripId = tripId;
+        this.sender = sender;
+        callbacks.dismiss = this.onDismiss;
+      }
+    }),
+
     processUpdates: function(data) {
       var self = this;
 
-      if (data.invitations) {
-        $.each(data.invitations, function(owner, record) {
+      var toMe;
+      if (data.invitations &&
+          (toMe = data.invitations[escapeUsername(this.username)])) {
+        $.each(toMe, function(owner, ownerRecords) {
           var ownerInvites = self.invitations[owner];
           if (!ownerInvites) {
             ownerInvites = self.invitations[owner] = {};
           }
 
-          $.each(record, function(recipient, sender) {
-            if (ownerInvites[recipient]) {
-              delete ownerInvites[recipient];
+          var uOwner;
+
+          $.each(ownerRecords, function(tripId, sender) {
+            var record = ownerInvites[tripId];
+            if (record) {
+              record.seen = true;
             } else {
-              self.onInvite(unescapeUsername(owner),
-                unescapeUsername(recipient), sender);
+              if (!uOwner) {
+                uOwner = unescapeUsername(owner);
+              }
+
+              debug.log('Received invite from ' + sender + ' to ' + uOwner +
+                ':' + tripId);
+
+              var callbacks = {};
+              var invite = self.invitation(uOwner, tripId, sender, callbacks);
+              ownerInvites[tripId] = {
+                invite: invite,
+                dismiss: callbacks.dismiss,
+                seen: true
+              };
+              self.onInvite(invite);
             }
           });
         });
       }
 
       if (this.invitations) {
-        $.each(this.invitations, function(owner, record) {
-          $.each(record, function(recipient, sender) {
-            self.onDismiss(unescapeUsername(owner),
-              unescapeUsername(recipient), sender);
+        $.each(this.invitations, function(owner, ownerRecords) {
+          $.each(ownerRecords, function(tripId, record) {
+            if (record.seen) {
+              delete record.seen;
+            } else {
+              delete ownerRecords[tripId];
+              record.dismiss();
+            }
           });
         });
       }
-
-      this.invitations = data.invitations || {};
     }
   },
 
   events: {
     /**
-     * @param owner the user who owns the trip.
-     * @param recipient the user invited to the trip.
-     * @param sender the user who sent the invitation.
+     * @param invitation
      */
     onInvite: '',
 
-    /**
-     * @param owner the user who owns the trip.
-     * @param recipient the user invited to the trip.
-     * @param sender the user who sent the invitation.
-     */
-    onDismiss: '',
-
     onError: 'memory'
   },
 
@@ -145,11 +176,13 @@
     this.invitations = {};
 
     prereqs.then(function(args) {
+      //this will have been set prior to groupManagerPromise completing
       self.username = args.identity.username;
     });
 
     groupManagerPromise.then(function(gm) {
-      gm.createSyncGroup('invitations', ['invitations'])
+      gm.createSyncGroup('invitations',
+          [['invitations', escapeUsername(self.username)]])
         .catch(self.onError);
     });
   }
diff --git a/src/travel.js b/src/travel.js
index e595521..3e86d43 100644
--- a/src/travel.js
+++ b/src/travel.js
@@ -86,6 +86,27 @@
         text: info,
         promise: promise
       }));
+    },
+
+    invite: function(recipient) {
+      var self = this;
+
+      this.info(strings.sendingInvite(recipient),
+        this.sync.invitationManager.invite(recipient,
+            this.sync.getActiveTripOwner(), this.sync.getActiveTripId())
+          .then(function() {
+            var me = self.sync.invitationManager.getUsername();
+            self.sync.message({
+              type: Message.INFO,
+              text: strings.invitationSent(recipient, me)
+            });
+          }, function(err) {
+            if (err.id === 'v.io/v23/verror.NoServers') {
+              throw strings.notReachable(recipient);
+            } else {
+              throw err;
+            }
+          }));
     }
   },
 
@@ -279,55 +300,40 @@
       }
     },
 
-    dismissInvite: function(owner, sender) {
-      var invite = this.invites[owner];
-      if (invite) {
-        invite.resolve(strings.invitationDismissed(sender, owner));
-        delete this.invites[owner];
-      }
-    },
-
-    handleInvite: function(owner, recipient, sender) {
+    handleInvite: function(invitation) {
       var self = this;
       var invitationManager = this.sync.invitationManager;
-      var me = invitationManager.getUsername();
 
-      if (recipient === me) {
-        var async = new Deferred();
+      var sender = invitation.sender;
+      var owner = invitation.owner;
 
-        this.dismissInvite(owner, sender);
-        this.invites[owner] = async;
+      var async = new Deferred();
 
-        var message = new Message({
-          type: Message.INFO,
-          html: strings.invitationReceived(sender, owner),
-          promise: async.promise
-        });
+      var message = new Message({
+        type: Message.INFO,
+        html: strings.invitationReceived(sender, owner),
+        promise: async.promise
+      });
 
-        message.$.find('a[name=accept]').click(function() {
-          invitationManager.accept(owner).then(function() {
-            delete self.invites[owner];
-            return strings.invitationAccepted(sender, owner);
-          }).then(async.resolve, async.reject);
-          return false;
-        });
-        message.$.find('a[name=decline]').click(function() {
-          invitationManager.decline(owner).then(function() {
-            delete self.invites[owner];
-            return strings.invitationDeclined(sender, owner);
-          }).then(async.resolve, async.reject);
-          return false;
-        });
+      message.$.find('a[name=accept]').click(function() {
+        invitation.accept().then(function() {
+          self.sync.watchForTrip(invitation.tripId);
+          return strings.invitationAccepted(sender, owner);
+        }).then(async.resolve, async.reject);
+        return false;
+      });
+      message.$.find('a[name=decline]').click(function() {
+        invitationManager.decline().then(function() {
+          return strings.invitationDeclined(sender, owner);
+        }).then(async.resolve, async.reject);
+        return false;
+      });
 
-        self.messages.push(message);
-      }
-    },
+      invitation.onDismiss.add(function() {
+        async.resolve(strings.invitationDismissed(sender, owner));
+      });
 
-    handleInviteDismiss: function(owner, recipient, sender) {
-      var me = this.sync.invitationManager.getUsername();
-      if (recipient === me) {
-        this.dismissInvite(owner, sender);
-      }
+      this.messages.push(message);
     },
 
     handleUserMessage: function(message, raw) {
@@ -376,8 +382,6 @@
     opts = opts || {};
     var vanadiumWrapper = opts.vanadiumWrapper || vanadiumWrapperDefault;
 
-    this.invites = {};
-
     var destinations = this.destinations = new Destinations();
     destinations.onAdd.add(this.handleDestinationAdd);
     destinations.onRemove.add(this.handleDestinationRemove);
@@ -437,7 +441,6 @@
     });
 
     sync.invitationManager.onInvite.add(this.handleInvite);
-    sync.invitationManager.onDismiss.add(this.handleInviteDismiss);
 
     messages.onMessage.add(this.handleUserMessage);
 
@@ -522,23 +525,7 @@
 
     this.commands = {
       invite: {
-        op: function(username) {
-          this.info(strings.sendingInvite(username),
-            this.sync.invitationManager.invite(username)
-              .then(function() {
-                var me = self.sync.invitationManager.getUsername();
-                self.sync.message({
-                  type: Message.INFO,
-                  text: strings.invitationSent(username, me)
-                });
-              }, function(err) {
-                if (err.id === 'v.io/v23/verror.NoServers') {
-                  throw strings.notReachable(username);
-                } else {
-                  throw err;
-                }
-              }));
-        }
+        op: this.invite
       },
 
       status: {
diff --git a/src/travelsync.js b/src/travelsync.js
index 7b217dc..e3406ac 100644
--- a/src/travelsync.js
+++ b/src/travelsync.js
@@ -61,7 +61,15 @@
     pushStatus: function() {
     },
 
-    setActiveTrip: function(tripId) {
+    getActiveTripId: function() {
+      return this.activeTripId;
+    },
+
+    getActiveTripOwner: function() {
+      return this.activeTripOwner;
+    },
+
+    setActiveTripId: function(tripId, pull) {
       var self = this;
 
       this.activeTripId = tripId;
@@ -71,8 +79,15 @@
         syncbase.put(['user', 'tripMetadata', tripId, 'latestSwitch'],
           Date.now()).catch(self.onError);
 
-        return syncbase.refresh();
+        return pull? syncbase.refresh() : Promise.resolve();
       });
+    },
+
+    /**
+     * Sets the active trip to the given trip ID after it is available.
+     */
+    watchForTrip: function(tripId) {
+      this.awaitedTripId = tripId;
     }
   },
 
@@ -485,8 +500,21 @@
     },
 
     processTrips: function(userTripMetadata, trips) {
+      var self = this;
       var trip;
 
+      if (this.awaitedTripId) {
+        this.setActiveTripId(this.awaitedTripId, false);
+        delete this.awaitedTripId;
+
+        /* Override isNascent check this frame. (Subsequently syncbase will be
+         * up to date.) */
+        if (!userTripMetadata) {
+          userTripMetadata = {};
+        }
+        userTripMetadata[this.activeTripId].latestSwitch = Date.now();
+      }
+
       if (this.activeTripId) {
         trip = trips && trips[this.activeTripId];
         if (!trip) {
@@ -514,10 +542,17 @@
         } else {
           this.activeTripId = uuid.v4();
           debug.log('Creating new trip ' + this.activeTripId);
-          trip = {};
+          trip = {
+            owner: this.invitationManager.getUsername()
+          };
+          this.startSyncbase.then(function(syncbase) {
+            syncbase.put(['trips', self.activeTripId, 'owner'], trip.owner)
+              .catch(self.onError);
+          });
         }
       }
 
+      this.activeTripOwner = trip.owner;
       this.processMessages(trip.messages);
       this.processDestinations(trip.destinations);
     },
@@ -586,7 +621,7 @@
       var self = this;
 
       this.status.tripSyncGroup = 'creating';
-      return groupManager.createSyncGroup('trip', [''])
+      return groupManager.createSyncGroup('trip', [[]])
         .then(function(sg) {
           self.status.tripSyncGroup = 'created';
           return sg;
diff --git a/src/vanadium-wrapper/syncbase-wrapper.js b/src/vanadium-wrapper/syncbase-wrapper.js
index c8b7fb6..b6f63ae 100644
--- a/src/vanadium-wrapper/syncbase-wrapper.js
+++ b/src/vanadium-wrapper/syncbase-wrapper.js
@@ -213,7 +213,7 @@
               ['Resolve', {in: ['...']}],
               ['Debug', {in: ['...']}]
             ]),
-            prefixes: prefixes.map(function(p) { return 't:' + p; }),
+            prefixes: prefixes.map(function(p) { return 't:' + joinKey(p); }),
             mountTables: mountTables
           });
         },