1
0
mirror of https://github.com/moparisthebest/kaiwa synced 2024-11-13 13:05:00 -05:00

Add message grouping, and avatars

This commit is contained in:
Lance Stout 2013-09-25 20:38:00 -07:00
parent 0aa116e6f6
commit 0f2b4b9ec1
10 changed files with 337 additions and 31 deletions

View File

@ -0,0 +1,107 @@
/*global app, $*/
"use strict";
var _ = require('underscore');
module.exports = {
initializeScroll: function () {
$(this.$scrollContainer).scroll(_.bind(_.throttle(this.handleScroll, 300), this));
this.pinnedToBottom = true;
this.lastScrollTop = 0;
},
scrollPageLoad: function () {
if (typeof this.lastScrollPosition === 'number') {
this.scrollTo(this.lastScrollPosition);
} else {
this.scrollToBottom();
}
},
scrollPageUnload: function () {
this.savePosition();
this.trimOldChats();
},
savePosition: function () {
this.lastScrollPosition = this.pinnedToBottom ? '' : this.$scrollContainer.scrollTop();
},
trimOldChats: function () {
var self = this;
var removedIds;
if (this.pinnedToBottom) {
_.delay(function () {
removedIds = self.collection.trimOlderChats();
removedIds.forEach(function (id) {
self.$('#chat' + id).remove();
});
}, 500);
}
},
handleScroll: function (e) {
var scrollTop = this.$scrollContainer[0].scrollTop;
var direction = scrollTop > this.lastScrollTop ? 'down' : 'up';
if (direction === 'up' && !this.isBottom()) {
this.pinnedToBottom = false;
} else if (this.isBottom()) {
this.handleAtBottom();
}
this.lastScrollTop = scrollTop;
},
scrollIfPinned: function (animate) {
if (this.pinnedToBottom) this.scrollToBottom(animate);
},
handleAtBottom: function () {
if (this.isVisible()) {
this.pinnedToBottom = true;
}
},
isBottom: function () {
var scrollTop = this.$scrollContainer[0].scrollTop;
var scrollHeight = this.$scrollContainer[0].scrollHeight;
var height = this.$scrollContainer.height();
var fromBottom = scrollHeight - (scrollTop + height);
return fromBottom < 40 || $('body').is(':animated');
},
resizeInput: function () {
var height;
var scrollHeight;
var newHeight;
var newPadding;
var paddingDelta;
var maxHeight = 102;
this.$chatInput.removeAttr('style');
height = this.$chatInput.height() + 10,
scrollHeight = this.$chatInput.get(0).scrollHeight,
newHeight = scrollHeight + 2;
if (newHeight > maxHeight) newHeight = maxHeight;
if (newHeight > height) {
this.$chatInput.css('height', newHeight);
newPadding = newHeight + 21;
paddingDelta = newPadding - parseInt(this.$messageList.css('paddingBottom'), 10);
if (!!paddingDelta) {
this.$messageList.css('paddingBottom', newPadding);
}
}
},
scrollTo: function (height, animate) {
if (animate) {
this.$scrollContainer.animate({
scrollTop: height
}, {
duration: 500,
queue: false
});
} else {
this.$scrollContainer.scrollTop(height);
}
},
scrollToBottom: function (animate) {
if (!this.isVisible()) return;
var height = this.$scrollContainer[0].scrollHeight;
this.scrollTo(height, animate);
},
isVisible: function () {
return app.currentPage === this;
}
};

View File

