/* global Metamaps, $, Howl */ /* * Dependencies: * Metamaps.Erb */ import Backbone from 'backbone' import Autolinker from 'autolinker' import _ from 'lodash' import underscore from 'underscore' import outdent from 'outdent' // TODO is this line good or bad // Backbone.$ = window.$ const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }) var Private = { messageHTML: outdent` <div class='chat-message'> <div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div> <div class='chat-message-text'>{{ message }}</div> <div class='chat-message-time'>{{ timestamp }}</div> <div class='clearfloat'></div> </div>`, participantHTML: outdent` <div class='participant participant-{{ id }} {{ selfClass }}'> <div class='chat-participant-image'> <img src='{{ image }}' style='border: 2px solid {{ color }};' /> </div> <div class='chat-participant-name'> {{ username }} {{ selfName }} </div> <button type='button' class='button chat-participant-invite-call' onclick='Metamaps.Realtime.inviteACall({{ id}});' ></button> <button type='button' class='button chat-participant-invite-join' onclick='Metamaps.Realtime.inviteToJoin({{ id}});' ></button> <span class='chat-participant-participating'> <div class='green-dot'></div> </span> <div class='clearfloat'></div> </div>`, templates: function () { underscore.templateSettings = { interpolate: /\{\{(.+?)\}\}/g } this.messageTemplate = underscore.template(Private.messageHTML) this.participantTemplate = underscore.template(Private.participantHTML) }, createElements: function () { this.$unread = $('<div class="chat-unread"></div>') this.$button = $('<div class="chat-button"><div class="tooltips">Chat</div></div>') this.$messageInput = $('<textarea placeholder="Send a message..." class="chat-input"></textarea>') this.$juntoHeader = $('<div class="junto-header">PARTICIPANTS</div>') this.$videoToggle = $('<div class="video-toggle"></div>') this.$cursorToggle = $('<div class="cursor-toggle"></div>') this.$participants = $('<div class="participants"></div>') this.$conversationInProgress = $(outdent` <div class="conversation-live"> LIVE <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();"> LEAVE </span> <span class="call-action join" onclick="Metamaps.Realtime.joinCall();"> JOIN </span> </div>`) this.$chatHeader = $('<div class="chat-header">CHAT</div>') this.$soundToggle = $('<div class="sound-toggle"></div>') this.$messages = $('<div class="chat-messages"></div>') this.$container = $('<div class="chat-box"></div>') }, attachElements: function () { this.$button.append(this.$unread) this.$juntoHeader.append(this.$videoToggle) this.$juntoHeader.append(this.$cursorToggle) this.$chatHeader.append(this.$soundToggle) this.$participants.append(this.$conversationInProgress) this.$container.append(this.$juntoHeader) this.$container.append(this.$participants) this.$container.append(this.$chatHeader) this.$container.append(this.$button) this.$container.append(this.$messages) this.$container.append(this.$messageInput) }, addEventListeners: function () { var self = this this.participants.on('add', function (participant) { Private.addParticipant.call(self, participant) }) this.participants.on('remove', function (participant) { Private.removeParticipant.call(self, participant) }) this.$button.on('click', function () { Handlers.buttonClick.call(self) }) this.$videoToggle.on('click', function () { Handlers.videoToggleClick.call(self) }) this.$cursorToggle.on('click', function () { Handlers.cursorToggleClick.call(self) }) this.$soundToggle.on('click', function () { Handlers.soundToggleClick.call(self) }) this.$messageInput.on('keyup', function (event) { Handlers.keyUp.call(self, event) }) this.$messageInput.on('focus', function () { Handlers.inputFocus.call(self) }) this.$messageInput.on('blur', function () { Handlers.inputBlur.call(self) }) }, initializeSounds: function () { this.sound = new Howl({ urls: [Metamaps.Erb['sounds/MM_sounds.mp3'], Metamaps.Erb['sounds/MM_sounds.ogg']], sprite: { joinmap: [0, 561], leavemap: [1000, 592], receivechat: [2000, 318], sendchat: [3000, 296], sessioninvite: [4000, 5393, true] } }) }, incrementUnread: function () { this.unreadMessages++ this.$unread.html(this.unreadMessages) this.$unread.show() }, addMessage: function (message, isInitial, wasMe) { if (!this.isOpen && !isInitial) Private.incrementUnread.call(this) function addZero (i) { if (i < 10) { i = '0' + i } return i } var m = _.clone(message.attributes) m.timestamp = new Date(m.created_at) var date = (m.timestamp.getMonth() + 1) + '/' + m.timestamp.getDate() date += ' ' + addZero(m.timestamp.getHours()) + ':' + addZero(m.timestamp.getMinutes()) m.timestamp = date m.image = m.user_image || 'http://www.hotpepper.ca/wp-content/uploads/2014/11/default_profile_1_200x200.png' // TODO: remove m.message = linker.link(m.message) var $html = $(this.messageTemplate(m)) this.$messages.append($html) if (!isInitial) this.scrollMessages(200) if (!wasMe && !isInitial && this.alertSound) this.sound.play('receivechat') }, initialMessages: function () { var messages = this.messages.models for (var i = 0; i < messages.length; i++) { Private.addMessage.call(this, messages[i], true) } }, handleInputMessage: function () { var message = { message: this.$messageInput.val() } this.$messageInput.val('') $(document).trigger(ChatView.events.message + '-' + this.room, [message]) }, addParticipant: function (participant) { var p = _.clone(participant.attributes) if (p.self) { p.selfClass = 'is-self' p.selfName = '(me)' } else { p.selfClass = '' p.selfName = '' } var html = this.participantTemplate(p) this.$participants.append(html) }, removeParticipant: function (participant) { this.$container.find('.participant-' + participant.get('id')).remove() } } var Handlers = { buttonClick: function () { if (this.isOpen) this.close() else if (!this.isOpen) this.open() }, videoToggleClick: function () { this.$videoToggle.toggleClass('active') this.videosShowing = !this.videosShowing $(document).trigger(this.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff) }, cursorToggleClick: function () { this.$cursorToggle.toggleClass('active') this.cursorsShowing = !this.cursorsShowing $(document).trigger(this.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff) }, soundToggleClick: function () { this.alertSound = !this.alertSound this.$soundToggle.toggleClass('active') }, keyUp: function (event) { switch (event.which) { case 13: // enter Private.handleInputMessage.call(this) break } }, inputFocus: function () { $(document).trigger(ChatView.events.inputFocus) }, inputBlur: function () { $(document).trigger(ChatView.events.inputBlur) } } const ChatView = function (messages, mapper, room) { this.room = room this.mapper = mapper this.messages = messages // backbone collection this.isOpen = false this.alertSound = true // whether to play sounds on arrival of new messages or not this.cursorsShowing = true this.videosShowing = true this.unreadMessages = 0 this.participants = new Backbone.Collection() Private.templates.call(this) Private.createElements.call(this) Private.attachElements.call(this) Private.addEventListeners.call(this) Private.initialMessages.call(this) Private.initializeSounds.call(this) this.$container.css({ right: '-300px' }) } ChatView.prototype.conversationInProgress = function (participating) { this.$conversationInProgress.show() this.$participants.addClass('is-live') if (participating) this.$participants.addClass('is-participating') this.$button.addClass('active') // hide invite to call buttons } ChatView.prototype.conversationEnded = function () { this.$conversationInProgress.hide() this.$participants.removeClass('is-live') this.$participants.removeClass('is-participating') this.$button.removeClass('active') this.$participants.find('.participant').removeClass('active') this.$participants.find('.participant').removeClass('pending') } ChatView.prototype.leaveConversation = function () { this.$participants.removeClass('is-participating') } ChatView.prototype.mapperJoinedCall = function (id) { this.$participants.find('.participant-' + id).addClass('active') } ChatView.prototype.mapperLeftCall = function (id) { this.$participants.find('.participant-' + id).removeClass('active') } ChatView.prototype.invitationPending = function (id) { this.$participants.find('.participant-' + id).addClass('pending') } ChatView.prototype.invitationAnswered = function (id) { this.$participants.find('.participant-' + id).removeClass('pending') } ChatView.prototype.addParticipant = function (participant) { this.participants.add(participant) } ChatView.prototype.removeParticipant = function (username) { var p = this.participants.find(p => p.get('username') === username) if (p) { this.participants.remove(p) } } ChatView.prototype.removeParticipants = function () { this.participants.remove(this.participants.models) } ChatView.prototype.open = function () { this.$container.css({ right: '0' }) this.$messageInput.focus() this.isOpen = true this.unreadMessages = 0 this.$unread.hide() this.scrollMessages(0) $(document).trigger(ChatView.events.openTray) } ChatView.prototype.addMessage = function (message, isInitial, wasMe) { this.messages.add(message) Private.addMessage.call(this, message, isInitial, wasMe) } ChatView.prototype.scrollMessages = function (duration) { duration = duration || 0 this.$messages.animate({ scrollTop: this.$messages[0].scrollHeight }, duration) } ChatView.prototype.clearMessages = function () { this.unreadMessages = 0 this.$unread.hide() this.$messages.empty() } ChatView.prototype.close = function () { this.$container.css({ right: '-300px' }) this.$messageInput.blur() this.isOpen = false $(document).trigger(ChatView.events.closeTray) } ChatView.prototype.remove = function () { this.$button.off() this.$container.remove() } /** * @class * @static */ ChatView.events = { message: 'ChatView:message', openTray: 'ChatView:openTray', closeTray: 'ChatView:closeTray', inputFocus: 'ChatView:inputFocus', inputBlur: 'ChatView:inputBlur', cursorsOff: 'ChatView:cursorsOff', cursorsOn: 'ChatView:cursorsOn', videosOff: 'ChatView:videosOff', videosOn: 'ChatView:videosOn' } export default ChatView