151:0178573216e3
Anton Shestakov <av6@dwimlabs.net>, Thu, 14 Apr 2016 02:05:24 +0800
index: remove offline contacts only when they change to being offline Since new contacts are currently added while they still have the default presence of 'unavailable', add event sees them as offline and throws them away immediately, before their presence could change to something else.

next change 152:6f87b9997c10
previous change 150:604052f25fdc

coffee/index.coffee

Permissions: -rw-r--r--

Other formats: Feeds:
Strophe.addNamespace('CAPS', 'http://jabber.org/protocol/caps')
Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates')
Strophe.addNamespace('DELAY', 'urn:xmpp:delay')
Strophe.addNamespace('LAST', 'jabber:iq:last')
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.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.capscache = {}
window.vcardcache = {}
contacts.on 'change:show', (model) ->
if model.get('type') is 'self'
clientState.set('show', model.get('show'))
contacts.on 'change:chatstate/self', (model, cs) ->
if Strophe.NS.CHATSTATES in (model?.get('features') ? [])
msg = $msg(to: model.get('jid'), from: X.conn.jid, type: 'chat')
.c(cs, xmlns: Strophe.NS.CHATSTATES)
X.conn.send(msg.tree())
contacts.on 'change:presence', (model) ->
dupes = contacts.where(bjid: model.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 model.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', ->
clientState.previous('contact')?.set('chatstate/self', 'inactive')
contact = clientState.get('contact')
contact?.set('chatstate/self', 'active')
$logs = $('[data-app="logs"]')
$logs.children().detach()
$logs.append(contact.chat.render().el)
$('[data-form="send"]').toggleClass('uk-hidden', not contact?)
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(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.CHATSTATES)
X.conn.disco.addFeature(Strophe.NS.DISCO_INFO)
X.conn.disco.addFeature(Strophe.NS.LAST)
X.conn.disco.addFeature(Strophe.NS.PING)
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)
X.sendPresence(status: 'Online', priority: 1)
getServerVersion()
getServerInfo()
X.conn.roster.get()
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 = ->
iq = $iq(type: 'get', id: X.conn.getUniqueId('version'), to: X.conn.domain)
.c('query', xmlns: Strophe.NS.VERSION)
okcb = (stanza) ->
clientState.set
'server/name': getText(stanza.getElementsByTagName('name')[0])
'server/version': getText(stanza.getElementsByTagName('version')[0])
'server/os': getText(stanza.getElementsByTagName('os')[0])
failcb = (stanza) ->
console.error("couldn't get version", stanza?.innerHTML)
X.conn.sendIQ(iq.tree(), okcb, failcb, 5000)
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 = (from, node, ver) ->
contact = contacts.get(from)
if contact?
return contact
self = from is X.conn.jid
if self
contacts.each (model) ->
model.set('type', 'contact')
bjid = Strophe.getBareJidFromJid(from)
contact = contacts.add
jid: from
bjid: bjid
type: if self then 'self' else 'contact'
contact.chat = getChat(bjid)
contact.on 'action/chat', ->
clientState.set('contact', contact)
contact.on 'action/authorize', ->
X.conn.roster.authorize(contact.get('bjid'))
contact.on 'action/unauthorize', ->
X.conn.roster.unauthorize(contact.get('bjid'))
contacts.remove(contact)
contact.on 'action/remove', ->
if X.conn.roster.findItem(contact.get('bjid'))
X.conn.roster.remove contact.get('bjid'), ->
contacts.remove(contact)
else
contacts.remove(contact)
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')
getContactProfile(contact)
if Strophe.getResourceFromJid(from)?
getClientInfo(contact, node, ver)
return contact
getContactProfile = (contact) ->
bjid = contact.get('bjid')
if bjid is Strophe.getBareJidFromJid(X.conn.jid)
bjid = null
if vcardcache[bjid]?
data = vcardcache[bjid]
contact.set(data)
if not data.avatar?
contact.unset('avatar')
return
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
vcardcache[bjid] = data
contact.set(data)
if not data.avatar?
contact.unset('avatar')
failcb = (stanza) ->
console.warn("couldn't get vcard", stanza)
X.conn.vcard.get(okcb, bjid, failcb)
getClientInfo = (contact, node, ver) ->
jid = contact.get('jid')
if node? and ver?
clientId = "#{ node }##{ ver }"
if capscache[clientId]?
return contact.set('features', capscache[clientId])
okcb = (stanza) ->
features = (el.getAttribute('var') for el in stanza.getElementsByTagName('feature'))
contact.set('features', features)
if clientId?
capscache[clientId] = features
failcb = (stanza) ->
console.warn("couldn't get client info", stanza)
X.conn.disco.info(jid, clientId, okcb, failcb, 5000)
getServerInfo = ->
caps = X.conn.features.getElementsByTagNameNS(Strophe.NS.CAPS, 'c')[0]
node = caps?.getAttribute('node')
ver = caps?.getAttribute('ver')
if node? and ver?
serverId = "#{ node }##{ ver }"
if capscache[serverId]?
return clientState.set('server/features', capscache[serverId])
okcb = (stanza) ->
features = (el.getAttribute('var') for el in stanza.getElementsByTagName('feature'))
clientState.set('server/features', features)
capscache[serverId] = features
failcb = (stanza) ->
console.warn("couldn't get server info", stanza)
X.conn.disco.info(X.conn.domain, serverId, okcb, failcb, 5000)
onPresence = (stanza) ->
type = stanza.getAttribute('type') ? 'available'
from = stanza.getAttribute('from')
switch type
when 'unsubscribe'
# Upon receiving the presence stanza of type "unsubscribe", the contact
# SHOULD acknowledge receipt of that subscription state notification
# through either "affirming" it by sending a presence stanza of type
# "unsubscribed" to the user or ...
X.conn.roster.unauthorize(from)
return true
when 'subscribed'
# Upon receiving the presence stanza of type "subscribed", the contact
# SHOULD acknowledge receipt of that subscription state notification
# through either "affirming" it by sending a presence stanza of type
# "subscribe" to the user or ...
X.conn.roster.subscribe(from)
return true
when 'unsubscribed'
# Upon receiving the presence stanza of type "unsubscribed", the user
# SHOULD acknowledge receipt of that subscription state notification
# through either "affirming" it by sending a presence stanza of type
# "unsubscribe" to the contact or ...
X.conn.roster.unsubscribe(from)
return true
when 'error'
console.error('got <presence type="error">', stanza)
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) ->
text = getText(stanza.getElementsByTagName('body')[0])
xhtml = stanza.getElementsByTagNameNS(Strophe.NS.XHTML_IM, 'html')[0]
if not (text or xhtml)
return true
from = stanza.getAttribute('from')
type = stanza.getAttribute('type')
stamp = getStamp(stanza)
contact = getContact(from)
if xhtml? and X.conn.roster.findItem(contact.get('bjid'))
body = xhtml.getElementsByTagNameNS(Strophe.NS.XHTML, 'body')[0]
html = Strophe.createHtml(body).innerHTML
contact.chat.collection.add
id: stanza.getAttribute('id')
type: type
cls: type
to: stanza.getAttribute('to')
from: from
stamp: stamp
contact: contact
thread: getText(stanza.getElementsByTagName('thread')[0])
subject: getText(stanza.getElementsByTagName('subject')[0])
text: text
html: html
return true
onChatState = (stanza) ->
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', ->
connData.unset('auth-errors')
X.on 'authfail', ->
connData.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')
class ConnectionData extends Backbone.Model
defaults:
username: ''
password: ''
validate: (attrs, options) ->
@unset('username-errors')
@unset('password-errors')
@unset('auth-errors')
if (attrs.username ? '').trim() is ''
@set('username-errors', ['This field is required.'])
if (attrs.password ? '') is ''
@set('password-errors', ['This field is required.'])
return @has('username-errors') or @has('password-errors')
window.connData = new ConnectionData()
connectfn = ->
if connData.isValid()
X.connect(connData.get('username').trim(), connData.get('password'))
$form = $('[data-form="connect"]')
window.connRivet = rivets.bind($form.get(0), data: connData, connect: connectfn)
$form.find('input').on 'keydown', (e) ->
if (not @required or @value isnt '') and e.keyCode is 13
e.preventDefault()
index = $form.find('input').index(@)
$next = $form.find('input').eq(index + 1)
if $next.length isnt 0
$next.focus()
else
$form.find('button').trigger('click')
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 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')
stamp: new Date()
contact: contacts.findWhere(type: 'self')
text: text
$('#msg').val('')
$('[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()
$('#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