@ -25,7 +25,8 @@ module.exports = HumanModel.define({
connected: ['bool', true, false], connected: ['bool', true, false],
shouldAskForAlertsPermission: ['bool', true, false], shouldAskForAlertsPermission: ['bool', true, false],
hasFocus: ['bool', true, false], hasFocus: ['bool', true, false],
_activeContact: ['string', true, ''] _activeContact: ['string', true, ''],
displayName: ['string', true, 'Me']
}, },
collections: { collections: {
contacts: Contacts, contacts: Contacts,

View File

@ -2,6 +2,7 @@
"use strict"; "use strict";
var HumanModel = require('human-model'); var HumanModel = require('human-model');
var templates = require('../templates');
module.exports = HumanModel.define({ module.exports = HumanModel.define({
@ -26,6 +27,16 @@ module.exports = HumanModel.define({
return me.isMe(this.from); return me.isMe(this.from);
} }
}, },
sender: {
deps: ['from', 'mine'],
fn: function () {
if (this.mine) {
return me;
} else {
return me.getContact(this.from);
}
}
},
delayed: { delayed: {
deps: ['delay'], deps: ['delay'],
fn: function () { fn: function () {
@ -81,6 +92,31 @@ module.exports = HumanModel.define({
} }
return me.getContact(this.from.bare).displayName; return me.getContact(this.from.bare).displayName;
} }
},
partialTemplateHtml: {
deps: ['edited', 'pending', 'body'],
cache: false,
fn: function () {
return templates.includes.bareMessage({message: this});
}
},
templateHtml: {
fn: function () {
return templates.includes.wrappedMessage({message: this});
}
},
classList: {
cache: false,
fn: function () {
var res = [];
if (this.mine) res.push('mine');
if (this.pending) res.push('pending');
if (this.delayed) res.push('delayed');
if (this.edited) res.push('edited');
return res.join(' ');
}
} }
}, },
session: { session: {
@ -115,5 +151,8 @@ module.exports = HumanModel.define({
edited: this.edited edited: this.edited
}; };
app.storage.archive.add(data); app.storage.archive.add(data);
},
shouldGroupWith: function (previous) {
return previous && previous.from.bare === this.from.bare;
} }
}); });

View File

