<|start_filename|>app/stores/match.js<|end_filename|>
'use strict';

var _ = require('lodash');
var Fluxxor = require('fluxxor');

var log = require('bows')('Match Store');

var constants = require('../action-constants');
var Common = require('./common');

var MatchStore = module.exports = Fluxxor.createStore({
  initialize: function() {
    this.models = [];

    this.isLoading = false;
    this.hasUnseen = false;
    this.error = null;
    this.createCompleteWaiting = [];
    this.deleteCompleteWaiting = [];

    this.bindActions(
      constants.MATCH_LOAD_PENDING, this.handleLoadPending,
      constants.MATCH_LOAD_COMPLETE, this.handleLoadComplete,
      constants.MATCH_GET_PENDING, this.handleGetPending,
      constants.MATCH_GET_COMPLETE, this.handleGetComplete,
      constants.MATCH_CREATE_PENDING, this.handleCreatePending,
      constants.MATCH_CREATE_COMPLETE, this.handleCreateComplete,
      constants.MATCH_CREATE_REMOTE, this.handleCreateRemote,
      constants.MATCH_UPDATE_REMOTE, this.handleUpdateRemote,
      constants.MATCH_DELETE_PENDING, this.handleDeletePending,
      constants.MATCH_DELETE_COMPLETE, this.handleDeleteComplete,
      constants.MATCH_DELETE_REMOTE, this.handleDeleteRemote,
      constants.MESSAGE_CREATE_REMOTE, this.handleMessageCreateRemote
    );

    log('INIT', this);
  },

  handleLoadPending: Common.handleLoadPending,
  handleLoadComplete: Common.handleLoadComplete,
  handleGetPending: Common.handleGetPending,
  handleGetComplete(payload) {
    if (payload.error) {
      return Common.handleGetComplete.call(this, payload);
    }

    return Common.handleGetComplete.call(this, {model: payload.model.person});
  },
  handleUpdateRemote: Common.handleUpdateRemote,
  handleDeletePending: Common.handleDeletePending,
  handleDeleteComplete: Common.handleDeleteComplete,
  handleDeleteRemote: Common.handleDeleteRemote,
  get: Common.get,
  getBy: Common.getBy,
  getAll: Common.getAll,
  getState: Common.getState,

  handleCreatePending(payload) {
    var model = _.extend({}, payload.attributes.person);

    this.createCompleteWaiting.push({
      operationId: payload.operationId,
      model: model,
    });
  },

  handleCreateComplete(payload) {
    var waiting = _.findWhere(this.createCompleteWaiting, {
      operationId: payload.operationId,
    });

    if (!waiting) {
      return;
    }

    this.createCompleteWaiting.splice(this.createCompleteWaiting.indexOf(waiting), 1);

    if (payload.model && payload.model.status === "Matched with other user") {
      alert('You and ' + waiting.model.login + ' are a match!');

      this.hasUnseen = true;
      waiting.model.isUnseen = true;

      this.models.push(waiting.model);

      this.emit('change');
    }
  },

  handleCreateRemote(payload) {
    var model = _.findWhere(this.models, {id: payload.model.id});

    if (!model) {
      this.models.push(model = {});

      this.hasUnseen = true;
      model.isUnseen = true;
    }

    _.extend(model, payload.model);

    this.emit('change');
  },

  handleMessageCreateRemote(payload) {
    console.log(payload);

    var model = _.findWhere(this.models, {id: payload.model.otherUserId});

    console.log(model);

    if (!model) {
      return;
    }

    model.isUnseen = true;

    this.calculateUnseen();
  },

  markSeen(id) {
    var model = this.get(id);

    if (!model) {
      return;
    }

    delete model.isUnseen;

    this.calculateUnseen();
  },

  calculateUnseen() {
    var hasUnseen = false;
    this.models.forEach(function(model) {
      hasUnseen = hasUnseen || model.isUnseen;
    });

    console.log("unseen", this.hasUnseen, "->", hasUnseen);

    if (this.hasUnseen !== hasUnseen) {
      this.hasUnseen = hasUnseen;
      this.emit('change');
    }
  },
});


<|start_filename|>app/stores/person.js<|end_filename|>
'use strict';

var _ = require('lodash');
var Fluxxor = require('fluxxor');

var log = require('bows')('Person Store');

var constants = require('../action-constants');
var Common = require('./common');

