diff --git a/clientapp/models/me.js b/clientapp/models/me.js index 067e8da..7e6ade3 100644 --- a/clientapp/models/me.js +++ b/clientapp/models/me.js @@ -91,6 +91,15 @@ module.exports = HumanModel.define({ this._activeContact = curr.id; } }, + getName: function () { + return this.displayName; + }, + getNickname: function () { + return this.displayName != this.nick ? this.nick : ''; + }, + getAvatar: function () { + return this.avatar; + }, setAvatar: function (id, type, source) { var self = this; fetchAvatar('', id, type, source, function (avatar) { @@ -102,7 +111,10 @@ module.exports = HumanModel.define({ this.soundEnabled = enable; }, getContact: function (jid, alt) { - if (typeof jid === 'string') jid = new client.JID(jid); + if (typeof jid === 'string') { + if (SERVER_CONFIG.domain && jid.indexOf('@') == -1) jid += '@' + SERVER_CONFIG.domain; + jid = new client.JID(jid); + } if (typeof alt === 'string') alt = new client.JID(alt); if (this.isMe(jid)) { diff --git a/clientapp/models/muc.js b/clientapp/models/muc.js index ba9658d..cbb3249 100644 --- a/clientapp/models/muc.js +++ b/clientapp/models/muc.js @@ -76,6 +76,27 @@ module.exports = HumanModel.define({ resources: Resources, messages: Messages }, + getName: function (jid) { + var nickname = jid.split('/')[1]; + var name = nickname; + var xmppContact = me.getContact(nickname); + if (xmppContact) { + name = xmppContact.displayName; + } + return name != '' ? name : nickname; + }, + getNickname: function (jid) { + var nickname = jid.split('/')[1]; + return nickname != this.getName(jid) ? nickname : ''; + }, + getAvatar: function (jid) { + var avatar = ""; + var xmppContact = me.getContact(jid.split('/')[1]); + if (xmppContact) { + avatar = xmppContact.avatar; + } + return avatar || 'https://gravatar.com/avatar'; + }, addMessage: function (message, notify) { message.owner = me.jid.bare; diff --git a/clientapp/models/resources.js b/clientapp/models/resources.js index 3693dcb..215b159 100644 --- a/clientapp/models/resources.js +++ b/clientapp/models/resources.js @@ -3,7 +3,6 @@ var BaseCollection = require('./baseCollection'); var Resource = require('./resource'); - module.exports = BaseCollection.extend({ type: 'resources', model: Resource, @@ -42,5 +41,20 @@ module.exports = BaseCollection.extend({ return -1; } return 1; + }, + search : function (letters, removeMe, addAll) { + if(letters == "" && !removeMe) return this; + + var collection = new module.exports(this.models); + if (addAll) + collection.add({id: this.parent.jid.bare + '/all'}); + + var pattern = new RegExp('^' + letters + '.*$', "i"); + var filtered = collection.filter(function(data) { + var nick = data.get("mucDisplayName"); + if (nick === me.nick) return false; + return pattern.test(nick); + }); + return new module.exports(filtered); } }); diff --git a/clientapp/pages/groupchat.js b/clientapp/pages/groupchat.js index aee3cce..42dc9e9 100644 --- a/clientapp/pages/groupchat.js +++ b/clientapp/pages/groupchat.js @@ -80,12 +80,15 @@ module.exports = BasePage.extend({ this.$chatInput.val(app.composing[this.model.jid] || ''); this.$chatBox = this.$('.chatBox'); this.$messageList = this.$('.messages'); + this.$autoComplete = this.$('.autoComplete'); this.staydown = new StayDown(this.$messageList[0], 500); this.renderMessages(); + this.renderCollection(this.model.resources, MUCRosterItem, this.$('.groupRoster')); + this.listenTo(this, 'rosterItemClicked', this.rosterItemSelected); this.listenTo(this.model.messages, 'add', this.handleChatAdded); $(window).on('resize', _.bind(this.resizeInput, this)); @@ -117,19 +120,47 @@ module.exports = BasePage.extend({ }, handleKeyDown: function (e) { if (e.which === 13 && !e.shiftKey) { - app.composing[this.model.jid] = ''; - this.sendChat(); + if (this.$autoComplete.css('display') != 'none') { + var nickname = this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")>:first-child").text(); + this.rosterItemSelected(nickname); + } else { + app.composing[this.model.jid] = ''; + this.sendChat(); + } e.preventDefault(); return false; - } else if (e.which === 38 && this.$chatInput.val() === '' && this.model.lastSentMessage) { - this.editMode = true; - this.$chatInput.addClass('editing'); - this.$chatInput.val(this.model.lastSentMessage.body); + } else if (e.which === 38) { // Up arrow + + if (this.$autoComplete.css('display') != 'none') { + var count = this.$autoComplete.find(">li").length; + var oldPos = this.autoCompletePos; + this.autoCompletePos = (oldPos - 1) < 1 ? count : oldPos - 1; + + this.$autoComplete.find(">:nth-child(" + oldPos + ")").removeClass('selected'); + this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected'); + + } + else if (this.$chatInput.val() === '' && this.model.lastSentMessage) { + this.editMode = true; + this.$chatInput.addClass('editing'); + this.$chatInput.val(this.model.lastSentMessage.body); + } e.preventDefault(); return false; - } else if (e.which === 40 && this.editMode) { - this.editMode = false; - this.$chatInput.removeClass('editing'); + } else if (e.which === 40) { // Down arrow + + if (this.$autoComplete.css('display') != 'none') { + var count = this.$autoComplete.find(">li").length; + var oldPos = this.autoCompletePos; + this.autoCompletePos = (oldPos + 1) > count ? 1 : oldPos + 1; + + this.$autoComplete.find(">:nth-child(" + oldPos + ")").removeClass('selected'); + this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected'); + } + else if (this.editMode) { + this.editMode = false; + this.$chatInput.removeClass('editing'); + } e.preventDefault(); return false; } else if (!e.ctrlKey && !e.metaKey) { @@ -158,6 +189,43 @@ module.exports = BasePage.extend({ } else if (this.typing) { this.pausedTyping(); } + + if (([38, 40, 13]).indexOf(e.which) === -1) { + var lastWord = this.$chatInput.val().split(' ').pop(); + if (lastWord.charAt(0) === '@') { + var models = this.model.resources.search(lastWord.substr(1) || '', true, true); + if (models.length) { + this.renderCollection(models, MUCRosterItem, this.$autoComplete); + this.autoCompletePos = 1; + this.$autoComplete.find(">:nth-child(" + this.autoCompletePos + ")").addClass('selected'); + this.$autoComplete.show(); + } + else + this.$autoComplete.hide(); + } + + if (this.$autoComplete.css('display') != 'none') { + if( lastWord === '') { + this.$autoComplete.hide(); + return; + } + } + } + }, + rosterItemSelected: function (nickName) { + if (nickName == me.nick) + nickName = 'me'; + var val = this.$chatInput.val(); + var splited = val.split(' '); + var length = splited.length-1; + var lastWord = splited.pop(); + if (('@' + nickName).indexOf(lastWord) > -1) + splited[length] = '@' + nickName + ' '; + else + splited.push('@' + nickName + ' '); + this.$chatInput.val(splited.join(' ')); + this.$autoComplete.hide(); + this.$chatInput.focus(); }, resizeInput: _.throttle(function () { var height; diff --git a/clientapp/templates.js b/clientapp/templates.js index 6b5746d..b70fb14 100644 --- a/clientapp/templates.js +++ b/clientapp/templates.js @@ -313,14 +313,14 @@ exports.includes.mucWrappedMessage = function anonymous(locals) { with (locals || {}) { var messageDate = Date.create(message.timestamp); buf.push('
  • ' + jade.escape((jade.interp = message.from.resource) == null ? "" : jade.interp) + "
    ' + jade.escape((jade.interp = message.sender.getName(message.from.full)) == null ? "" : jade.interp) + '
    ' + jade.escape((jade.interp = message.sender.getNickname(message.from.full)) == null ? "" : jade.interp) + "
    #
        '); + buf.push('
        #
              '); } return buf.join(""); }; diff --git a/clientapp/templates/includes/mucWrappedMessage.jade b/clientapp/templates/includes/mucWrappedMessage.jade index e348b9e..0229424 100644 --- a/clientapp/templates/includes/mucWrappedMessage.jade +++ b/clientapp/templates/includes/mucWrappedMessage.jade @@ -2,9 +2,10 @@ li .sender a.messageAvatar(href='#') - img(src="https://gravatar.com/avatar", alt=message.from.resource, data-placement="below") + img(src=message.sender.getAvatar(message.from.full), alt=message.from.resource, data-placement="below") .messageWrapper .message_header - .name #{message.from.resource} + .name #{message.sender.getName(message.from.full)} + .nickname #{message.sender.getNickname(message.from.full)} .date(title=messageDate.format('{Dow}, {MM}/{dd}/{yyyy} - {h}:{mm} {Tt}')) #{messageDate.format('{h}:{mm} {tt}')} include mucBareMessage diff --git a/clientapp/templates/pages/groupchat.jade b/clientapp/templates/pages/groupchat.jade index 5f0539e..3cc24ab 100644 --- a/clientapp/templates/pages/groupchat.jade +++ b/clientapp/templates/pages/groupchat.jade @@ -12,5 +12,6 @@ section.page.chat span#members_toggle_count ul.groupRoster .chatBox + ul.autoComplete form.formwrap textarea(name='chatInput', type='text', placeholder='Send a message...', autocomplete='off') diff --git a/clientapp/views/mucRosterItem.js b/clientapp/views/mucRosterItem.js index 9704ce4..bcfdad7 100644 --- a/clientapp/views/mucRosterItem.js +++ b/clientapp/views/mucRosterItem.js @@ -8,6 +8,9 @@ var templates = require('../templates'); module.exports = HumanView.extend({ template: templates.includes.mucRosterItem, + events: { + 'click': 'handleClick' + }, classBindings: { show: '', chatState: '', @@ -19,5 +22,8 @@ module.exports = HumanView.extend({ render: function () { this.renderAndBind({contact: this.model}); return this; + }, + handleClick: function (e) { + this.parent.trigger('rosterItemClicked', this.model.mucDisplayName); } }); diff --git a/public/css/otalk.css b/public/css/otalk.css index 9a3035f..ecc139d 100644 --- a/public/css/otalk.css +++ b/public/css/otalk.css @@ -1154,6 +1154,40 @@ button.secondary:hover:not(:disabled) { transition: none; -webkit-transition: none; } +.conversation .chatBox .autoComplete { + display: none; + position: fixed; + left: 245px; + bottom: 64px; + border: 2px solid #ddd; + border-bottom: 0; + border-radius: 0.25rem 0.25rem 0 0; + background-color: #fff; + z-index: 100; + list-style-type: none; + padding: 3px; + margin: 0px; +} +.conversation .chatBox .autoComplete li { + padding: 3px 25px; + margin: 0px; + border-radius: 0.25rem; + position: relative; + cursor: pointer; +} +.conversation .chatBox .autoComplete li:hover, +.conversation .chatBox .autoComplete li.selected { + background-color: #439fe0; +} +.conversation .chatBox .autoComplete li:hover .name, +.conversation .chatBox .autoComplete li.selected .name { + color: #fff; +} +.conversation .chatBox .autoComplete li .name { + color: #9e9ea6; + font-size: 14px; + font-weight: bold; +} .conversation .chatBox .formwrap { transition: none; -webkit-transition: none; @@ -1312,15 +1346,32 @@ button.secondary:hover:not(:disabled) { } .messages .messageWrapper .message_header .name { display: inline-block; + margin-right: 0.25rem; font-weight: 900; font-size: 15px; color: #3d3c40; line-height: 22px; cursor: pointer; } +.messages .messageWrapper .message_header .name:empty { + margin-right: 0; +} +.messages .messageWrapper .message_header .nickname { + display: inline-block; + font-weight: 500; + font-size: 14px; + color: #9e9ea6; + line-height: 22px; + cursor: pointer; +} +.messages .messageWrapper .message_header .nickname:not(:empty) { + margin-right: 0.25rem; +} +.messages .messageWrapper .message_header .nickname:not(:empty):before { + content: '@'; +} .messages .messageWrapper .message_header .date { display: inline-block; - margin-left: 0.25rem; color: #babbbf; font-size: 12px; width: 60px; diff --git a/public/css/pages/chat.styl b/public/css/pages/chat.styl index 5672238..b92917d 100644 --- a/public/css/pages/chat.styl +++ b/public/css/pages/chat.styl @@ -194,6 +194,37 @@ transition: none -webkit-transition: none + .autoComplete + display: none + position: fixed + left: 245px + bottom: 64px + border: 2px solid #ddd + border-bottom: 0 + border-radius: 0.25rem 0.25rem 0 0 + background-color: #fff + z-index: 100 + list-style-type: none + padding: 3px + margin: 0px + + li + padding: 3px 25px + margin: 0px + border-radius: 0.25rem + position: relative + cursor: pointer + + &:hover, &.selected + background-color: #439FE0 + .name + color: #fff + + .name + color: $gray + font-size: 14px + font-weight: bold + .formwrap transition: none -webkit-transition: none @@ -344,15 +375,29 @@ .name display: inline-block + margin-right: 0.25rem font-weight: $font-weight-bolder font-size: $font-size-message color: $black line-height: 22px cursor: pointer + &:empty + margin-right: 0 + + .nickname + display: inline-block + font-weight: $font-weight-classic + font-size: $font-size-base + color: $gray + line-height: 22px + cursor: pointer + &:not(:empty) + margin-right: 0.25rem + &:before + content: '@' .date display: inline-block - margin-left: 0.25rem color: $gray-light font-size: $font-size-small width: 60px