--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/coffee/messages.coffee Mon Mar 21 19:57:24 2016 +0800
+class Tram.Message extends Backbone.Model
+ @on 'add change:stamp', ->
+ @set('d/mstamp', moment(@get('stamp')))
+class Tram.Messages extends Backbone.Collection
+ splitThreshold: 30 * 60 * 1000
+ foldThreshold: 60 * 1000
+ model.get('stamp').valueOf()
+ @_splitOrFold(prev, model)
+ @_splitOrFold(model, next)
+ _splitOrFold: (m1, m2) ->
+ if @_splittable(m1, m2)
+ m2.set('d/split', true)
+ else if @_foldable(m1, m2)
+ _splittable: (m1, m2) ->
+ Math.abs(m1.get('stamp').valueOf() - m2.get('stamp').valueOf()) > @splitThreshold
+ # check stamp, type, etc
+ m1.get('from') is m2.get('from') and Math.abs(m1.get('stamp').valueOf() - m2.get('stamp').valueOf()) < @foldThreshold
+class Tram.MessageView extends Backbone.View
+ template: $('#message-template').html()
+ @model.get('contact')?.get('d/handle') or @model.get('from')
+ @rivet = rivets.bind(@el, model: @model, view: this)
+class Tram.LogApp extends Backbone.View
+ @listenTo(@collection, 'add', @onAdd)
+ onAdd: (model, collection) ->
+ mi = collection.indexOf(model)
+ view = new Tram.MessageView(model: model)
+ @$el.children().eq(mi - 1).after(el)
+ return @$el.scrollTop() + @$el.height() == @$el.get(0).scrollHeight
+ @$el.scrollTop(@$el.get(0).scrollHeight)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/coffee/rivets.coffee Mon Mar 21 19:57:24 2016 +0800
+ observe: (obj, keypath, callback) ->
+ obj.on('change:' + keypath, callback)
+ unobserve: (obj, keypath, callback) ->
+ obj.off('change:' + keypath, callback)
+ set: (obj, keypath, value) ->
+ obj.set(keypath, value)
+rivets.formatters['format-date'] = (value, format) ->
+ if value? then value.format(format) else ''
+rivets.formatters['iso-date'] = (value) ->
+ if value? then value.toISOString() else ''
+rivets.formatters['from-now'] = (value) ->
+ if value? then value.fromNow() else ''
--- a/css/tram-im.css Mon Mar 21 19:50:21 2016 +0800
+++ b/css/tram-im.css Mon Mar 21 19:57:24 2016 +0800
+ border-top: 1px solid lightgray;
+ vertical-align: middle;
--- a/index.html Mon Mar 21 19:50:21 2016 +0800
+++ b/index.html Mon Mar 21 19:57:24 2016 +0800
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.2.3/backbone-min.js"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/rivets/0.8.1/rivets.bundled.min.js"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/2.25.0/js/uikit.min.js"></script>
<script src="/vendor/strophejs/strophe.min.js"></script>
<i class="uk-icon-sign-out"></i> Log out
+ <script type="text/template" id="message-template">
+ <div class="splitter" rv-if="model:d/split"></div>
+ <div class="column uk-width-1-1">
+ <div style="padding-top: 4px;" rv-hide="model:d/fold">
+ <strong rv-text="view.getHandle < model:contact:d/handle model:from"></strong>
+ <time class="stamp uk-text-muted" rv-datetime="model:d/mstamp | iso-date" rv-title="model:d/mstamp | format-date 'HH:mm:ss'" rv-text="model:d/mstamp | from-now"></time>
+ <div rv-data-id="model:id" rv-class="model:cls" rv-text="model:text"></div>
<script src='/js/tram.js'></script>
+ <script src='/js/rivets.js'></script>
<script src='/js/validation.js'></script>
<script src='/js/xmpp.js'></script>
+ <script src='/js/messages.js'></script>
<script src='/config.js'></script>
<script src='/js/index.js'></script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/js/messages.js Mon Mar 21 19:57:24 2016 +0800
+// Generated by CoffeeScript 1.10.0
+ var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+ Tram.Message = (function(superClass) {
+ extend(Message, superClass);
+ return Message.__super__.constructor.apply(this, arguments);
+ Message.prototype.initialize = function() {
+ return this.on('add change:stamp', function() {
+ if (this.has('stamp')) {
+ return this.set('d/mstamp', moment(this.get('stamp')));
+ return this.unset('d/mstamp');
+ Tram.Messages = (function(superClass) {
+ extend(Messages, superClass);
+ return Messages.__super__.constructor.apply(this, arguments);
+ Messages.prototype.model = Tram.Message;
+ Messages.prototype.splitThreshold = 30 * 60 * 1000;
+ Messages.prototype.foldThreshold = 60 * 1000;
+ Messages.prototype.comparator = function(model) {
+ return model.get('stamp').valueOf();
+ Messages.prototype.initialize = function() {
+ return this.on('add', this.onAdd);
+ Messages.prototype.onAdd = function(model) {
+ mi = this.indexOf(model);
+ prev = this.models[mi - 1];
+ this._splitOrFold(prev, model);
+ next = this.models[mi + 1];
+ return this._splitOrFold(model, next);
+ Messages.prototype._splitOrFold = function(m1, m2) {
+ if (this._splittable(m1, m2)) {
+ return m2.set('d/split', true);
+ } else if (this._foldable(m1, m2)) {
+ return m2.set('d/fold', true);
+ Messages.prototype._splittable = function(m1, m2) {
+ return Math.abs(m1.get('stamp').valueOf() - m2.get('stamp').valueOf()) > this.splitThreshold;
+ Messages.prototype._foldable = function(m1, m2) {
+ return m1.get('from') === m2.get('from') && Math.abs(m1.get('stamp').valueOf() - m2.get('stamp').valueOf()) < this.foldThreshold;
+ })(Backbone.Collection);
+ Tram.MessageView = (function(superClass) {
+ extend(MessageView, superClass);
+ function MessageView() {
+ return MessageView.__super__.constructor.apply(this, arguments);
+ MessageView.prototype.tagName = 'div';
+ MessageView.prototype.className = 'message';
+ MessageView.prototype.template = $('#message-template').html();
+ MessageView.prototype.initialize = function() {
+ this.$el.html(this.template);
+ MessageView.prototype.bind = function() {};
+ MessageView.prototype.getHandle = function() {
+ return ((ref = this.model.get('contact')) != null ? ref.get('d/handle') : void 0) || this.model.get('from');
+ MessageView.prototype.render = function(model) {
+ this.rivet = rivets.bind(this.el, {
+ MessageView.prototype.remove = function() {
+ return MessageView.__super__.remove.apply(this, arguments);
+ Tram.LogApp = (function(superClass) {
+ extend(LogApp, superClass);
+ return LogApp.__super__.constructor.apply(this, arguments);
+ LogApp.prototype.initialize = function() {
+ return this.listenTo(this.collection, 'add', this.onAdd);
+ LogApp.prototype.onAdd = function(model, collection) {
+ mi = collection.indexOf(model);
+ view = new Tram.MessageView({
+ this.$el.children().eq(mi - 1).after(el);
+ LogApp.prototype.bottomed = function() {
+ return this.$el.scrollTop() + this.$el.height() === this.$el.get(0).scrollHeight;
+ LogApp.prototype.scroll = function() {
+ return this.$el.scrollTop(this.$el.get(0).scrollHeight);
+//# sourceMappingURL=messages.js.map
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/js/messages.js.map Mon Mar 21 19:57:24 2016 +0800
+ "coffee/messages.coffee"
+ "mappings": "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"
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/js/rivets.js Mon Mar 21 19:57:24 2016 +0800
+// Generated by CoffeeScript 1.10.0
+ rivets.adapters[':'] = {
+ observe: function(obj, keypath, callback) {
+ return obj.on('change:' + keypath, callback);
+ unobserve: function(obj, keypath, callback) {
+ return obj.off('change:' + keypath, callback);
+ get: function(obj, keypath) {
+ return obj.get(keypath);
+ set: function(obj, keypath, value) {
+ return obj.set(keypath, value);
+ rivets.formatters['format-date'] = function(value, format) {
+ return value.format(format);
+ rivets.formatters['iso-date'] = function(value) {
+ return value.toISOString();
+ rivets.formatters['from-now'] = function(value) {
+ return value.fromNow();
+//# sourceMappingURL=rivets.js.map
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/js/rivets.js.map Mon Mar 21 19:57:24 2016 +0800
+ "mappings": ";AAAA;EAAA,MAAM,CAAC,QAAS,CAAA,GAAA,CAAhB,GACE;IAAA,OAAA,EAAS,SAAC,GAAD,EAAM,OAAN,EAAe,QAAf;aACP,GAAG,CAAC,EAAJ,CAAO,SAAA,GAAY,OAAnB,EAA4B,QAA5B;IADO,CAAT;IAGA,SAAA,EAAW,SAAC,GAAD,EAAM,OAAN,EAAe,QAAf;aACT,GAAG,CAAC,GAAJ,CAAQ,SAAA,GAAY,OAApB,EAA6B,QAA7B;IADS,CAHX;IAMA,GAAA,EAAK,SAAC,GAAD,EAAM,OAAN;aACH,GAAG,CAAC,GAAJ,CAAQ,OAAR;IADG,CANL;IASA,GAAA,EAAK,SAAC,GAAD,EAAM,OAAN,EAAe,KAAf;aACH,GAAG,CAAC,GAAJ,CAAQ,OAAR,EAAiB,KAAjB;IADG,CATL;;;EAaF,MAAM,CAAC,UAAW,CAAA,aAAA,CAAlB,GAAmC,SAAC,KAAD,EAAQ,MAAR;IACjC,IAAG,aAAH;aAAe,KAAK,CAAC,MAAN,CAAa,MAAb,EAAf;KAAA,MAAA;aAAyC,GAAzC;;EADiC;;EAInC,MAAM,CAAC,UAAW,CAAA,UAAA,CAAlB,GAAgC,SAAC,KAAD;IAC9B,IAAG,aAAH;aAAe,KAAK,CAAC,WAAN,CAAA,EAAf;KAAA,MAAA;aAAwC,GAAxC;;EAD8B;;EAIhC,MAAM,CAAC,UAAW,CAAA,UAAA,CAAlB,GAAgC,SAAC,KAAD;IAC9B,IAAG,aAAH;aAAe,KAAK,CAAC,OAAN,CAAA,EAAf;KAAA,MAAA;aAAoC,GAApC;;EAD8B;AAtBhC"
\ No newline at end of file