@ -1,17 +1,27 @@
/*global $, app, me, client*/ /*global $, app, me, client*/
"use strict"; "use strict";
var _ = require('underscore');
var BasePage = require('./base'); var BasePage = require('./base');
var templates = require('../templates'); var templates = require('../templates');
var Message = require('../views/message'); var Message = require('../views/message');
var MessageModel = require('../models/message'); var MessageModel = require('../models/message');
var chatHelpers = require('../helpers/chatHelpers');
module.exports = BasePage.extend({ module.exports = BasePage.extend(chatHelpers).extend({
template: templates.pages.chat, template: templates.pages.chat,
initialize: function (spec) { initialize: function (spec) {
this.editMode = false; this.editMode = false;
this.model.fetchHistory(); this.model.fetchHistory();
this.listenTo(this, 'pageloaded', this.handlePageLoaded);
this.listenTo(this, 'pageunloaded', this.handlePageUnloaded);
this.listenTo(this.model.messages, 'change:body', this.refreshModel);
this.listenTo(this.model.messages, 'change:edited', this.refreshModel);
this.listenTo(this.model.messages, 'change:pending', this.refreshModel);
this.render(); this.render();
}, },
events: { events: {
@ -40,14 +50,47 @@ module.exports = BasePage.extend({
}); });
}, },
render: function () { render: function () {
if (this.rendered) return this;
this.rendered = true;
this.renderAndBind(); this.renderAndBind();
this.typingTimer = null; this.typingTimer = null;
this.$chatInput = this.$('.chatBox textarea'); this.$chatInput = this.$('.chatBox textarea');
this.$chatBox = this.$('.chatBox');
this.$messageList = this.$('.messages'); this.$messageList = this.$('.messages');
this.renderCollection(this.model.messages, Message, this.$('.messages')); this.$scrollContainer = this.$messageList;
this.registerBindings();
this.listenTo(this.model.messages, 'add', this.handleChatAdded);
this.renderCollection();
$(window).on('resize', _.bind(this.handleWindowResize, this));
this.initializeScroll();
return this; return this;
}, },
handlePageLoaded: function () {
this.scrollPageLoad();
this.handleWindowResize();
},
handlePageUnloaded: function () {
this.scrollPageUnload();
},
renderCollection: function () {
var self = this;
var previous;
var bottom = this.isBottom() || this.$messageList.is(':empty');
this.model.messages.each(function (model, i) {
self.appendModel(model);
});
this.scrollIfPinned();
},
handleWindowResize: function () {
this.scrollIfPinned();
this.$chatInput.trigger('keyup');
},
handleKeyDown: function (e) { handleKeyDown: function (e) {
clearTimeout(this.typingTimer); clearTimeout(this.typingTimer);
if (e.which === 13 && !e.shiftKey) { if (e.which === 13 && !e.shiftKey) {
@ -86,29 +129,6 @@ module.exports = BasePage.extend({
}); });
} }
}, },
resizeInput: function () {
var height;
var scrollHeight;
var newHeight;
var newPadding;
var paddingDelta;
var maxHeight = 102;
this.$chatInput.removeAttr('style');
height = this.$chatInput.height() + 10,
scrollHeight = this.$chatInput.get(0).scrollHeight,
newHeight = scrollHeight + 2;
if (newHeight > maxHeight) newHeight = maxHeight;
if (newHeight > height) {
this.$chatInput.css('height', newHeight);
newPadding = newHeight + 21;
paddingDelta = newPadding - parseInt(this.$messageList.css('paddingBottom'), 10);
if (!!paddingDelta) {
this.$messageList.css('paddingBottom', newPadding);
}
}
},
pausedTyping: function () { pausedTyping: function () {
if (this.typing) { if (this.typing) {
this.typing = false; this.typing = false;
@ -130,7 +150,7 @@ module.exports = BasePage.extend({
chatState: 'active' chatState: 'active'
}; };
if (this.editMode) { if (this.editMode) {
message.replace = this.model.lastSentMessage.id || this.model.lastSentMessage.cid; message.replace = this.model.lastSentMessage.id;
} }
var id = client.sendMessage(message); var id = client.sendMessage(message);
@ -141,7 +161,6 @@ module.exports = BasePage.extend({
this.model.lastSentMessage.correct(message); this.model.lastSentMessage.correct(message);
} else { } else {
var msgModel = new MessageModel(message); var msgModel = new MessageModel(message);
msgModel.cid = id;
this.model.messages.add(msgModel); this.model.messages.add(msgModel);
this.model.lastSentMessage = msgModel; this.model.lastSentMessage = msgModel;
} }
@ -150,5 +169,32 @@ module.exports = BasePage.extend({
this.typing = false; this.typing = false;
this.$chatInput.removeClass('editing'); this.$chatInput.removeClass('editing');
this.$chatInput.val(''); this.$chatInput.val('');
},
handleChatAdded: function (model) {
this.appendModel(model, true);
},
refreshModel: function (model) {
var existing = this.$('#chat' + model.cid);
console.log(model);
console.log(model.classList);
existing.replaceWith(model.partialTemplateHtml);
},
appendModel: function (model, preload) {
var self = this;
var isGrouped = model.shouldGroupWith(this.lastModel);
var newEl, first, last;
if (isGrouped) {
newEl = $(model.partialTemplateHtml);
last = this.$messageList.find('li').last();
last.find('.messageWrapper').append(newEl);
last.addClass('chatGroup');
} else {
newEl = $(model.templateHtml);
this.$messageList.append(newEl);
}
this.lastModel = model;
this.scrollIfPinned();
} }
}); });

View File