var PersonStore = module.exports = Fluxxor.createStore({
  initialize: function() {
    this.models = [];

    this.isLoading = false;
    this.error = null;

    this.bindActions(
      constants.PERSON_GET_PENDING, this.handleGetPending,
      constants.PERSON_GET_COMPLETE, this.handleGetComplete
    );

    log('INIT', this);
  },

  handleGetPending: Common.handleGetPending,
  handleGetComplete(payload) {
    if (payload.error) {
      return Common.handleGetComplete.call(this, payload);
    }

    return Common.handleGetComplete.call(this, {model: payload.model.person});
  },
  get: Common.get,
  getBy: Common.getBy,
  getAll: Common.getAll,
  getState: Common.getState,
});


<|start_filename|>app/stores/common.js<|end_filename|>
'use strict';

var _ = require('lodash');

var Common = {
  handleLoadPending: function(payload) {
    this.isLoading = true;

    this.emit('change');
  },

  handleLoadComplete: function(payload) {
    this.isLoading = false;

    if (payload.error) {
      this.error = payload.error;

      this.emit('change');

      return;
    }

    _.each(payload.models, function(newModel) {
      var model = _.findWhere(this.models, {id: newModel.id});

      if (!model) {
        this.models.push(model = {});
      }

      _.extend(model, newModel);
    }, this);

    this.emit('change');
  },

  handleGetPending: function(payload) {
    this.isLoading = true;

    var model = _.findWhere(this.models, {id: payload.id});

    if (model) {
      _.extend(model, {
        isLoading: true,
      });
    }

    this.emit('change');
  },

  handleGetComplete: function(payload) {
    this.isLoading = false;

    var model = _.findWhere(this.models, {id: payload.model.id});

    if (!model) {
      this.models.push(model = _.extend({}, payload.model));
    }

    delete model.isLoading;

    this.emit('change');
  },

  handleCreatePending: function(payload) {
    var model = _.extend({}, payload.attributes, {
      isNew: true,
    });

    this.createCompleteWaiting.push({
      operationId: payload.operationId,
      model: model,
    });

    this.models.push(model);

    this.emit('change');
  },

  handleCreateComplete: function(payload) {
    var waiting = _.findWhere(this.createCompleteWaiting, {
      operationId: payload.operationId,
    });

    if (!waiting) {
      // TODO: error handling
      return;
    }

    this.createCompleteWaiting.splice(this.createCompleteWaiting.indexOf(waiting), 1);

    _.extend(waiting.model, payload.model, {
      isNew: false,
    });

    this.emit('change');
  },

  handleCreateRemote: function(payload) {
    var model = _.findWhere(this.models, {id: payload.model.id});

    if (!model) {
      this.models.push(model = {});
    }

    _.extend(model, payload.model);

    this.emit('change');
  },

  handleUpdatePending: function(payload) {
    var model = _.findWhere(this.models, {id: payload.id});

    if (!model) {
      return;
    }

    var backup = _.extend({}, _.pick(model, _.keys(payload.attributes)));

    _.extend(model, payload.attributes, {
      isUpdating: true,
    });

    this.updateCompleteWaiting.push({
      operationId: payload.operationId,
      model: model,
      backup: backup,
    });

    this.emit('change');
  },

  handleUpdateComplete: function(payload) {
    var waiting = _.findWhere(this.updateCompleteWaiting, {
      operationId: payload.operationId,
    });

    if (!waiting) {
      // TODO: error handling
      return;
    }

    this.updateCompleteWaiting.splice(this.updateCompleteWaiting.indexOf(waiting), 1);

    _.extend(waiting.model, payload.error ? waiting.backup : payload.model, {
      isUpdating: false,
    });

    this.emit('change');
  },

  handleUpdateRemote: function(payload) {
    var model = _.findWhere(this.models, {id: payload.model.id});

    if (!model) {
      return;
    }

    _.extend(model, payload.model);

    this.emit('change');
  },

  handleDeletePending: function(payload) {
    var model = _.findWhere(this.models, {id: payload.id});

    if (!model) {
      return;
    }

    this.deleteCompleteWaiting.push({
      operationId: payload.operationId,
      model: model,
    });

    _.extend(model, payload.attributes, {
      isDeleting: true,
    });

    this.emit('change');
  },

  handleDeleteComplete: function(payload) {
    var waiting = _.findWhere(this.deleteCompleteWaiting, {
      operationId: payload.operationId,
    });

    if (!waiting) {
      return;
    }

    var index = this.models.indexOf(waiting.model);
    if (index !== -1) {
      this.models.splice(index, 1);
    }

    _.extend(waiting.model, {
      isDeleting: false,
    });

    this.emit('change');
  },

  handleDeleteRemote: function(payload) {
    var model = _.findWhere(this.models, {id: payload.id});

    if (!model) {
      return;
    }

    this.models.splice(this.models.indexOf(model), 1);

    this.emit('change');
  },

  get: function(id) {
    return _.findWhere(this.models, {id: id});
  },

  getBy: function(filter) {
    return _.where(this.models, filter);
  },

  getAll: function() {
    return this.models.slice();
  },

  getState: function() {
    return {
      error: this.error,
      models: this.getAll(),
    };
  },

  makeGetBy: function(key) {
    return function(value) {
      return this.getBy(_.object([[key, value]]));
    };
  },
};

