112:f4a4878c99a3
Anton Shestakov <av6@dwimlabs.net>, Thu, 07 Apr 2016 22:56:16 +0800
index: check if roster has the item before removing it Sometimes we get events from contacts that are not in user's roster, trying to remove such contacts used to fail before this patch.

next change 120:b71b4dae9feb
previous change 111:b2e3a00a9691

coffee/index.coffee

Permissions: -rw-r--r--

Other formats: Feeds:
Strophe.addNamespace('LAST', 'jabber:iq:last')
Strophe.addNamespace('TIME', 'urn:xmpp:time')
Strophe.addNamespace('VCARD_UPDATE', 'vcard-temp:x:update')
class window.Tram.ClientState extends Backbone.Model
defaults:
show: 'offline'
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.chats = {}
contacts.on 'change:show', (model) ->
if model.get('type') is 'self'
clientState.set('show', model.get('show'))
clientState.on 'action/show', (show, status) ->
X.conn.send($pres().c('priority').t('1').up().c('show').t(show).up().c('status').t(status).tree())
clientState.on 'action/disconnect', ->
X.disconnect('Logged out')
clientState.on 'change:contact', ->
contact = clientState.get('contact')
$logs = $('[data-app="logs"]')
$logs.children().detach()
$logs.append(contact.chat.render().el)
$('[data-form="send"]').toggleClass('uk-hidden', not clientState.has('contact'))
onConnected = ->
X.conn.addHandler(onPresence, null, 'presence')
X.conn.addHandler(onProfileUpdate, Strophe.NS.VCARD_UPDATE, 'presence')
X.conn.addHandler(onChatMessage, null, 'message', 'chat')
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.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(Tram.NS.WEBRTC)
X.conn.send($pres().c('priority').t('1').up().c('status').t('Online').tree())
getVersion()
X.conn.roster.get()
onDisconnected = ->
location.reload()
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
getVersion = ->
iq = $iq(type: 'get', id: X.conn.getUniqueId('version'), to: Tram.config.domain).c('query', xmlns: Strophe.NS.VERSION)
okcb = (stanza) ->
name = getText(stanza.getElementsByTagName('name')[0])
version = getText(stanza.getElementsByTagName('version')[0])
os = getText(stanza.getElementsByTagName('os')[0])
clientState.set
'server/name': name
'server/version': version
'server/os': os
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.VERSION, 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) ->
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)
return contact
getContactProfile = (contact) ->
okcb = (stanza) ->
vcard = stanza.getElementsByTagName('vCard')[0]
if !vcard
console.warn("no vcard in response", stanza)
return
contact.set
nickname: getText(vcard.querySelector('NICKNAME'))
fullname: getText(vcard.querySelector('FN'))
firstname: getText(vcard.querySelector('N > GIVEN'))
lastname: getText(vcard.querySelector('N > FAMILY'))
mime = getText(vcard.querySelector('PHOTO > TYPE'))
data = getText(vcard.querySelector('PHOTO > BINVAL'))
if mime and data
contact.set('avatar', mime: mime, data: data)
else
contact.unset('avatar')
failcb = (stanza) ->
console.warn("couldn't get vcard", stanza)
bjid = contact.get('bjid')
if bjid is Strophe.getBareJidFromJid(X.conn.jid)
bjid = null
X.conn.vcard.get(okcb, bjid, failcb)
onPresence = (stanza) ->
type = stanza.getAttribute('type') ? 'available'
from = stanza.getAttribute('from')
switch type
when 'unsubscribe'
console.warn("""not handling <presence type="#{ type }">""", stanza)
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
contact = getContact(from)
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'
delay = stanza.getElementsByTagName('delay')[0]
stamp = if delay? then new Date(delay.getAttribute('stamp')) else new Date()
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
onProfileUpdate = (stanza) ->
from = stanza.getAttribute('from')
contact = getContact(from)
getContactProfile(contact)
return true
onChatMessage = (stanza) ->
from = stanza.getAttribute('from')
type = stanza.getAttribute('type')
text = getText(stanza.getElementsByTagName('body')[0])
thread = getText(stanza.getElementsByTagName('thread')[0])
delay = stanza.getElementsByTagName('delay')[0]
stamp = if delay? then new Date(delay.getAttribute('stamp')) else new Date()
contact = getContact(from)
if text
contact.chat.collection.add
id: stanza.getAttribute('id')
type: type
cls: type
from: from
stamp: stamp
contact: contact
thread: thread
text: text
return true
onWebRTC = (stanza) ->
from = stanza.getAttribute('from')
contact = getContact(from)
intent = stanza.getElementsByTagName('intent')[0]
payload = stanza.getElementsByTagName('payload')[0]
delay = stanza.getElementsByTagName('delay')[0]
stamp = if delay? then new Date(delay.getAttribute('stamp')) else new Date()
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()
$(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')
X.conn.send($msg(to: contact.get('jid'), from: X.conn.jid, type: 'chat').c('body').t(text).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()
$('[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')
window.setInterval ->
$('time[datetime]').each ->
$this = $(this)
$this.text(moment($this.attr('datetime')).fromNow())
, 15 * 1000