@ -27,6 +27,21 @@ exports.head = function anonymous(locals) {
return buf.join(""); return buf.join("");
}; };
// bareMessage.jade compiled template
exports.includes.bareMessage = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push("<div" + jade.attrs({
id: "chat" + message.cid,
"class": "message" + " " + message.classList
}, {
"class": true,
id: true
}) + '><span class="timestamp">' + jade.escape(null == (jade.interp = message.formattedTime) ? "" : jade.interp) + '</span><p class="body">' + jade.escape(null == (jade.interp = message.body) ? "" : jade.interp) + "</p></div>");
}
return buf.join("");
};
// contactListItem.jade compiled template // contactListItem.jade compiled template
exports.includes.contactListItem = function anonymous(locals) { exports.includes.contactListItem = function anonymous(locals) {
var buf = []; var buf = [];
@ -59,6 +74,15 @@ exports.includes.message = function anonymous(locals) {
return buf.join(""); return buf.join("");
}; };
// messageGroup.jade compiled template
exports.includes.messageGroup = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push("<li></li>");
}
return buf.join("");
};
// mucListItem.jade compiled template // mucListItem.jade compiled template
exports.includes.mucListItem = function anonymous(locals) { exports.includes.mucListItem = function anonymous(locals) {
var buf = []; var buf = [];
@ -82,6 +106,29 @@ exports.includes.mucMessage = function anonymous(locals) {
return buf.join(""); return buf.join("");
}; };
// wrappedMessage.jade compiled template
exports.includes.wrappedMessage = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<li><a href="#" class="messageAvatar"><img' + jade.attrs({
src: message.sender.avatar,
alt: message.sender.displayName,
"data-placement": "below"
}, {
src: true,
alt: true,
"data-placement": true
}) + '/><span class="name">' + jade.escape(null == (jade.interp = message.sender.displayName + ": &nbsp;") ? "" : jade.interp) + '</span></a><div class="messageWrapper"><div' + jade.attrs({
id: "chat" + message.cid,
"class": "message" + " " + message.classList
}, {
"class": true,
id: true
}) + '><span class="timestamp">' + jade.escape(null == (jade.interp = message.formattedTime) ? "" : jade.interp) + '</span><p class="body">' + jade.escape(null == (jade.interp = message.body) ? "" : jade.interp) + "</p></div></div></li>");
}
return buf.join("");
};
// growlMessage.jade compiled template // growlMessage.jade compiled template
exports.misc.growlMessage = function anonymous(locals) { exports.misc.growlMessage = function anonymous(locals) {
var buf = []; var buf = [];

View File

@ -0,0 +1,3 @@
.message(id='chat'+message.cid, class=message.classList)
span.timestamp=message.formattedTime
p.body=message.body

View File

@ -0,0 +1 @@
li

View File

@ -0,0 +1,6 @@
li
a.messageAvatar(href='#')
img(src=message.sender.avatar, alt=message.sender.displayName, data-placement="below")
span.name=message.sender.displayName + ": &nbsp;"
.messageWrapper
include bareMessage

View File

@ -72,15 +72,39 @@
position: relative position: relative
z-index: 1 z-index: 1
list-style: none list-style: none
margin: 0px
padding: 0px padding: 0px
width: 100% width: 100%
min-height: 40px
display: block display: block
borderbox() borderbox()
border: 1px solid blue
&:last-child .message &:last-child .message
border: none border: none
&.chatGroup
border: 1px solid red
.messageAvatar
position: relative
display: inline-block
float: left
width: 30px
height: 30px
margin: 0px 8px 8px 8px
img
avatar()
border: 1px solid red
.name
text-indent: -9999em
position: absolute
width: 1px
.messageWrapper
margin-left: 50px
border-left: 1px solid red
.message .message
font-size: 12px font-size: 12px
margin: 0px margin: 0px

View File

@ -689,17 +689,49 @@ h3 {
position: relative; position: relative;
z-index: 1; z-index: 1;
list-style: none; list-style: none;
margin: 0px;
padding: 0px; padding: 0px;
width: 100%; width: 100%;
min-height: 40px;
display: block; display: block;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid #00f;
} }
.messages li:last-child .message { .messages li:last-child .message {
border: none; border: none;
} }
.messages li.chatGroup {
border: 1px solid #f00;
}
.messages li .messageAvatar {
position: relative;
display: inline-block;
float: left;
width: 30px;
height: 30px;
margin: 0px 8px 8px 8px;
}
.messages li .messageAvatar img {
width: 30px;
height: 30px;
-moz-border-radius: 50px;
-webkit-border-radius: 50px;
-khtml-border-radius: 50px;
-o-border-radius: 50px;
-border-radius: 50px;
border-radius: 50px;
border: 1px solid #f00;
}
.messages li .messageAvatar .name {
text-indent: -9999em;
position: absolute;
width: 1px;
}
.messages .messageWrapper {
margin-left: 50px;
border-left: 1px solid #f00;
}
.messages .message { .messages .message {
font-size: 12px; font-size: 12px;
margin: 0px; margin: 0px;