module.exports = Common;


<|start_filename|>app/action-constants.js<|end_filename|>
'use strict';

var keyMirror = require('react/lib/keyMirror');

// don't generate this dynamically - grep is important

module.exports = keyMirror({
  LOGIN_PENDING: null,
  LOGIN_COMPLETE: null,
  PERSON_LOAD_PENDING: null,
  PERSON_LOAD_COMPLETE: null,
  PERSON_GET_PENDING: null,
  PERSON_GET_COMPLETE: null,
  PERSON_UPDATE_REMOTE: null,
  PERSON_DELETE_REMOTE: null,
  MATCH_LOAD_PENDING: null,
  MATCH_LOAD_COMPLETE: null,
  MATCH_GET_PENDING: null,
  MATCH_GET_COMPLETE: null,
  MATCH_CREATE_PENDING: null,
  MATCH_CREATE_COMPLETE: null,
  MATCH_CREATE_REMOTE: null,
  MATCH_UPDATE_REMOTE: null,
  MATCH_DELETE_PENDING: null,
  MATCH_DELETE_COMPLETE: null,
  MATCH_DELETE_REMOTE: null,
  MESSAGE_LOAD_PENDING: null,
  MESSAGE_LOAD_COMPLETE: null,
  MESSAGE_GET_PENDING: null,
  MESSAGE_GET_COMPLETE: null,
  MESSAGE_CREATE_PENDING: null,
  MESSAGE_CREATE_COMPLETE: null,
  MESSAGE_CREATE_REMOTE: null,
});


<|start_filename|>webpack.config.js<|end_filename|>
var webpack = require('webpack');

var release = (process.env.NODE_ENV === 'production');

var plugins = [
  new webpack.NormalModuleReplacementPlugin(/^react$/, 'react/addons'),
];

var jsxLoader = ['jsx?harmony'];

