78:32a3b275330a
Anton Shestakov <av6@dwimlabs.net>, Sat, 02 Apr 2016 19:11:08 +0800
index: UserState - a new model for tracking local state and ui interactions

next change 79:e0fd1f0f59bf
previous change 76:625459f68893

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.UserState extends Backbone.Model
defaults:
show: 'offline'
window.contacts ?= new Tram.Contacts()
window.contactsApp ?= new Tram.ContactsApp(el: $('[data-app="contacts"]'), collection: contacts)
window.messages ?= new Tram.Messages()
window.logApp ?= new Tram.LogApp(el: $('[data-app="log"]'), collection: messages)
window.calls ?= new Tram.Calls()
window.callsApp ?= new Tram.CallsApp(el: $('[data-app="calls"]'), collection: calls)
window.userState ?= new Tram.UserState()
window.faviconApp ?= new Tram.FaviconApp(model: userState)
contacts.on 'change:show', (model) ->
if model.get('type') is 'self'
userState.set('show', model.get('show'))
onConnected = ->
X.conn.addHandler(onPresence, null, 'presence')
X.conn.addHandler(onProfileUpdate, Strophe.NS.VCARD_UPDATE, 'presence')
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 = ->
userState.set('show', 'offline')
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])
delay = stanza.getElementsByTagName('delay')[0]
stamp = if delay? then new Date(delay.getAttribute('stamp')) else new Date()
messages.add
id: stanza.getAttribute('id')
type: stanza.getAttribute('type')
from: stanza.getAttribute('from')
stamp: stamp
cls: 'info'
name: name
version: version
os: os
text: "connected to #{ name } #{ version } on #{ 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
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')
contact = contacts.add
jid: from
bjid: Strophe.getBareJidFromJid(from)
type: if self then 'self' else 'contact'
contact.on 'authorize', ->
X.conn.roster.authorize(contact.get('bjid'))
contact.on 'unauthorize', ->
X.conn.roster.unauthorize(contact.get('bjid'))
contacts.remove(contact)
contact.on 'remove', ->
X.conn.roster.remove contact.get('bjid'), ->
contacts.remove(contact)
contact.w = new Tram.WebRTCInterface(contact)
contact.on 'call', (media) ->
contact.set('callstate', 'outgoing')
contact.w.init(true, audio: 'a' in media, video: 'v' in media)
contact.on 'accept', (media) ->
contact.w.init(false, audio: 'a' in media, video: 'v' in media)
contact.on 'decline 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'
switch type
when 'subscribed', 'unsubscribe', 'unsubscribed'
console.warn("""not handling <presence type="#{ type }">""", stanza)
return true
when 'error'
console.error('got <presence type="error">', stanza)
return true
from = stanza.getAttribute('from')
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()
messages.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
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')
messages.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()
$('[data-disconnect-button]').on 'click', ->
X.disconnect('Logged out')
$(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')
$('[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