322:7dfdf32e8577
Anton Shestakov <av6@dwimlabs.net>, Sat, 14 Jul 2018 20:23:13 +0800
index: authorizing contact also adds it to the roster Maybe there are cases when this doesn't make sense, but so far this looks like the right thing to do.

next change 323:6195c462c8de
previous change 280:3b1f4238078c

coffee/index.coffee

Permissions: -rw-r--r--

Other formats: Feeds:
Strophe.addNamespace('CAPS', 'http://jabber.org/protocol/caps')
Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2')
Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates')
Strophe.addNamespace('CSI', 'urn:xmpp:csi:0')
Strophe.addNamespace('DELAY', 'urn:xmpp:delay')
Strophe.addNamespace('FORWARD', 'urn:xmpp:forward:0')
Strophe.addNamespace('LAST', 'jabber:iq:last')
Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0')
Strophe.addNamespace('PRIVATE', 'jabber:iq:private')
Strophe.addNamespace('TIME', 'urn:xmpp:time')
window.contacts = new Tram.Contacts()
window.contactsApp = new Tram.ContactsApp(el: $('[data-app="contacts"]'), collection: contacts)
window.calls = new Tram.Calls()
window.callsApp = new Tram.CallsApp(el: $('[data-app="calls"]'), collection: calls)
window.settings = new Tram.ClientSettings()
window.serverInfo = new Tram.ServerInfo()
window.clientState = new Tram.ClientState()
window.faviconApp = new Tram.FaviconApp(model: clientState)
window.sidebarApp = new Tram.SidebarApp(el: $('[data-app="sidebar"]'), model: clientState)
window.progressApp = new Tram.ProgressApp(el: $('[data-app="progress"]'), model: clientState)
window.chats = {}
window.profiles = new Tram.Profiles()
window.entities = new Tram.Entities()
contacts.on 'change:show', (model) ->
if model.get('type') is 'self'
clientState.set('show', model.get('show'))
origTitle = document.title
contacts.on 'change:d/unread', ->
total = contacts.reduce (memo, c) ->
memo += c.get('d/unread') ? 0
, 0
if total > 0
document.title = "(#{total}) #{origTitle}"
else
document.title = origTitle
contacts.on 'change:chatstate/self', (contact, cs) ->
if settings.get('chatstates') and Strophe.NS.CHATSTATES in (contact?.get('features') ? [])
msg = $msg(to: contact.get('jid'), from: X.conn.jid, type: 'chat')
.c(cs, xmlns: Strophe.NS.CHATSTATES)
X.conn.send(msg.tree())
contacts.on 'change:presence', (contact) ->
dupes = contacts.where(bjid: contact.get('bjid'))
offline = (c for c in dupes when c.get('presence') is 'unavailable')
if offline.length is dupes.length
offline = (c for c in offline when c.get('jid') isnt contact.get('jid'))
contacts.remove(offline)
clientState.on 'action/show', (show, status) ->
X.sendPresence(show: show, status: status, priority: 1)
clientState.on 'action/disconnect', ->
X.disconnect('Logged out')
clientState.on 'change:contact', ->
previous = clientState.previous('contact')
if previous?
previous.set('chatstate/self', 'inactive')
previous.unset('active')
contact = clientState.get('contact')
if contact?
contact.set('chatstate/self', 'active')
contact.set('active', true)
_(contacts.where(bjid: contact.get('bjid'))).each (contact) ->
contact.unset('d/unread')
$logs = $('[data-app="logs"]')
$logs.children().detach()
$logs.append(contact.chat.render().el)
contact.chat.scroll()
$('[data-form="send"]').toggleClass('uk-hidden', not contact?)
clientState.on 'change:csi', ->
state = clientState.get('csi')
X.conn.send($build(state, xmlns: Strophe.NS.CSI).tree())
onConnected = ->
X.conn.addHandler(onPresence, null, 'presence')
X.conn.addHandler(onChatMessage, null, 'message', 'chat')
X.conn.addHandler(onChatState, Strophe.NS.CHATSTATES, 'message')
X.conn.addHandler(onWebRTC, Tram.NS.WEBRTC, 'message', 'chat')
X.conn.addHandler(onAnyError, null, null, 'error')
X.conn.addHandler(onGetLast, Strophe.NS.LAST, 'iq', 'get')
X.conn.addHandler(onGetTime, Strophe.NS.TIME, 'iq', 'get')
X.conn.addHandler(onGetVersion, Strophe.NS.VERSION, 'iq', 'get')
X.conn.ping.addPingHandler(onPing)
X.conn.disco.addIdentity('client', 'web', Tram.info.client)
X.conn.disco.addFeature(Strophe.NS.CAPS)
X.conn.disco.addFeature(Strophe.NS.CARBONS)
X.conn.disco.addFeature(Strophe.NS.CHATSTATES)
X.conn.disco.addFeature(Strophe.NS.CSI)
X.conn.disco.addFeature(Strophe.NS.DISCO_INFO)
X.conn.disco.addFeature(Strophe.NS.LAST)
X.conn.disco.addFeature(Strophe.NS.MESSAGE_CORRECT)
X.conn.disco.addFeature(Strophe.NS.PING)
X.conn.disco.addFeature(Strophe.NS.PRIVATE)
X.conn.disco.addFeature(Strophe.NS.TIME)
X.conn.disco.addFeature(Strophe.NS.VERSION)
X.conn.disco.addFeature(Strophe.NS.XHTML_IM)
X.conn.disco.addFeature(Tram.NS.WEBRTC)
dSettings = $.Deferred()
dCache = $.Deferred()
dVersion = $.Deferred()
dInfo = $.Deferred()
dCarbons = $.Deferred()
dRoster = $.Deferred()
loadSettings(dSettings.resolve, dSettings.reject)
loadEntityCache(dCache.resolve, dCache.reject)
getServerVersion(dVersion.resolve, dVersion.reject)
X.conn.roster.get(dRoster.resolve)
$.when(dCache).always ->
getServerEntity(dInfo.resolve, dInfo.reject)
$.when(dInfo).always ->
if Strophe.NS.CARBONS in serverInfo.get('features')
enableCarbons(dCarbons.resolve, dCarbons.reject)
else
dCarbons.reject()
hasCSI = X.conn.features?.getElementsByTagNameNS(Strophe.NS.CSI, 'csi')[0]?
serverInfo.set('csi', hasCSI)
if hasCSI
enableCSI()
$.when(dSettings, dVersion, dInfo, dRoster, dCarbons).always ->
X.sendPresence(status: 'Online', priority: 1)
entities.on('add change', saveEntityCache)
settings.on('change', saveSettings)
onDisconnected = ->
location.reload()
getStamp = (stanza) ->
delay = stanza.getElementsByTagNameNS(Strophe.NS.DELAY, 'delay')[0]
return new Date(delay?.getAttribute('stamp') ? Date.now())
getText = (el) ->
if not el
return null
str = ''
if el.childNodes.length is 0 and el.nodeType is Strophe.ElementType.TEXT
str += el.nodeValue
for node in el.childNodes
if node.nodeType is Strophe.ElementType.TEXT
str += node.nodeValue
return str
getServerVersion = (doneCallback, failCallback) ->
iq = $iq(type: 'get', id: X.conn.getUniqueId('version'), to: X.conn.domain)
.c('query', xmlns: Strophe.NS.VERSION)
okcb = (stanza) ->
serverInfo.set
name: getText(stanza.getElementsByTagName('name')[0])
version: getText(stanza.getElementsByTagName('version')[0])
os: getText(stanza.getElementsByTagName('os')[0])
doneCallback?()
failcb = (stanza) ->
console.error("couldn't get version", stanza?.innerHTML)
failCallback?()
X.conn.sendIQ(iq.tree(), okcb, failcb, Tram.config.iqTimeout)
enableCarbons = (doneCallback, failCallback) ->
iq = $iq(type: 'set', id: X.conn.getUniqueId('enable'))
.c('enable', xmlns: Strophe.NS.CARBONS)
okcb = (stanza) ->
serverInfo.set('carbons', true)
doneCallback?()
failcb = (stanza) ->
serverInfo.set('carbons', false)
failCallback?()
X.conn.sendIQ(iq.tree(), okcb, failcb, Tram.config.iqTimeout)
enableCSI = ->
csiTimer = null
$(window).on 'focus.tram', ->
if csiTimer?
window.clearTimeout(csiTimer)
csiTimer = null
clientState.set('csi', 'active')
$(window).on 'blur.tram', ->
if csiTimer?
window.clearTimeout(csiTimer)
csiTimer = window.setTimeout ->
clientState.set('csi', 'inactive')
, Tram.config.inactiveTime
loadSettings = (doneCallback, failCallback) ->
iq = $iq(type: 'get', id: X.conn.getUniqueId('settings'))
.c('query', xmlns: Strophe.NS.PRIVATE)
.c('settings', xmlns: Tram.NS.PRIVATE)
okcb = (stanza) ->
el = stanza.getElementsByTagName('settings')[0]
if el?.firstChild?
try
data = JSON.parse(el.firstChild.nodeValue)
catch
failCallback?()
return
if data?
settings.set(data)
doneCallback?()
failcb = (stanza) ->
console.error("couldn't load settings", stanza)
failCallback?()
X.conn.sendIQ(iq.tree(), okcb, failcb, Tram.config.iqTimeout)
_saveSettings = (doneCallback, failCallback) ->
payload = JSON.stringify(settings.toJSON())
iq = $iq(type: 'set', id: X.conn.getUniqueId('settings'))
.c('query', xmlns: Strophe.NS.PRIVATE)
.c('settings', xmlns: Tram.NS.PRIVATE).t(payload)
okcb = (stanza) ->
doneCallback?()
failcb = (stanza) ->
console.warn("couldn't save settings", stanza)
failCallback?()
X.conn.sendIQ(iq.tree(), okcb, failcb, Tram.config.iqTimeout)
saveSettings = _(_saveSettings).debounce(1000)
loadEntityCache = (doneCallback, failCallback) ->
iq = $iq(type: 'get', id: X.conn.getUniqueId('entity-cache'))
.c('query', xmlns: Strophe.NS.PRIVATE)
.c('entity-cache', xmlns: Tram.NS.PRIVATE)
okcb = (stanza) ->
el = stanza.getElementsByTagName('entity-cache')[0]
if el?.firstChild?
try
data = JSON.parse(el.firstChild.nodeValue)
catch
failCallback?()
return
if data?
entities.add(data)
doneCallback?()
failcb = (stanza) ->
console.error("couldn't load caps cache", stanza)
failCallback?()
X.conn.sendIQ(iq.tree(), okcb, failcb, Tram.config.iqTimeout)
_saveEntityCache = (doneCallback, failCallback) ->
payload = JSON.stringify(entities.chain().filter((e) -> e.has('id')).map((e) -> e.toJSON()).value())
iq = $iq(type: 'set', id: X.conn.getUniqueId('entity-cache'))
.c('query', xmlns: Strophe.NS.PRIVATE)
.c('entity-cache', xmlns: Tram.NS.PRIVATE).t(payload)
okcb = (stanza) ->
doneCallback?()
failcb = (stanza) ->
console.warn("couldn't save entity cache", stanza)
failCallback?()
X.conn.sendIQ(iq.tree(), okcb, failcb, Tram.config.iqTimeout)
saveEntityCache = _(_saveEntityCache).debounce(1000)
onAnyError = (stanza) ->
console.warn('got an error:', stanza, stanza.outerHTML)
return true
onGetLast = (stanza) ->
id = stanza.getAttribute('id')
from = stanza.getAttribute('from')
iq = $iq(to: from, type: 'result', id: id)
.c('query', xmlns: Strophe.NS.LAST, seconds: '0')
X.conn.send(iq.tree())
return true
onGetTime = (stanza) ->
now = moment()
id = stanza.getAttribute('id')
from = stanza.getAttribute('from')
iq = $iq(to: from, type: 'result', id: id)
.c('time', xmlns: Strophe.NS.TIME)
.c('tzo').t(now.format('Z')).up()
.c('utc').t(now.toISOString())
X.conn.send(iq.tree())
return true
onGetVersion = (stanza) ->
id = stanza.getAttribute('id')
from = stanza.getAttribute('from')
iq = $iq(to: from, type: 'result', id: id)
.c('query', xmlns: Strophe.NS.VERSION)
.c('name').t(Tram.info.client).up()
.c('version').t(Tram.info.version)
X.conn.send(iq.tree())
return true
onPing = (stanza) ->
X.conn.ping.pong(stanza)
return true
getChat = (jid) ->
chats[jid] ?= new Tram.LogApp(collection: new Tram.Messages())
getContact = (jid, node, ver) ->
contact = contacts.get(jid)
if contact?
return contact
self = jid is X.conn.jid
if self
contacts.each (model) ->
model.set('type', 'contact')
bjid = Strophe.getBareJidFromJid(jid)
contact = contacts.add
jid: jid
bjid: bjid
type: if self then 'self' else 'contact'
contact.set('profile', getProfile(bjid))
if Strophe.getResourceFromJid(jid)?
getClientEntity(contact, node, ver)
contact.chat = getChat(bjid)
contact.chat.collection.on 'add', (model) ->
if model.get('type') is 'chat' and model.get('from') is contact.get('jid') and clientState.get('contact')?.get('bjid') isnt bjid
contact.set('d/unread', (contact.get('d/unread') ? 0) + 1)
contact.on 'action/chat', ->
clientState.set('contact', contact)
contact.on 'action/authorize', ->
X.conn.roster.authorize(bjid)
if not X.conn.roster.findItem(bjid)
X.conn.roster.add bjid, null, [], ->
X.conn.roster.subscribe(bjid, 'Subscribing back', null)
contacts.remove(contact)
contact.on 'action/unauthorize', ->
X.conn.roster.unauthorize(bjid)
contacts.remove(contact)
contact.on 'action/remove', ->
if X.conn.roster.findItem(bjid)
X.conn.roster.remove bjid, ->
contacts.remove(contacts.where(bjid: bjid))
else
if contacts.find(bjid: bjid, presence: 'subscribe')?
X.conn.roster.unauthorize(bjid)
contacts.remove(contacts.where(bjid: bjid))
contact.w = new Tram.WebRTCInterface(contact)
contact.on 'action/call', (media) ->
contact.set('callstate', 'outgoing')
contact.w.init(true, audio: 'a' in media, video: 'v' in media)
contact.on 'action/accept', (media) ->
contact.w.init(false, audio: 'a' in media, video: 'v' in media)
contact.on 'action/decline action/hangup', ->
contact.w.sendIntent('terminate')
contact.w.disconnect()
contact.unset('callstate')
return contact
getProfile = (bjid) ->
profile = profiles.get(bjid)
if profile?
return profile
profile = profiles.add(bjid: bjid)
okcb = (stanza) ->
vcard = stanza.getElementsByTagNameNS(Strophe.NS.VCARD, 'vCard')[0]
if not vcard?
console.warn("no vcard in response", stanza)
return
data =
nickname: getText(vcard.querySelector('NICKNAME'))
fullname: getText(vcard.querySelector('FN'))
firstname: getText(vcard.querySelector('N > GIVEN'))
lastname: getText(vcard.querySelector('N > FAMILY'))
avatar =
mime: getText(vcard.querySelector('PHOTO > TYPE'))
data: getText(vcard.querySelector('PHOTO > BINVAL'))
if avatar.mime and avatar.data
data.avatar = avatar
profile.set(data)
failcb = (stanza) ->
console.warn("couldn't get vcard", stanza)
to = bjid
if to is Strophe.getBareJidFromJid(X.conn.jid)
to = null
X.conn.vcard.get(okcb, to, failcb)
return profile
getClientEntity = (contact, node, ver) ->
jid = contact.get('jid')
if node? and ver?
nodever = "#{ node }##{ ver }"
entity = entities.get(nodever)
if entity?
contact.set('entity', entity)
contact.set('features', entity.get('features'))
return entity
entity = entities.add(id: nodever)
else
entity = entities.add(jid: jid)
contact.set('entity', entity)
okcb = (stanza) ->
identities = ({
type: el.getAttribute('type')
name: el.getAttribute('name')
category: el.getAttribute('category')
} for el in stanza.getElementsByTagName('identity'))
features = (el.getAttribute('var') for el in stanza.getElementsByTagName('feature'))
entity.set
client: _(identities).findWhere(category: 'client')
identities: identities
features: features
contact.set('features', features)
failcb = (stanza) ->
console.warn("couldn't get client info", stanza)
X.conn.disco.info(jid, nodever, okcb, failcb, Tram.config.iqTimeout)
return entity
getServerEntity = (doneCallback, failCallback) ->
jid = X.conn.domain
caps = X.conn.features.getElementsByTagNameNS(Strophe.NS.CAPS, 'c')[0]
node = caps?.getAttribute('node')
ver = caps?.getAttribute('ver')
if node? and ver?
nodever = "#{ node }##{ ver }"
entity = entities.get(nodever)
if entity?
serverInfo.set('entity', entity)
serverInfo.set('features', entity.get('features'))
doneCallback?()
return entity
entity = entities.add(id: nodever)
else
entity = entities.add(jid: jid)
serverInfo.set('entity', entity)
okcb = (stanza) ->
identities = ({
type: el.getAttribute('type')
name: el.getAttribute('name')
category: el.getAttribute('category')
} for el in stanza.getElementsByTagName('identity'))
features = (el.getAttribute('var') for el in stanza.getElementsByTagName('feature'))
entity.set
client: _(identities).findWhere(category: 'client')
identities: identities
features: features
serverInfo.set('features', features)
doneCallback?()
failcb = (stanza) ->
console.warn("couldn't get server info", stanza)
failCallback?()
X.conn.disco.info(jid, nodever, okcb, failcb, Tram.config.iqTimeout)
return entity
onPresence = (stanza) ->
type = stanza.getAttribute('type') ? 'available'
from = stanza.getAttribute('from')
bjid = Strophe.getBareJidFromJid(from)
# +--------------------------------------------------+
# | STANZA TYPE | ACCEPT | DENY |
# +--------------------------------------------------+
# | subscribe | subscribed | unsubscribed |
# | subscribed | subscribe | unsubscribe |
# | unsubscribe | unsubscribed | subscribed |
# | unsubscribed | unsubscribe | subscribe |
# +--------------------------------------------------+
switch type
when 'subscribe'
if X.conn.roster.findItem(bjid)
X.conn.roster.authorize(bjid)
return true
when 'subscribed'
X.conn.roster.subscribe(bjid)
return true
when 'unsubscribe'
X.conn.roster.unauthorize(bjid)
return true
when 'unsubscribed'
X.conn.roster.unsubscribe(bjid)
return true
when 'error'
return true
caps = stanza.getElementsByTagNameNS(Strophe.NS.CAPS, 'c')[0]
ver = caps?.getAttribute('ver')
node = caps?.getAttribute('node')
contact = getContact(from, node, ver)
show = getText(stanza.getElementsByTagName('show')[0])
status = getText(stanza.getElementsByTagName('status')[0])
priority = getText(stanza.getElementsByTagName('priority')[0])
switch type
when 'available'
show ?= 'online'
priority ?= '0'
when 'unavailable'
show = 'offline'
priority = '0'
contact.set
presence: type
show: show
status: status
priority: priority
if type is 'unavailable'
contact.w.disconnect()
if type in ['available', 'unavailable'] and contact.get('type') isnt 'self'
stamp = getStamp(stanza)
contact.chat.collection.add
id: stanza.getAttribute('id')
type: 'presence'
cls: 'presence'
from: from
stamp: stamp
contact: contact
presence: type
show: show
status: status
priority: priority
text: status ? "is now #{ show }"
return true
onChatMessage = (stanza) ->
sent = stanza.getElementsByTagNameNS(Strophe.NS.CARBONS, 'sent')[0]
received = stanza.getElementsByTagNameNS(Strophe.NS.CARBONS, 'received')[0]
carbon = sent? or received?
if sent?
forwarded = sent.getElementsByTagNameNS(Strophe.NS.FORWARD, 'forwarded')[0]
else if received?
forwarded = received.getElementsByTagNameNS(Strophe.NS.FORWARD, 'forwarded')[0]
if forwarded?
message = forwarded.getElementsByTagName('message')[0]
if carbon
onChatState(message)
else
message = stanza
text = getText(message.getElementsByTagName('body')[0])
xhtml = message.getElementsByTagNameNS(Strophe.NS.XHTML_IM, 'html')[0]
if not (text or xhtml)
return true
id = message.getAttribute('id')
to = message.getAttribute('to')
from = message.getAttribute('from')
type = message.getAttribute('type') ? 'normal'
thread = getText(message.getElementsByTagName('thread')[0])
subject = getText(message.getElementsByTagName('subject')[0])
cls = type
stamp = getStamp(stanza)
if type is 'error'
return true
contact = getContact(from)
fromSelf = contact.get('bjid') is Strophe.getBareJidFromJid(X.conn.jid)
if xhtml? and (fromSelf or X.conn.roster.findItem(contact.get('bjid')))
body = xhtml.getElementsByTagNameNS(Strophe.NS.XHTML, 'body')[0]
html = Strophe.createHtml(body).innerHTML
if forwarded? and fromSelf
chat = getContact(to).chat
cls = 'self'
else
chat = contact.chat
replace = stanza.getElementsByTagNameNS(Strophe.NS.MESSAGE_CORRECT, 'replace')[0]
rid = replace?.getAttribute('id')
if rid?
msg = chat.collection.get(rid)
if msg? and msg.get('from') is from
msg.set
id: id
corrected: true
carbon: carbon
thread: thread
subject: subject
text: text
html: html
return true
chat.collection.add
id: id
type: type
cls: cls
to: to
from: from
contact: contact
stamp: stamp
carbon: carbon
thread: thread
subject: subject
text: text
html: html
return true
onChatState = (stanza) ->
delay = stanza.getElementsByTagNameNS(Strophe.NS.DELAY, 'delay')[0]
if delay?
return true
from = stanza.getAttribute('from')
contact = getContact(from)
elements = stanza.getElementsByTagNameNS(Strophe.NS.CHATSTATES, '*')
if elements[0]?
contact.set('chatstate', elements[0].tagName.toLowerCase())
return true
onWebRTC = (stanza) ->
from = stanza.getAttribute('from')
contact = getContact(from)
intent = stanza.getElementsByTagNameNS(Tram.NS.WEBRTC, 'intent')[0]
payload = stanza.getElementsByTagNameNS(Tram.NS.WEBRTC, 'payload')[0]
stamp = getStamp(stanza)
if intent?
switch getText(intent)
when 'initiate'
contact.set('callstate', 'incoming')
contact.chat.collection.add
id: stanza.getAttribute('id')
type: 'call'
cls: 'call'
from: from
stamp: stamp
contact: contact
text: 'incoming call'
when 'terminate'
contact.w.disconnect()
contact.unset('callstate')
if payload?
contact.w.onPayload(stanza)
return true
window.X = new Tram.XMPPInterface()
X.on 'connecting', ->
connForm.unset('auth-errors')
X.on 'authfail', ->
connForm.set('auth-errors', ['Invalid username or password.'])
X.on 'disconnected', ->
$('[data-step="login"]').removeClass('uk-hidden')
$('[data-step="main"]').addClass('uk-hidden')
onDisconnected()
X.on 'connected attached', ->
$('[data-step="login"]').addClass('uk-hidden')
$('[data-step="main"]').removeClass('uk-hidden')
onConnected()
X.on 'status', (status) ->
switch status
when Strophe.Status.CONNECTING
clientState.set('progress', 0)
when Strophe.Status.CONNECTED
clientState.set('progress', 100)
else
clientState.unset('progress')
$(window).on 'beforeunload unload', ->
X.disconnect('Window closed')
window.connForm = new Tram.ConnectionForm()
connectfn = ->
if connForm.isValid()
X.connect(connForm.get('username').trim(), connForm.get('password'))
$form = $('[data-form="connect"]')
$form.streamline()
window.connRivet = rivets.bind($form, form: connForm, connect: connectfn)
sendMessage = ->
text = $('#msg').val()
if text isnt '' and clientState.has('contact')
contact = clientState.get('contact')
msg = $msg(to: contact.get('jid'), from: X.conn.jid, type: 'chat').c('body').t(text).up()
if settings.get('chatstates') and Strophe.NS.CHATSTATES in (contact.get('features') ? [])
msg.c('active', xmlns: Strophe.NS.CHATSTATES)
contact.set('chatstate/self', 'active', silent: true)
X.conn.send(msg.tree())
contact.chat.collection.add
type: 'chat'
cls: 'self'
from: X.conn.jid
to: contact.get('jid')
contact: contacts.findWhere(type: 'self')
text: text
contact.chat.scroll()
$('#msg').val('')
$('body').on 'dragover dragenter', (event) ->
event.stopPropagation()
event.preventDefault()
$('body').on 'drop', (event) ->
event.stopPropagation()
event.preventDefault()
$('[data-send-button]').on('click', sendMessage)
$('#msg').on 'keypress', (e) ->
if e.keyCode is 13
sendMessage()
else
clientState.get('contact')?.set('chatstate/self', 'composing')
$('#msg').on 'blur', ->
if clientState.get('contact')?.get('chatstate/self') is 'composing'
clientState.get('contact')?.set('chatstate/self', 'paused')
$('[data-add-button]').on 'click', ->
jid = $('#new-contact').val().trim()
if jid is ''
return
if '@' not in jid
jid = "#{ jid }@#{ Tram.config.domain }"
$('#new-contact').val('')
X.conn.roster.add jid, null, [], ->
X.conn.roster.subscribe(jid, 'I want to chat', null)
$('#new-contact').on 'keypress', (e) ->
if e.keyCode is 13
$('[data-add-button]').trigger('click')
$('[data-unregister-button]').on 'click', ->
X.unregister()
$('#settings-modal').one 'show.uk.modal', ->
window.settingsRivet = rivets.bind($('[data-app="settings"]'), settings: settings)
window.serverRivet = rivets.bind($('[data-app="server-info"]'), server: serverInfo)
$('#profile-modal').one 'show.uk.modal', ->
window.profileRivet = rivets.bind($('[data-app="profile"]'), contact: contacts.get(X.conn.jid))
window.setInterval ->
$('time[datetime]').each ->
$this = $(this)
$this.text(moment($this.attr('datetime')).fromNow())
, 15 * 1000