if (release)  {
  plugins.push(new webpack.DefinePlugin({
    'process.env': {
      // This has effect on the react lib size
      'NODE_ENV': JSON.stringify('production'),
    },
  }));

  plugins.push(new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.js'));
  plugins.push(new webpack.optimize.DedupePlugin());
  plugins.push(new webpack.optimize.UglifyJsPlugin());
} else {
  jsxLoader = ['react-hot', 'jsx?harmony'];
}

var config = module.exports = {
  debug: !release,
  cache: !release,
  devtool: !release && 'inline-source-map',
  entry: {
    'app': './app',
    vendor: ['react/addons', 'react-router', 'bows', 'fluxxor', 'lodash'],
  },
  output: {
    path: __dirname + '/dist',
    filename: '[name].js',
  },
  plugins: plugins,
  resolve: {
    extensions: ['', '.js', '.jsx', '.scss'],
  },
  module: {
    loaders: [
      { test: /\.js$/, loaders: ['jsx?harmony'] },
      { test: /\.jsx$/, loaders: jsxLoader },
      {
        test: /\.scss$/,
        loaders: [
          'style-loader',
          'css-loader',
          'autoprefixer-loader',
          'sass-loader?includePaths[]=./app/base/style,includePaths[]=./node_modules',
        ],
      },
      { test: /\.css$/,   loader: 'style-loader!css-loader' },
      { test: /\.png$/,   loader: 'url-loader?limit=10000' },
      { test: /\.svg$/,   loader: 'raw-loader?' },
    ],
  },
};


<|start_filename|>app/stores/login.js<|end_filename|>
'use strict';

var Fluxxor = require('fluxxor');

var log = require('bows')('Login Store');

var constants = require('../action-constants');

var LoginStore = module.exports = Fluxxor.createStore({
  initialize() {
    this.error = null;
    this.isLoading = false;
    this.doing = null;

    this.jwt = null;
    this.profile = null;

    this.bindActions(
      constants.LOGIN_PENDING, this.handleLoginPending,
      constants.LOGIN_COMPLETE, this.handleLoginComplete
    );

    log('INIT', this);
  },

  handleLoginPending() {
    this.isLoading = true;

    this.jwt = null;
    this.profile = null;

    window.localStorage.removeItem("jwt");
    window.localStorage.removeItem("profile");

    this.emit("change");
  },

  handleLoginComplete(payload) {
    log("login complete");

    this.isLoading = false;

    if (payload.error) {
      this.error = payload.error;
    } else if (payload.jwt && payload.profile) {
      this.jwt = payload.jwt;
      this.profile = payload.profile;

      window.localStorage.setItem("jwt", this.jwt);
      window.localStorage.setItem("profile", JSON.stringify(this.profile));
    }

    this.emit("change");
  },

  getJwt() {
    return this.jwt;
  },

  getProfile() {
    return this.profile;
  },
});


<|start_filename|>Dockerfile<|end_filename|>
FROM nginx
ADD nginx.conf /etc/nginx/conf.d/default.conf
RUN rm /usr/share/nginx/html/*
ADD dist /usr/share/nginx/html
CMD nginx -g "daemon off;"


<|start_filename|>app/actions.js<|end_filename|>
'use strict';

var _ = require('lodash');
var agent = require('superagent');
var defaults = require('superagent-defaults');
var uuid = require('uuid');
var Houkou = require('houkou');
var DecodeJWT = require('jwt-decode');
var transform = require('transform-keys');

var camelify = transform(function (key) {
  return key.replace(/_([a-z])/ig, function (_, $1) {
    return $1.toUpperCase();
  });
});

var log = require('bows')('Actions');

var constants = require('./action-constants');

var makeFetchMultiple = function(ctx, pendingEvent, completeEvent, key, urlTemplate) {
  log('Generating fetchMultiple function', pendingEvent, completeEvent);

  return function() {
    var operationId = uuid.v1();

    this.dispatch(pendingEvent, {
      operationId: operationId,
    });

    var self = this;
    ctx.agent.get(urlTemplate.build({})).end(function(res) {
      if (!res.ok) {
        return self.dispatch(completeEvent, {
          operationId: operationId,
          error: res.text,
        });
      }

      return self.dispatch(completeEvent, {
        operationId: operationId,
        models: res.body[key].map(camelify),
      });
    });
  };
};

var makeFetchOne = function(ctx, pendingEvent, completeEvent, urlTemplate) {
  log('Generating fetchOneOf function', pendingEvent, completeEvent);

  return function(id) {
    var operationId = uuid.v1();

    this.dispatch(pendingEvent, {
      operationId: operationId,
      id: id,
    });

    var self = this;
    ctx.agent.get(urlTemplate.build({id: id})).end(function(res) {
      if (!res.ok) {
        return self.dispatch(completeEvent, {
          operationId: operationId,
          error: res.text,
        });
      }

      return self.dispatch(completeEvent, {
        operationId: operationId,
        model: camelify(res.body),
      });
    });
  };
};

var makeCreate = function(ctx, pendingEvent, completeEvent, urlTemplate) {
  log('Generating create function', pendingEvent, completeEvent);

  return function(parameters, attributes) {
    var operationId = uuid.v1();

    attributes = _.extend({}, attributes);
    parameters = _.extend({}, parameters);

    this.dispatch(pendingEvent, {
      operationId: operationId,
      attributes: attributes,
    });

    var self = this;
    ctx.agent.post(urlTemplate.build(parameters)).send(attributes).end(function(res) {
      if (!res.ok) {
        return self.dispatch(completeEvent, {
          operationId: operationId,
          error: res.text,
        });
      }

      return self.dispatch(completeEvent, {
        operationId: operationId,
        model: camelify(res.body),
      });
    });
  };
};

var makeUpdate = function(ctx, pendingEvent, completeEvent, urlTemplate) {
  log('Generating update function', pendingEvent, completeEvent);

  return function(attributes) {
    var operationId = uuid.v1();
    var id = attributes.id;

    attributes = _.extend({}, attributes);

    this.dispatch(pendingEvent, {
      operationId: operationId,
      id: id,
      attributes: attributes,
    });

    var self = this;
    ctx.agent.put(urlTemplate.build({id: id})).send(attributes).end(function(res) {
      if (!res.ok) {
        return self.dispatch(completeEvent, {
          operationId: operationId,
          error: res.text,
        });
      }

      return self.dispatch(completeEvent, {
        operationId: operationId,
        model: camelify(res.body),
      });
    });
  };
};

var makeDelete = function(ctx, pendingEvent, completeEvent, urlTemplate) {
  log('Generating delete function', pendingEvent, completeEvent);

  return function(id) {
    var operationId = uuid.v1();

    this.dispatch(pendingEvent, {
      operationId: operationId,
      id: id,
    });

    var self = this;
    ctx.agent.del(urlTemplate.build({id: id})).end(function(res) {
      if (!res.ok) {
        return self.dispatch(completeEvent, {
          operationId: operationId,
          error: res.text,
        });
      }

      return self.dispatch(completeEvent, {
        operationId: operationId,
      });
    });
  };
};

module.exports = function createActions(baseUrl, pubnubKey) {
  var ctx = {
    agent: agent,
    jwt: null,
    pubnub: null,

    setJwt(jwt) {
      if (!this.jwt) {
        this.jwt = jwt;
        this.agent = defaults().set('authorization', 'Bearer ' + jwt);
      }
    },

    connectPubnub(channel, actions) {
      if (!this.pubnub) {
        this.pubnub = PUBNUB.init({
          subscribe_key: pubnubKey,
          ssl: true,
        });

        var self = this;

        this.pubnub.subscribe({
          channel: channel,
          message(incoming, env, channel) {
            if (incoming.type === "message") {
              actions.dispatch(constants.MESSAGE_CREATE_REMOTE, {
                model: camelify(incoming),
              });
            }

            if (incoming.type === "match") {
              self.agent.get(baseUrl + '/v1/matches/' + incoming.other_user_id + '.json').end(function(res) {
                if (!res.ok || !res.body || !res.body.person) {
                  return;
                }

                return actions.dispatch(constants.MATCH_CREATE_REMOTE, {
                  model: camelify(res.body.person),
                });
              });
            }
          },
        });
      }
    },
  };

  return {
    peopleFetch: makeFetchMultiple(ctx, constants.PERSON_LOAD_PENDING, constants.PERSON_LOAD_COMPLETE, 'people', new Houkou(baseUrl + '/v1/people.json')),
    personFetch: makeFetchOne(ctx, constants.PERSON_GET_PENDING, constants.PERSON_GET_COMPLETE, new Houkou(baseUrl + '/v1/people/:id.json')),
    matchesFetch: makeFetchMultiple(ctx, constants.MATCH_LOAD_PENDING, constants.MATCH_LOAD_COMPLETE, 'matches', new Houkou(baseUrl + '/v1/matches.json')),
    matchFetch: makeFetchOne(ctx, constants.MATCH_GET_PENDING, constants.MATCH_GET_COMPLETE, new Houkou(baseUrl + '/v1/matches/:id.json')),
    matchCreate: makeCreate(ctx, constants.MATCH_CREATE_PENDING, constants.MATCH_CREATE_COMPLETE, new Houkou(baseUrl + '/v1/matches.json')),
    matchDelete: makeDelete(ctx, constants.MATCH_DELETE_PENDING, constants.MATCH_DELETE_COMPLETE, new Houkou(baseUrl + '/v1/matches/:id.json')),
    messageCreate: makeCreate(ctx, constants.MESSAGE_CREATE_PENDING, constants.MESSAGE_CREATE_COMPLETE, new Houkou(baseUrl + '/v1/matches/:id/messages.json')),

    init() {
      try {
        var jwt = window.localStorage.getItem("jwt");
        var profile = JSON.parse(window.localStorage.getItem("profile"));

        var decoded = DecodeJWT(jwt);
      } catch (e) { return; }

      if (decoded.exp*1000 <= Date.now()) {
        return;
      }

      ctx.setJwt(jwt);
      ctx.connectPubnub(profile.channel, this);

      this.dispatch(constants.LOGIN_COMPLETE, {
        jwt: jwt,
        profile: profile,
      });
    },

    login() {
      var operationId = uuid.v1();

      this.dispatch(constants.LOGIN_PENDING, {
        operationId: operationId,
      });

      var self = this;

      var onMessage = function onMessage(ev) {
        if (ev.origin !== window.location.origin) {
          return;
        }

        if (typeof ev.data !== "object" || ev.data === null || typeof ev.data.token !== "string") {
          return;
        }

        window.removeEventListener('message', onMessage);

        var token = ev.data.token;

        ctx.setJwt(token);

        ctx.agent.get(baseUrl + '/v1/profile.json').end(function(res) {
          if (!res.ok) {
            return self.dispatch(constants.LOGIN_COMPLETE, {
              operationId: operationId,
              error: res.text,
            });
          }

          var profile = camelify(res.body.person);

          ctx.connectPubnub(profile.channel, self);

          return self.dispatch(constants.LOGIN_COMPLETE, {
            operationId: operationId,
            jwt: token,
            profile: profile,
          });
        });
      };
      window.addEventListener('message', onMessage);

      window.open(baseUrl + '/auth?redirect_uri=' + escape(window.location.origin + '/login-callback.html'));
    },
  };
};


<|start_filename|>app/stores/suggestion.js<|end_filename|>
'use strict';

var _ = require('lodash');
var Fluxxor = require('fluxxor');

var log = require('bows')('Suggestion Store');

var constants = require('../action-constants');
var Common = require('./common');

var SuggestionStore = module.exports = Fluxxor.createStore({
  initialize: function() {
    this.models = [];

    this.timer = null;
    this.timeout = 1;

    this.isLoading = false;
    this.error = null;

    this.bindActions(
      constants.PERSON_LOAD_PENDING, this.handleLoadPending,
      constants.PERSON_LOAD_COMPLETE, this.handleLoadComplete
    );

    log('INIT', this);
  },

  handleLoadPending: Common.handleLoadPending,
  handleLoadComplete(payload) {
    if (Array.isArray(payload.models) && payload.models.length === 0) {
      this.timeout = Math.min(this.timeout * 2 + 1000, 1000 * 60);
    } else {
      this.timeout = 1;
    }

    return Common.handleLoadComplete.call(this, payload);
  },
  get: Common.get,
  getAll: Common.getAll,
  getState: Common.getState,

  length() {
    return this.models.length;
  },

  peek(n) {
    return this.models.slice(0, n);
  },

  shift() {
    this.models.shift();

    this.emit('change');
  },
});


<|start_filename|>app/stores/index.js<|end_filename|>
'use strict';

var LoginStore = require('./login');
var PersonStore = require('./person');
var MatchStore = require('./match');
var MessageStore = require('./message');
var SuggestionStore = require('./suggestion');

var createStores = module.exports = function createStores() {
  return {
  	LoginStore: new LoginStore(),
    PersonStore: new PersonStore(),
    MatchStore: new MatchStore(),
    MessageStore: new MessageStore(),
    SuggestionStore: new SuggestionStore(),
  };
};


<|start_filename|>app/stores/message.js<|end_filename|>
'use strict';

var _ = require('lodash');
var Fluxxor = require('fluxxor');

var log = require('bows')('Message Store');

var constants = require('../action-constants');
var Common = require('./common');

var MessageStore = module.exports = Fluxxor.createStore({
  initialize: function() {
    this.models = [];

    this.isLoading = false;
    this.error = null;
    this.createCompleteWaiting = [];

    this.bindActions(
      constants.MATCH_GET_PENDING, this.handleLoadPending,
      constants.MATCH_GET_COMPLETE, this.handleLoadComplete,
      constants.MESSAGE_CREATE_PENDING, this.handleCreatePending,
      constants.MESSAGE_CREATE_COMPLETE, this.handleCreateComplete,
      constants.MESSAGE_CREATE_REMOTE, this.handleCreateRemote
    );

    log('INIT', this);
  },

  handleLoadPending: Common.handleLoadPending,
  handleLoadComplete(payload) {
    if (payload.error) {
      return Common.handleLoadComplete.call(this, payload);
    }

    return Common.handleLoadComplete.call(this, {
      models: payload.model.messages.map(function(message) {
        return _.extend({}, message, {otherUserId: payload.model.person.id});
      }),
    });
  },
  handleCreatePending: Common.handleCreatePending,
  handleCreateComplete(payload) {
    if (payload.error) {
      return Common.handleCreateComplete.call(this, payload);
    }

    return Common.handleCreateComplete.call(this, payload);
  },
  handleCreateRemote(payload) {
    var model = _.findWhere(this.models, {id: payload.model.id});

    if (!model) {
      this.models.push(model = {});
    }

    _.extend(model, payload.model);

    this.emit('change');
  },
  get: Common.get,
  getBy: Common.getBy,
  getAll: Common.getAll,
  getState: Common.getState,
  getByMatchId: Common.makeGetBy("matchId"),
  getByOtherUserId: Common.makeGetBy("otherUserId"),
});
