diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..3a2b74f --- /dev/null +++ b/README.markdown @@ -0,0 +1,8 @@ +Jappix for Yunohost +============ + +[Yunohost project](https://yunohost.org/) + +Official website: + +Jappix v1.1.2 diff --git a/conf/main.xml b/conf/main.xml index a956031..bce22bc 100644 --- a/conf/main.xml +++ b/conf/main.xml @@ -1,23 +1,24 @@ - YunoJappix + CHANGENAME a free social network CHANGELANG - YunoJappix + CHANGENAME on off on off on - + support@conference.yunohost.org off on off off off + on off diff --git a/manifest.json b/manifest.json index 54ad0e9..2e27640 100644 --- a/manifest.json +++ b/manifest.json @@ -5,6 +5,7 @@ "en": "A free social network", "fr": "Un réseau social libre" }, + "licence": "AGPL v3", "developer": { "name": "titoko", "email": "titoko@titoko.fr", @@ -16,27 +17,38 @@ { "name": "domain", "ask": { - "en": "Choose a domain for Jappix" + "en": "Choose a domain for Jappix", + "fr": "Choisissez un domaine pour Jappix" }, "example": "domain.org" }, { "name": "path", "ask": { - "en": "Choose a path for Jappix" + "en": "Choose a path for Jappix", + "fr": "Choisissez un chemin pour Jappix" }, "example": "/jappix", "default": "/jappix" }, + { + "name": "name", + "ask": { + "en": "Choose a name for Jappix", + "fr": "Choisissez un nom pour Jappix" + }, + "example": "YunoJappix", + "default": "YunoJappix" + }, { "name": "language", "ask": { - "en": "Choose the language of the Jappix", - "fr": "Choissisez la langue du Jappix" - }, - "example": "en", - "default": "en" - } + "en": "Choose the language of the Jappix", + "fr": "Choissisez la langue du Jappix" + }, + "choices" : ["en", "fr", "es"], + "default" : "en" + } ] } } diff --git a/scripts/backup b/scripts/backup new file mode 100644 index 0000000..fa6583b --- /dev/null +++ b/scripts/backup @@ -0,0 +1,15 @@ +#!/bin/bash +app=jappix + +# The parameter $1 is the backup directory location +# which will be compressed afterward +backup_dir=$1/apps/$app +mkdir -p $backup_dir + +# Backup sources & data +sudo cp -a /var/www/$app/. $backup_dir/sources + +# Copy Nginx and YunoHost parameters to make the script "standalone" +sudo cp -a /etc/yunohost/apps/$app/. $backup_dir/yunohost +domain=$(sudo yunohost app setting $app domain) +sudo cp -a /etc/nginx/conf.d/$domain.d/$app.conf $backup_dir/nginx.conf \ No newline at end of file diff --git a/scripts/install b/scripts/install index 3ba9cf6..b28e42b 100644 --- a/scripts/install +++ b/scripts/install @@ -3,7 +3,8 @@ # Retrieve arguments domain=$1 path=$2 -language=$3 +name=$3 +language=$4 # Check domain/path availability sudo yunohost app checkurl $domain$path -a jappix @@ -15,10 +16,10 @@ fi path=${path%/} # Copy files to the right place - final_path=/var/www/jappix - sudo mkdir -p $final_path - sudo cp -r ../source/* $final_path - sudo cp ../conf/*.xml $final_path/store/conf/ +final_path=/var/www/jappix +sudo mkdir -p $final_path +sudo cp -r ../source/* $final_path +sudo cp ../conf/*.xml $final_path/store/conf/ # Set permissions to jappix directory sudo chown -R www-data: $final_path @@ -27,7 +28,7 @@ sudo chown -R www-data: $final_path sudo sed -i "s@PATHTOCHANGE2@$path@g" ../conf/nginx.conf if [ -z "$path" ]; then - path="/" + path="/" fi sudo ls $final_path/i18n/$language > /dev/null 2>&1 @@ -36,6 +37,7 @@ then language="en" fi +sudo yunohost app setting jappix name -v $language sudo yunohost app setting jappix language -v $language sudo sed -i "s@PATHTOCHANGE@$path@g" ../conf/nginx.conf @@ -45,6 +47,7 @@ sudo sed -i "s@PATHTOCHANGE@$path@g" $final_path/store/conf/main.xml sudo sed -i "s@PATHTOCHANGE@$path@g" $final_path/store/conf/hosts.xml sudo sed -i "s@DOMAINTOCHANGE@$domain@g" $final_path/store/conf/main.xml sudo sed -i "s@CHANGELANG@$language@g" $final_path/store/conf/main.xml +sudo sed -i "s@CHANGENAME@$name@g" $final_path/store/conf/main.xml sudo sed -i "s@DOMAINTOCHANGE@$domain@g" $final_path/store/conf/hosts.xml # Reload Nginx and regenerate SSOwat conf diff --git a/scripts/remove b/scripts/remove index c1ef14a..acd702c 100644 --- a/scripts/remove +++ b/scripts/remove @@ -3,3 +3,11 @@ domain=$(sudo yunohost app setting jappix domain) sudo rm -rf /var/www/jappix sudo rm -f /etc/nginx/conf.d/$domain.d/jappix.conf + +sudo yunohost app setting jappix domain -d +sudo yunohost app setting jappix path -d +sudo yunohost app setting jappix name -d +sudo yunohost app setting jappix language -d + +sudo service nginx reload +sudo yunohost app ssowatconf diff --git a/scripts/restore b/scripts/restore new file mode 100644 index 0000000..c5ff657 --- /dev/null +++ b/scripts/restore @@ -0,0 +1,16 @@ +#!/bin/bash +app=jappix + +# The parameter $1 is the uncompressed restore directory location +backup_dir=$1/apps/$app + +# Restore sources & data +sudo cp -a $backup_dir/sources/. /var/www/$app + +# Restore Nginx and YunoHost parameters +sudo cp -a $backup_dir/yunohost/. /etc/yunohost/apps/$app +domain=$(sudo yunohost app setting $app domain) +sudo cp -a $backup_dir/nginx.conf /etc/nginx/conf.d/$domain.d/$app.conf + +# Restart webserver +sudo service nginx reload \ No newline at end of file diff --git a/scripts/upgrade b/scripts/upgrade index 20c569d..6d2ec50 100644 --- a/scripts/upgrade +++ b/scripts/upgrade @@ -3,8 +3,13 @@ # Retrieve arguments domain=$(sudo yunohost app setting jappix domain) path=$(sudo yunohost app setting jappix path) +name=$(sudo yunohost app setting jappix name) language=$(sudo yunohost app setting jappix language) +if [[ "$name" = "" ]]; +then + name="YunoJappix" +fi if [[ "$language" = "" ]]; then language="en" @@ -37,6 +42,7 @@ sudo sed -i "s@PATHTOCHANGE@$path@g" $final_path/store/conf/main.xml sudo sed -i "s@PATHTOCHANGE@$path@g" $final_path/store/conf/hosts.xml sudo sed -i "s@DOMAINTOCHANGE@$domain@g" $final_path/store/conf/main.xml sudo sed -i "s@CHANGELANG@$language@g" $final_path/store/conf/main.xml +sudo sed -i "s@CHANGENAME@$name@g" $final_path/store/conf/main.xml sudo sed -i "s@DOMAINTOCHANGE@$domain@g" $final_path/store/conf/hosts.xml # Reload Nginx and regenerate SSOwat conf diff --git a/source/CHANGELOG.md b/source/CHANGELOG.md index ec18f65..bbe9a13 100644 --- a/source/CHANGELOG.md +++ b/source/CHANGELOG.md @@ -4,6 +4,71 @@ Jappix Changelog Here's the log of what has changed over the Jappix releases. +Primo, v1.1.2 (October 2014) +---------------------------- + + * XEP-0353: Jingle Message Initiation @valeriansaliou + * Fixes Jingle calls in Chrome 38+ @valeriansaliou + + +Primo, v1.1.1 (September 2014) +------------------------------ + + * Ignore empty XHTML-IM messages @eijebong, @valeriansaliou + * Fix a bug with message markers @valeriansaliou + + +Primo, v1.1.0 (June 2014) +------------------------- + + * XEP-0272: Multiparty Jingle (Muji) @valeriansaliou + * Prevent client crash on huge messages @valeriansaliou + * Beautified client code (JavaScript) @valeriansaliou + * Fix unavailable MUC rooms @emamirazavi + + +One, v1.0.7 (May 2014) +---------------------- + + * Fix BackLinks design @valeriansaliou + * Sort Jappix Mobile contacts alphabetically @valeriansaliou + * Display offline contacts in Jappix Mini @valeriansaliou + + +One, v1.0.6 (May 2014) +---------------------- + + * XEP-0308: Last Message Correction @valeriansaliou + * XEP-0333: Chat Markers @valeriansaliou + * XEP-0319: Last User Interaction into Presence @valeriansaliou + * XEP-0224: Attention @valeriansaliou + * XEP-0152: Reachability Addresses @valeriansaliou + * XEP-0334: Message Processing Hints @valeriansaliou + * Fix gateway contacts management @valeriansaliou + * Fix sounds in Jappix Mini @aryo, @valeriansaliou + + +One, v1.0.5 (May 2014) +---------------------- + + * Fix MUC bookmark shortcut button @valeriansaliou + * Fix HTML5 notifications in Firefox 22+ @valeriansaliou + * Fix server commands tool @valeriansaliou + * New translations added (Uzbek), and a few ones updated @nurkamol, @valeriansaliou + + +One, v1.0.4 (May 2014) +---------------------- + + * Fix update tool (on some environments) @valeriansaliou + * Fix MUC room join @maranda, @valeriansaliou + * Fix special chars in JIDs for Jappix Mini @dunger, @valeriansaliou + * Fix WebSocket session termination in JSJaC @sstrigler + * Enhance backend security (verify SSL certificates) @valeriansaliou + * Add assets client cache option @valeriansaliou + * Add SSO support to Jappix Mobile @valeriansaliou + + One, v1.0.3 (March 2014) ------------------------ diff --git a/source/PROTOCOL.md b/source/PROTOCOL.md index bced59f..846dfb1 100644 --- a/source/PROTOCOL.md +++ b/source/PROTOCOL.md @@ -11,56 +11,76 @@ Here are listed the XMPP Protocol Extensions that Jappix supports, as well as th * RFC-6122: Extensible Messaging and Presence Protocol (XMPP): Address Format -# XMPP Extensions +# XMPP Extensions (Standardized) - * XEP-0045: Multi-User Chat *v1.25* + * XEP-0004: Data Forms *v2.9* + * XEP-0012: Last Activity *v2.0* + * XEP-0016: Privacy Lists *v1.6* * XEP-0030: Service Discovery *v2.4* + * XEP-0045: Multi-User Chat *v1.25* + * XEP-0049: Private XML Storage *v1.2* + * XEP-0050: Ad-Hoc Commands *v1.2* + * XEP-0054: vcard-temp *v1.2* + * XEP-0055: Jabber Search *v1.3* * XEP-0060: Publish-Subscribe *v1.13* - * XEP-0124: Bidirectional-streams Over Synchronous HTTP (BOSH) *v1.10* - * XEP-0115: Entity Capabilities *v1.5* + * XEP-0066: Out of Band Data *v1.5* + * XEP-0071: XHTML-IM *v1.5* + * XEP-0072: SOAP Over XMPP *v1.0* + * XEP-0077: In-Band Registration *v2.4* + * XEP-0080: User Location *v1.7* + * XEP-0084: User Avatar *v1.1* + * XEP-0085: Chat State Notifications *v2.1* + * XEP-0092: Software Version *v1.1* * XEP-0107: User Mood *v1.2* * XEP-0108: User Activity *v1.3* + * XEP-0115: Entity Capabilities *v1.5* * XEP-0118: User Tune *v1.2* - * XEP-0080: User Location *v1.7* - * XEP-0172: User Nickname *v1.1* - * XEP-0084: User Avatar *v1.1* - * XEP-0277: Microblogging over XMPP *v0.6* - * XEP-xxxx: Notification Inbox *v0.1* - * Alternate URL: http://xmpp.org/extensions/inbox/notification-inbox.html - * XEP-0203: Delayed Delivery *v2.0* + * XEP-0124: Bidirectional-streams Over Synchronous HTTP (BOSH) *v1.10* * XEP-0144: Roster Item Exchange *v1.0* - * XEP-0072: SOAP Over XMPP *v1.0* - * XEP-0085: Chat State Notifications *v2.1* - * XEP-0071: XHTML-IM *v1.5* - * XEP-0313: Message Archive Management *v0.3* - * Alternate URL: https://demo.frenchtouch.pro/valerian.saliou/xmpp/extensions/xep-0313.html - * XEP-0012: Last Activity *v2.0* - * XEP-0049: Private XML Storage *v1.2* - * XEP-0077: In-Band Registration *v2.4* - * XEP-0055: Jabber Search *v1.3* - * XEP-0050: Ad-Hoc Commands *v1.2* - * XEP-0092: Software Version *v1.1* - * XEP-0004: Data Forms *v2.9* - * XEP-0054: vcard-temp *v1.2* - * XEP-0202: Entity Time *v2.0* - * XEP-0199: XMPP Ping *v2.0* - * XEP-0184: Message Delivery Receipts *v1.2* - * XEP-0016: Privacy Lists *v1.6* - * XEP-0066: Out of Band Data *v1.5* - * XEP-0280: Message Carbons *v0.9* - * XEP-0292: vCard4 Over XMPP *v0.10* + * XEP-0152: Reachability Addresses *v1.0* * XEP-0166: Jingle *v1.1* * XEP-0167: Jingle RTP Sessions *v1.1* + * XEP-0172: User Nickname *v1.1* * XEP-0176: Jingle ICE-UDP Transport Method *v1.0* + * XEP-0177: Jingle Raw UDP Transport Method *v1.1* + * XEP-0184: Message Delivery Receipts *v1.2* + * XEP-0199: XMPP Ping *v2.0* + * XEP-0202: Entity Time *v2.0* + * XEP-0203: Delayed Delivery *v2.0* + * XEP-0224: Attention *v1.0* * XEP-0215: External Service Discovery *v0.5* + * XEP-0249: Direct MUC Invitations *v1.2* * XEP-0262: Use of ZRTP in Jingle RTP Sessions *v1.0* * XEP-0266: Codecs for Jingle Audio *v1.0* * XEP-0269: Jingle Early Media *v0.1* + * XEP-0277: Microblogging over XMPP *v0.6* + * XEP-0278: Jingle Relay Nodes *v0.2* + * XEP-0280: Message Carbons *v0.9* + * XEP-0292: vCard4 Over XMPP *v0.10* * XEP-0293: Jingle RTP Feedback Negotiation *v0.1* * XEP-0294: Jingle RTP Header Extensions Negotiation *v0.1* * XEP-0299: Codecs for Jingle Video *v0.1* + * XEP-0308: Last Message Correction *v1.0* + * XEP-0319: Last User Interaction in Presence *v0.2* * XEP-0320: Use of DTLS-SRTP in Jingle Sessions *v0.2* + * XEP-0333: Chat Markers *v0.2* + * XEP-0334: Message Processing Hints *v0.1* * XEP-0338: Jingle Grouping Framework *v0.1* + * XEP-0339: Source-Specific Media Attributes in Jingle *v0.1* + * XEP-0353: Jingle Message Initiation *v0.1* + + +# XMPP Extensions (Updated) + + * XEP-0272: Multiparty Jingle (Muji) *v0.2* + * Alternate URL: https://demo.hakuma.holdings/valerian.saliou/xmpp/extensions/xep-0272.html + * XEP-0313: Message Archive Management *v0.3* + * Alternate URL: https://demo.hakuma.holdings/valerian.saliou/xmpp/extensions/xep-0313.html + + +# XMPP Extensions (Proposed) + * XEP-xxxx: Notification Inbox *v0.1* + * Alternate URL: http://xmpp.org/extensions/inbox/notification-inbox.html # Others diff --git a/source/README.md b/source/README.md index 3741682..8413ee0 100644 --- a/source/README.md +++ b/source/README.md @@ -6,7 +6,7 @@ Jappix is a fresh new open social platform which enables you to create your own You can build your own Jappix installation for your own requirements: if you want to use it as a personal social client, you can download it and put it on your webserver. It's easy, fast and free. -[![build status](https://ci.frenchtouch.pro/projects/7/status.png?ref=master)](https://ci.frenchtouch.pro/projects/7?ref=master) +[![build status](https://ci.hakuma.holdings/projects/7/status.png?ref=master)](https://ci.hakuma.holdings/projects/7?ref=master) License @@ -32,9 +32,9 @@ Start translating on https://www.transifex.com/projects/p/jappix/ (new translato Links ----- -* Jappix project website: http://jappix.org/ +* Jappix project website: https://jappix.org/ * Jappix project dev panel: https://github.com/jappix/jappix -* Jappix nodes list: http://jappix.net/ +* Jappix nodes list: https://jappix.net/ * Jappix main service: https://jappix.com/ * Jappix commercial support: https://jappix.pro/ @@ -45,7 +45,7 @@ Mirrors In case a master service is down (GitHub for Git access or Jappix.org for project download), here is a list of available mirrors: * Project website mirror: https://project.jappix.com/ -* Development repository mirror: https://code.frenchtouch.pro/jappix/jappix +* Development repository mirror: https://code.hakuma.holdings/jappix/jappix MUC Links diff --git a/source/VERSION b/source/VERSION index 9ac78f1..3793834 100644 --- a/source/VERSION +++ b/source/VERSION @@ -1 +1 @@ -One [1.0.3] \ No newline at end of file +Primo [1.1.2] diff --git a/source/app/bundles/desktop.xml b/source/app/bundles/desktop.xml index fce814b..4042d38 100644 --- a/source/app/bundles/desktop.xml +++ b/source/app/bundles/desktop.xml @@ -1,5 +1,5 @@ - fonts.css~main.css~images.css~board.css~home.css~others.css~tools.css~roster.css~myinfos.css~pageengine.css~channel.css~pageswitch.css~smileys.css~popup.css~vcard.css~options.css~favorites.css~discovery.css~directory.css~adhoc.css~privacy.css~inbox.css~mucadmin.css~integratebox.css~userinfos.css~search.css~welcome.css~me.css~rosterx.css~jingle.css - origin.js~jxhr.js~datejs.js~jquery.js~jquery.ui.js~jquery.json.js~jquery.form.js~jquery.timers.js~jquery.placeholder.js~jquery.textrange.js~base64.js~jsjac.js~jsjac.jingle.js~system.js~constants.js~datastore.js~browser-detect.js~home.js~talk.js~popup.js~audio.js~board.js~bubble.js~chat.js~groupchat.js~smileys.js~oob.js~avatar.js~mucadmin.js~connection.js~dataform.js~discovery.js~directory.js~adhoc.js~privacy.js~errors.js~name.js~favorites.js~features.js~interface.js~xmpplinks.js~iq.js~message.js~chatstate.js~receipts.js~tooltip.js~filter.js~links.js~inbox.js~microblog.js~music.js~notification.js~httpreply.js~options.js~integratebox.js~pubsub.js~pep.js~presence.js~roster.js~jingle.js~storage.js~console.js~common.js~utilities.js~date.js~caps.js~vcard.js~userinfos.js~search.js~autocompletion.js~welcome.js~me.js~rosterx.js~mam.js~carbons.js + fonts.css~main.css~images.css~board.css~home.css~others.css~tools.css~roster.css~myinfos.css~pageengine.css~channel.css~pageswitch.css~smileys.css~popup.css~vcard.css~options.css~favorites.css~discovery.css~directory.css~adhoc.css~privacy.css~inbox.css~mucadmin.css~integratebox.css~userinfos.css~search.css~welcome.css~me.css~rosterx.css~call.css~jingle.css~muji.css + origin.js~jxhr.js~datejs.js~jquery.js~jquery.ui.js~jquery.json.js~jquery.form.js~jquery.timers.js~jquery.placeholder.js~jquery.textrange.js~jquery.scrollto.js~base64.js~jsjac.js~jsjac.jingle.js~system.js~constants.js~datastore.js~browser-detect.js~home.js~talk.js~popup.js~audio.js~board.js~bubble.js~chat.js~groupchat.js~smileys.js~oob.js~avatar.js~mucadmin.js~connection.js~dataform.js~discovery.js~directory.js~adhoc.js~privacy.js~errors.js~name.js~favorites.js~features.js~interface.js~xmpplinks.js~iq.js~message.js~chatstate.js~receipts.js~tooltip.js~filter.js~links.js~inbox.js~microblog.js~music.js~notification.js~httpreply.js~options.js~integratebox.js~pubsub.js~pep.js~presence.js~roster.js~call.js~jingle.js~muji.js~storage.js~console.js~common.js~utilities.js~date.js~caps.js~vcard.js~userinfos.js~search.js~autocompletion.js~welcome.js~me.js~rosterx.js~mam.js~carbons.js~correction.js~markers.js~attention.js diff --git a/source/app/images/placeholders/jingle_audio_local.png b/source/app/images/placeholders/jingle_audio_local.png new file mode 100644 index 0000000..582f507 Binary files /dev/null and b/source/app/images/placeholders/jingle_audio_local.png differ diff --git a/source/app/images/placeholders/jingle_audio_remote.png b/source/app/images/placeholders/jingle_audio_remote.png new file mode 100644 index 0000000..187b9db Binary files /dev/null and b/source/app/images/placeholders/jingle_audio_remote.png differ diff --git a/source/app/images/placeholders/jingle_video_local.png b/source/app/images/placeholders/jingle_video_local.png index f32d01b..79ddff5 100644 Binary files a/source/app/images/placeholders/jingle_video_local.png and b/source/app/images/placeholders/jingle_video_local.png differ diff --git a/source/app/images/placeholders/jingle_video_remote.png b/source/app/images/placeholders/jingle_video_remote.png new file mode 100644 index 0000000..476138f Binary files /dev/null and b/source/app/images/placeholders/jingle_video_remote.png differ diff --git a/source/app/images/sprites/call.png b/source/app/images/sprites/call.png new file mode 100644 index 0000000..5b332dc Binary files /dev/null and b/source/app/images/sprites/call.png differ diff --git a/source/app/images/sprites/home.png b/source/app/images/sprites/home.png index afe6304..5b5dd00 100644 Binary files a/source/app/images/sprites/home.png and b/source/app/images/sprites/home.png differ diff --git a/source/app/images/sprites/jingle.png b/source/app/images/sprites/jingle.png deleted file mode 100644 index d97aba0..0000000 Binary files a/source/app/images/sprites/jingle.png and /dev/null differ diff --git a/source/app/images/sprites/talk.png b/source/app/images/sprites/talk.png index 80a2e04..ce2ea9d 100644 Binary files a/source/app/images/sprites/talk.png and b/source/app/images/sprites/talk.png differ diff --git a/source/app/images/sprites/welcome.png b/source/app/images/sprites/welcome.png index 61b72b4..e11565c 100644 Binary files a/source/app/images/sprites/welcome.png and b/source/app/images/sprites/welcome.png differ diff --git a/source/app/javascripts/adhoc.js b/source/app/javascripts/adhoc.js index 125a369..e4e898b 100644 --- a/source/app/javascripts/adhoc.js +++ b/source/app/javascripts/adhoc.js @@ -29,24 +29,24 @@ var AdHoc = (function () { try { // Popup HTML content - var html = - '
' + Common._e("Commands") + '
' + - - '
' + - '
' + - - '
' + - '
' + - - '
' + - '
' + - - '' + Common._e("Close") + '' + + var html = + '
' + Common._e("Commands") + '
' + + + '
' + + '
' + + + '
' + + '
' + + + '
' + + '
' + + + '' + Common._e("Close") + '' + '
'; - + // Create the popup Popup.create('adhoc', html); - + // Associate the events self.launch(); } catch(e) { @@ -88,16 +88,16 @@ var AdHoc = (function () { try { // Open the popup self.open(); - + // Add a XID marker $('#adhoc .adhoc-head').html('' + Name.getBuddy(xid).htmlEnc() + ' (' + xid.htmlEnc() + ')'); - + // Get the highest entity resource var highest = Presence.highestPriority(xid); - + if(highest) xid = highest; - + // Start a new adhoc command DataForm.go(xid, 'command', '', '', 'adhoc'); } catch(e) { @@ -120,10 +120,10 @@ var AdHoc = (function () { try { // Open the popup self.open(); - + // Add a XID marker $('#adhoc .adhoc-head').html('' + server.htmlEnc() + ''); - + // Start a new adhoc command DataForm.go(server, 'command', '', '', 'adhoc'); } catch(e) { @@ -142,9 +142,7 @@ var AdHoc = (function () { try { // Click event - $('#adhoc .bottom .finish').click( - self.close() - ); + $('#adhoc .bottom .finish').click(self.close); } catch(e) { Console.error('AdHoc.launch', e); } diff --git a/source/app/javascripts/anonymous.js b/source/app/javascripts/anonymous.js index dac4971..f38f819 100644 --- a/source/app/javascripts/anonymous.js +++ b/source/app/javascripts/anonymous.js @@ -20,6 +20,28 @@ var Anonymous = (function () { var self = {}; + /** + * Registers connection handlers + * @private + * @param {object} con + * @return {undefined} + */ + self._registerHandlers = function(con) { + + try { + con.registerHandler('message', Message.handle); + con.registerHandler('presence', Presence.handle); + con.registerHandler('iq', IQ.handle); + con.registerHandler('onconnect', self.connected); + con.registerHandler('onerror', Errors.handle); + con.registerHandler('ondisconnect', self.disconnected); + } catch(e) { + Console.error('Anonymous._registerHandlers', e); + } + + }; + + /** * Connected to an anonymous session * @public @@ -29,40 +51,40 @@ var Anonymous = (function () { try { Console.info('Jappix (anonymous) is now connected.'); - + // Connected marker Connection.connected = true; Connection.current_session = true; Connection.reconnect_try = 0; Connection.reconnect_timer = 0; - + // Not resumed? if(!Connection.resume) { // Create the app Talk.create(); - + // Send our first presence Presence.sendFirst(''); - + // Set last activity stamp DateUtils.last_activity = DateUtils.getTimeStamp(); - + // Create the new groupchat Chat.checkCreate(Common.generateXID(ANONYMOUS_ROOM, 'groupchat'), 'groupchat'); - + // Remove some nasty elements for the anonymous mode $('.tools-mucadmin, .tools-add').remove(); } - + // Resumed else { // Send again our presence Presence.sendActions(); - + // Change the title Interface.updateTitle(); } - + // Remove the waiting icon Interface.removeGeneralWait(); } catch(e) { @@ -107,21 +129,16 @@ var Anonymous = (function () { // Check BOSH origin BOSH_SAME_ORIGIN = Origin.isSame(httpbase); - + // We create the new http-binding connection con = new JSJaCHttpBindingConnection({ httpbase: httpbase }); } - + // And we handle everything that happen - con.registerHandler('message', Message.handle); - con.registerHandler('presence', Presence.handle); - con.registerHandler('iq', IQ.handle); - con.registerHandler('onconnect', self.connected); - con.registerHandler('onerror', Errors.handle); - con.registerHandler('ondisconnect', self.disconnected); - + self._registerHandlers(con); + // We set the anonymous connection parameters oArgs = {}; oArgs.domain = server; @@ -129,10 +146,10 @@ var Anonymous = (function () { oArgs.resource = JAPPIX_RESOURCE + ' Anonymous (' + (new Date()).getTime() + ')'; oArgs.secure = true; oArgs.xmllang = XML_LANG; - + // We connect ! con.connect(oArgs); - + // Change the page title Interface.title('wait'); } catch(e) { @@ -140,7 +157,7 @@ var Anonymous = (function () { // Reset Jappix self.disconnected(); - + // Open an unknown error Board.openThisError(2); } finally { @@ -160,16 +177,19 @@ var Anonymous = (function () { try { $(document).ready(function() { Console.info('Anonymous mode detected, connecting...'); - + // We add the login wait div Interface.showGeneralWait(); - + // Get the vars - if(XMPPLinks.links_var.r) + if(XMPPLinks.links_var.r) { ANONYMOUS_ROOM = XMPPLinks.links_var.r; - if(XMPPLinks.links_var.n) + } + + if(XMPPLinks.links_var.n) { ANONYMOUS_NICK = XMPPLinks.links_var.n; - + } + // Fire the login action self.login(HOST_ANONYMOUS); }); diff --git a/source/app/javascripts/attention.js b/source/app/javascripts/attention.js new file mode 100644 index 0000000..abcd44b --- /dev/null +++ b/source/app/javascripts/attention.js @@ -0,0 +1,216 @@ +/* + +Jappix - An open social platform +Implementation of XEP-0224: Attention + +------------------------------------------------- + +License: AGPL +Author: Valérian Saliou + +*/ + +// Bundle +var Attention = (function () { + + /** + * Alias of this + * @private + */ + var self = {}; + + + /** + * Displays attention message + * @private + * @param {string} xid + * @param {string} body + * @return {undefined} + */ + self._display = function(xid, body, mode) { + + try { + var name = Name.getBuddy(xid).htmlEnc(); + var hash = hex_md5(xid); + + // Compute some variables + var message = Common._e(Common.printf("You requested %s's attention to the conversation", name)); + + if(mode == 'him') { + message = Common._e(Common.printf("%s requested your attention to the conversation", name)); + } + + if(body) { + message += ' (' + body + ')'; + } + + // Display notification + Message.display( + 'chat', + xid, + hash, + name, + message, + DateUtils.getCompleteTime(), + DateUtils.getTimeStamp(), + 'system-message', + true, + undefined, + mode + ); + + // Add a marker to displayed message + $('#' + hash + ' .content .one-line.system-message:last').addClass('attention-notice'); + } catch(e) { + Console.error('Attention._display', e); + } + + }; + + + /** + * Sends attention stanza + * @private + * @param {string} xid + * @param {string} body + * @return {object} + */ + self._stanza = function(xid, body) { + + try { + var message = new JSJaCMessage(); + message.setType('headline'); + message.setTo(xid); + + if(body) { + message.setBody(body); + } + + // Attention node + message.appendNode('attention', { + 'xmlns': NS_URN_ATTENTION + }); + + con.send(message); + + return message; + } catch(e) { + Console.error('Attention._stanza', e); + } + + }; + + + /** + * Returns whether last attention message exists or not + * @private + * @param {string} xid + * @return {boolean} + */ + self._lastExists = function(xid, mode) { + + var last_exists = false; + + try { + var line_sel = $('#' + hex_md5(xid) + ' .content .one-line[data-mode="' + mode + '"]:last'); + last_exists = line_sel.is('.system-message.attention-notice') ? true : false; + } catch(e) { + Console.error('Attention._lastExists', e); + } finally { + return last_exists; + } + + }; + + + /** + * Return whether entity supports attention notifications + * @public + * @param {string} xid + * @return {boolean} + */ + self.hasSupport = function(xid) { + + var has_support = false; + + try { + has_support = true ? $('#' + hex_md5(xid)).attr('data-attention') == 'true' : false; + } catch(e) { + Console.error('Attention.hasSupport', e); + } finally { + return has_support; + } + + }; + + + /** + * Send an attention message + * @public + * @param {string} xid + * @param {string} body + * @return {undefined} + */ + self.send = function(xid, body) { + + try { + var mode = 'me'; + + // Don't send attention message twice + if(self._lastExists(xid, mode) === false) { + // Send message stanza + self._stanza(xid, body); + + // Display attention notification + self._display(xid, body, mode); + } else { + Console.debug('Attention.send', 'Not sending attention message to: ' + xid + ' because already sent.'); + } + } catch(e) { + Console.error('Attention.send', e); + } + + }; + + + /** + * Receive an attention notification + * @public + * @param {string} xid + * @return {undefined} + */ + self.receive = function(xid, body) { + + try { + var mode = 'him'; + var hash = hex_md5(xid); + + // Don't receive attention message twice + if((self._lastExists(xid, mode) === false) && Common.exists('#' + hash)) { + // Display attention notification + self._display(xid, body, mode); + + // Show a notification + Interface.messageNotify(hash, 'personal'); + Audio.play('catch-attention'); + + Board.quick( + xid, + 'chat', + Common._e("Attention to conversation requested."), + Name.getBuddy(xid) + ); + } + } catch(e) { + Console.error('Attention.receive', e); + } + + }; + + + /** + * Return class scope + */ + return self; + +})(); \ No newline at end of file diff --git a/source/app/javascripts/audio.js b/source/app/javascripts/audio.js index cc1fe51..62652ea 100644 --- a/source/app/javascripts/audio.js +++ b/source/app/javascripts/audio.js @@ -29,7 +29,7 @@ var Audio = (function () { * @private * @return {boolean} */ - self._is_supported = function() { + self._isSupported = function() { is_supported = true; @@ -38,7 +38,7 @@ var Audio = (function () { is_supported = false; } } catch(e) { - Console.error('Audio._is_supported', e); + Console.error('Audio._isSupported', e); } finally { return is_supported; } @@ -46,6 +46,57 @@ var Audio = (function () { }; + /** + * Append audio DOM code + * @private + * @return {undefined} + */ + self._appendDOM = function() { + + try { + // If the audio elements aren't yet in the DOM + if(!Common.exists('#audio')) { + $('body').append( + '
' + + '' + + + '' + + + '' + + + '' + + + '' + + + '' + + '
' + ); + } + } catch(e) { + Console.error('Audio._appendDOM', e); + } + + }; + + /** * Plays the given sound ID * @public @@ -58,44 +109,14 @@ var Audio = (function () { repeat = (typeof repeat === 'boolean') ? repeat : false; // Not supported? - if(!self._is_supported()) { + if(!self._isSupported()) { return false; } - + // If the sounds are enabled if(DataStore.getDB(Connection.desktop_hash, 'options', 'sounds') === '1') { - // If the audio elements aren't yet in the DOM - if(!Common.exists('#audio')) { - $('body').append( - '
' + - '' + - - '' + - - '' + - - '' + - - '' + - '
' - ); - } - + self._appendDOM(); + // We play the target sound var audio_raw_sel = $('#audio audio').filter('#' + name); var audio_sel = audio_raw_sel[0]; @@ -112,7 +133,7 @@ var Audio = (function () { var duration = parseInt((audio_raw_sel.attr('data-duration') || 0), 10); self._timeout_stop = false; - + audio_raw_sel.oneTime((duration + 's'), function() { if(!self._timeout_stop) { self.play(name, repeat); @@ -146,10 +167,10 @@ var Audio = (function () { try { // Not supported? - if(!self._is_supported()) { + if(!self._isSupported()) { return false; } - + self._timeout_stop = true; // Check the audio container exists before doing anything... @@ -159,7 +180,7 @@ var Audio = (function () { if(audio_parent_sel.size()) { audio_raw_sel.stopTime(); - + if(audio_sel) { if(!audio_sel.paused) { audio_sel.pause(); diff --git a/source/app/javascripts/autocompletion.js b/source/app/javascripts/autocompletion.js index 1b28e02..78f48b5 100644 --- a/source/app/javascripts/autocompletion.js +++ b/source/app/javascripts/autocompletion.js @@ -32,14 +32,17 @@ var Autocompletion = (function () { try { // Put the two strings into lower case - var sA = a[0].toLowerCase(); - var sB = b[0].toLowerCase(); - + var sort_a = a[0].toLowerCase(); + var sort_b = b[0].toLowerCase(); + // Process the sort - if(sA > sB) + if(sort_a > sort_b) { return 1; - if(sA < sB) + } + + if(sort_a < sort_b) { return -1; + } } catch(e) { Console.error('Autocompletion.caseInsensitiveSort', e); } @@ -49,40 +52,51 @@ var Autocompletion = (function () { /** * Split a query into its subqueries ready to be used in autocompletion - * The function return an array containing two others : the first with subqueries - * and the second with remaining parts - * For example, if query is "A B C", the subqueries are ["C", "B C", "A B C"] and - * the remaining parts are ["A B ", "A ", ""] + * @public * @param {string} query - * @return {Array} + * @return {object} */ self.getSubQueries = function(query) { - var subqueries = []; - var remnants = []; + var result = []; - var queryLastCharPos = query.length - 1; - var spaceCounter = 0; - for (var i=queryLastCharPos; i>=0; i--) { - // Search from the end of the query - var iChar = query.charAt(i); - if (spaceCounter === 0 && iChar.search(/\s/) === 0) { - // the first "local" space was found - // add the subquery and its remnant to results - subqueries.push(query.slice(i+1)); - remnants.push(query.slice(0, i+1)); - spaceCounter++; - } else { - spaceCounter = 0; + try { + var subqueries = []; + var remnants = []; + + var query_last_char_pos = query.length - 1; + var space_counter = 0; + var cur_char; + + for(var i = query_last_char_pos; i >= 0; i--) { + // Search from the end of the query + cur_char = query.charAt(i); + + if(space_counter === 0 && cur_char.search(/\s/) === 0) { + // The first "local" space was found + // Add the subquery and its remnant to results + subqueries.push(query.slice(i+1)); + remnants.push(query.slice(0, i+1)); + + space_counter++; + } else { + space_counter = 0; + } } - } - if (spaceCounter === 0) { - // If the first char of the query is not a space, add the full query to results - subqueries.push(query); - remnants.push(""); + + if(space_counter === 0) { + // If the first char of the query is not a space, add the full query to results + subqueries.push(query); + remnants.push(''); + } + + result = [subqueries, remnants]; + } catch(e) { + Console.error('Autocompletion.getSubQueries', e); + } finally { + return result; } - return [subqueries, remnants]; }; @@ -102,25 +116,37 @@ var Autocompletion = (function () { try { // Replace forbidden characters in regex query = Common.escapeRegex(query); + // Build an array of regex to use - var queryRegExp = []; - for (i = 0; i' + aType + '' + aBinval + '' + aChecksum + 'false'); - + Console.info('Avatar retrieved from server: ' + handleFrom); } - + // vCard is empty else { self.reset(handleFrom); } - + // We got a new checksum for us? if(((oChecksum !== null) && (oChecksum != aChecksum)) || !Presence.first_sent) { // Define a proper checksum var pChecksum = aChecksum; - - if(pChecksum == 'none') + + if(pChecksum == 'none') { pChecksum = ''; - + } + // Update our temp. checksum DataStore.setDB(Connection.desktop_hash, 'checksum', 1, pChecksum); - + // Send the stanza - if(!Presence.first_sent) + if(!Presence.first_sent) { Storage.get(NS_OPTIONS); - else if(DataStore.hasPersistent()) + } else if(DataStore.hasPersistent()) { Presence.sendActions(pChecksum); + } } } catch(e) { Console.error('Avatar.handle', e); @@ -228,7 +235,7 @@ var Avatar = (function () { try { // Store the empty avatar DataStore.setPersistent('global', 'avatar', xid, 'nonenonenonefalse'); - + // Display the empty avatar self.display(xid, hash, 'none', 'none'); } catch(e) { @@ -253,18 +260,19 @@ var Avatar = (function () { // Initialize the vars var container = hash + ' .avatar-container'; var code = ''; - + // Replace with the new avatar (in the roster and in the chat) $('.' + container).html(code); - + // We can remove the pending marker Utils.removeArrayValue(self.pending, xid); } catch(e) { diff --git a/source/app/javascripts/base64.js b/source/app/javascripts/base64.js index 6e5366e..1ef3528 100644 --- a/source/app/javascripts/base64.js +++ b/source/app/javascripts/base64.js @@ -17,12 +17,12 @@ var Base64 = (function () { var chr1, chr2, chr3; var enc1, enc2, enc3, enc4; var i = 0; - + do { chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); - + enc1 = chr1 >> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); @@ -33,14 +33,14 @@ var Base64 = (function () { } else if (isNaN(chr3)) { enc4 = 64; } - + output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4); } while (i < input.length); - + return output; }, - + /** * Decodes a base64 string. * @param {String} input The string to decode. @@ -50,22 +50,22 @@ var Base64 = (function () { var chr1, chr2, chr3; var enc1, enc2, enc3, enc4; var i = 0; - + // remove all characters that are not A-Z, a-z, 0-9, +, /, or = input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); - + do { enc1 = keyStr.indexOf(input.charAt(i++)); enc2 = keyStr.indexOf(input.charAt(i++)); enc3 = keyStr.indexOf(input.charAt(i++)); enc4 = keyStr.indexOf(input.charAt(i++)); - + chr1 = (enc1 << 2) | (enc2 >> 4); chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4; - + output = output + String.fromCharCode(chr1); - + if (enc3 != 64) { output = output + String.fromCharCode(chr2); } @@ -73,7 +73,7 @@ var Base64 = (function () { output = output + String.fromCharCode(chr3); } } while (i < input.length); - + return output; } }; diff --git a/source/app/javascripts/board.js b/source/app/javascripts/board.js index 335e243..fd73bd4 100644 --- a/source/app/javascripts/board.js +++ b/source/app/javascripts/board.js @@ -20,6 +20,137 @@ var Board = (function () { var self = {}; + /* Contants */ + self.NOTIFICATION = (window.Notification || window.mozNotification || window.webkitNotification); + + + /** + * Generate board info message + * @private + * @param {string} id + * @return {string} + */ + self._generateBoardInfo = function(id) { + + var text = null; + + try { + switch(id) { + // Password change + case 1: + text = Common._e("Your password has been changed, now you can connect to your account with your new login data."); + + break; + + // Account deletion + case 2: + text = Common._e("Your XMPP account has been removed, bye!"); + + break; + + // Account logout + case 3: + text = Common._e("You have been logged out of your XMPP account, have a nice day!"); + + break; + + // Groupchat join + case 4: + text = Common._e("The room you tried to join doesn't seem to exist."); + + break; + + // Groupchat removal + case 5: + text = Common._e("The groupchat has been removed."); + + break; + + // Non-existant groupchat user + case 6: + text = Common._e("The user that you want to reach is not present in the room."); + + break; + } + } catch(e) { + Console.error('Board._generateBoardInfo', e); + } finally { + return text; + } + + }; + + + /** + * Generate board error message + * @private + * @param {string} id + * @return {string} + */ + self._generateBoardError = function(id) { + + var text = null; + + try { + switch(id) { + // Custom error + case 1: + text = '' + Common._e("Error") + ' » '; + + break; + + // Network error + case 2: + text = Common._e("Jappix has been interrupted by a network issue, a bug or bad login (check that you entered the right credentials), sorry for the inconvenience."); + + break; + + // List retrieving error + case 3: + text = Common._e("The element list on this server could not be obtained!"); + + break; + + // Attaching error + case 4: + text = Common.printf(Common._e("An error occured while uploading your file: maybe it is too big (%s maximum) or forbidden!"), JAPPIX_MAX_UPLOAD); + + break; + } + } catch(e) { + Console.error('Board._generateBoardError', e); + } finally { + return text; + } + + }; + + + /** + * Attaches board events + * @private + * @param {object} board_sel + * @return {undefined} + */ + self._attachEvents = function(board_sel) { + + try { + board_sel.click(function() { + self.closeThis(this); + }); + + board_sel.oneTime('5s', function() { + self.closeThis(this); + }); + + board_sel.slideDown(); + } catch(e) { + Console.error('Board._attachEvents', e); + } + + }; + + /** * Creates a board panel * @public @@ -32,97 +163,29 @@ var Board = (function () { try { // Text var var text = ''; - + // Info if(type == 'info') { - switch(id) { - // Password change - case 1: - text = Common._e("Your password has been changed, now you can connect to your account with your new login data."); - - break; - - // Account deletion - case 2: - text = Common._e("Your XMPP account has been removed, bye!"); - - break; - - // Account logout - case 3: - text = Common._e("You have been logged out of your XMPP account, have a nice day!"); - - break; - - // Groupchat join - case 4: - text = Common._e("The room you tried to join doesn't seem to exist."); - - break; - - // Groupchat removal - case 5: - text = Common._e("The groupchat has been removed."); - - break; - - // Non-existant groupchat user - case 6: - text = Common._e("The user that you want to reach is not present in the room."); - - break; - } + text = self._generateBoardInfo(id); + } else { + text = self._generateBoardError(id); } - - // Error - else { - switch(id) { - // Custom error - case 1: - text = '' + Common._e("Error") + ' » '; - - break; - - // Network error - case 2: - text = Common._e("Jappix has been interrupted by a network issue, a bug or bad login (check that you entered the right credentials), sorry for the inconvenience."); - - break; - - // List retrieving error - case 3: - text = Common._e("The element list on this server could not be obtained!"); - - break; - - // Attaching error - case 4: - text = Common.printf(Common._e("An error occured while uploading your file: maybe it is too big (%s maximum) or forbidden!"), JAPPIX_MAX_UPLOAD); - - break; - } - } - + // No text? - if(!text) + if(!text) { return false; - + } + // Append the content - $('#board').append('
' + text + '
'); - + $('#board').append( + '
' + text + '
' + ); + // Events (click and auto-hide) - $('#board .one-board.' + type + '[data-id="' + id + '"]') - - .click(function() { - self.closeThis(this); - }) - - .oneTime('5s', function() { - self.closeThis(this); - }) - - .slideDown(); - + self._attachEvents( + $('#board .one-board.' + type + '[data-id="' + id + '"]') + ); + return true; } catch(e) { Console.error('Board.create', e); @@ -158,7 +221,7 @@ var Board = (function () { try { // In a first, we destroy other boards self.destroy(); - + // Then we display the board self.create(type, id); } catch(e) { @@ -235,99 +298,74 @@ var Board = (function () { try { // Cannot process? - if(Common.isFocused() || !content || !(window.webkitNotifications || window.Notification)) { + if(Common.isFocused() || !content || !self.NOTIFICATION) { return; } - + // Default icon? if(!icon) { icon = './images/others/default-avatar.png'; - + // Avatar icon? if(xid) { var avatar_xml = Common.XMLFromString( DataStore.getPersistent('global', 'avatar', xid) ); + var avatar_type = $(avatar_xml).find('type').text() || 'image/png'; var avatar_binval = $(avatar_xml).find('binval').text(); - - if(avatar_binval && avatar_type) + + if(avatar_binval && avatar_type) { icon = 'data:' + avatar_type + ';base64,' + avatar_binval; + } } } - + // Default title? if(!title) { title = Common._e("New event!"); } - // Click callback - var cb_click_fn = function() { + // Create notification + var notification = new self.NOTIFICATION(title, { + dir: 'auto', + lang: '', + body: content, + tag: type, + icon: icon + }); + + // Click event + notification.onclick = function() { // Click action? switch(type) { case 'chat': Interface.switchChan(hex_md5(xid)); break; - + case 'groupchat': Interface.switchChan(hex_md5(Common.bareXID(xid))); break; - + default: break; } - + // Focus on msg-me window.focus(); - + // Remove notification - this.cancel(); + this.close(); }; - - // Check for notification permission - try { - if(Notification.permission == 'granted' || Notification.permission === undefined) { - var notification = new Notification(title, { - dir: 'auto', - lang: '', - body: content, - tag: type, - icon: icon - }); - notification.onclick = cb_click_fn; + // Show event + notification.onshow = function() { + setTimeout(function() { + notification.close(); + }, 10000); + }; - setTimeout(function() { - notification.close(); - }, 10000); - - if(notification.permission == 'granted') { - return notification; - } - } - } catch(_e) { - if(window.webkitNotifications.checkPermission() === 0) { - // Create notification - var notification = window.webkitNotifications.createNotification(icon, title, content); - - // Auto-hide after a while - notification.ondisplay = function(event) { - setTimeout(function() { - event.currentTarget.cancel(); - }, 10000); - }; - - // Click event - notification.onclick = cb_click_fn; - - // Show notification - notification.show(); - - return notification; - } - } - - return null; + return notification; } catch(e) { Console.error('Board.quick', e); } @@ -343,21 +381,7 @@ var Board = (function () { self.quickPermission = function() { try { - try { - // W3C Notification API (still a draft!) - if(Notification.permission !== 'granted') { - // Ask for permission - Notification.requestPermission(); - } - } catch (_e) { - // WebKit Notification API (fallback) - if(!window.webkitNotifications || (window.webkitNotifications.checkPermission() === 0)) { - return; - } - - // Ask for permission - window.webkitNotifications.requestPermission(); - } + self.NOTIFICATION.requestPermission(); } catch(e) { Console.error('Board.quickPermission', e); } @@ -376,8 +400,9 @@ var Board = (function () { // Fires quickPermission() on document click $(document).click(function() { // Ask for permission to use quick boards - if((typeof con != 'undefined') && con.connected()) + if((typeof con != 'undefined') && con.connected()) { self.quickPermission(); + } }); } catch(e) { Console.error('Board.launch', e); diff --git a/source/app/javascripts/browser-detect.js b/source/app/javascripts/browser-detect.js index 9f83444..cef3c4a 100644 --- a/source/app/javascripts/browser-detect.js +++ b/source/app/javascripts/browser-detect.js @@ -6,12 +6,12 @@ var BrowserDetect = { init: function () { this.browser = this.searchString(this.dataBrowser) || "An unknown browser"; - this.version = this.searchVersion(navigator.userAgent) - || this.searchVersion(navigator.appVersion) - || "an unknown version"; + this.version = this.searchVersion(navigator.userAgent) || + this.searchVersion(navigator.appVersion) || + "an unknown version"; this.OS = this.searchString(this.dataOS) || "an unknown OS"; }, - + searchString: function (data) { for (var i=0;i' + attrs.text + ''; + }); + } + + // Append notification to DOM + call_subitem_sel.html( + '
' + + '
' + + '
' + + '' + + '
' + + + '' + + '
' + + + '
' + + '' + fullname + '' + + '' + map[type].text + '' + + + '
' + buttons_html + '
' + + '
' + + '
' + ); + + // Apply user avatar + Avatar.get(sender_xid, 'cache', 'true', 'forget'); + + // Apply button events + if(typeof map[type].buttons === 'object') { + $.each(map[type].buttons, function(button, attrs) { + call_tools_all_sel.find('a.reply-button[data-action="' + button + '"]').click(function() { + try { + // Remove notification + self._unnotify(); + + // Execute callback, if any + if(typeof attrs.cb === 'function') { + attrs.cb(xid, mode, options_arr); + } + + Console.info('Closed call notification drawer'); + } catch(e) { + Console.error('Call.notify[async]', e); + } finally { + return false; + } + }); + }); + } + + // Enable notification box! + call_tool_sel.addClass('active'); + + // Open notification box! + call_content_sel.show(); + } catch(e) { + Console.error('Call.notify', e); + } finally { + return false; + } + + }; + + + /** + * Remove notification + * @private + * @return {boolean} + */ + self._unnotify = function() { + + try { + // Selectors + var call_tools_all_sel = $('#top-content .tools-all:has(.tools.call)'); + var call_tool_sel = call_tools_all_sel.find('.tools.call'); + var call_content_sel = call_tools_all_sel.find('.call-content'); + var call_subitem_sel = call_content_sel.find('.tools-content-subitem'); + + // Close & disable notification box + call_content_sel.hide(); + call_subitem_sel.empty(); + call_tool_sel.removeClass('active'); + + // Stop all sounds + Audio.stop('incoming-call'); + Audio.stop('outgoing-call'); + } catch(e) { + Console.error('Call._unnotify', e); + } finally { + return false; + } + + }; + + + /** + * Processes the video elements size + * @private + * @param {object} screen + * @param {object} video + * @return {object} + */ + self._process_size = function(screen, video) { + + try { + if(!(typeof screen === 'object' && typeof video === 'object')) { + throw 'Invalid object passed, aborting!'; + } + + // Get the intrinsic size of the video + var video_w = video[0].videoWidth || video.width(); + var video_h = video[0].videoHeight || video.height(); + + // Get the screen size of the video + var screen_w = screen.width(); + var screen_h = screen.height(); + + // Process resize ratios (2 cases) + var r_1 = screen_h / video_h; + var r_2 = screen_w / video_w; + + // Process resized video sizes + var video_w_1 = video_w * r_1; + var video_h_1 = video_h * r_1; + + var video_w_2 = video_w * r_2; + var video_h_2 = video_h * r_2; + + // DOM view modifiers + var dom_width = 'auto'; + var dom_height = 'auto'; + var dom_left = 0; + var dom_top = 0; + + // Landscape/Portrait/Equal container? + if(video_w > video_h || (video_h == video_w && screen_w < screen_h)) { + // Not sufficient? + if(video_w_1 < screen_w) { + dom_width = screen_w + 'px'; + dom_top = -1 * (video_h_2 - screen_h) / 2; + } else { + dom_height = screen_h + 'px'; + dom_left = -1 * (video_w_1 - screen_w) / 2; + } + } else if(video_h > video_w || (video_h == video_w && screen_w > screen_h)) { + // Not sufficient? + if(video_h_1 < screen_h) { + dom_height = screen_h + 'px'; + dom_left = -1 * (video_w_1 - screen_w) / 2; + } else { + dom_width = screen_w + 'px'; + dom_top = -1 * (video_h_2 - screen_h) / 2; + } + } else if(screen_w == screen_h) { + dom_width = screen_w + 'px'; + dom_height = screen_h + 'px'; + } + + return { + width : dom_width, + height : dom_height, + left : dom_left, + top : dom_top + }; + } catch(e) { + Console.error('Call._process_size', e); + } + + }; + + + /** + * Adapts the local video view + * @public + * @param {object} local_sel + * @return {undefined} + */ + self.adapt_local = function(local_sel) { + + try { + var local_video_sel = local_sel.find('video'); + + // Process new sizes + var sizes = Call._process_size( + local_sel, + local_video_sel + ); + + // Apply new sizes + local_video_sel.css({ + 'height': sizes.height, + 'width': sizes.width, + 'margin-top': sizes.top, + 'margin-left': sizes.left + }); + } catch(e) { + Console.error('Call.adapt_local', e); + } + + }; + + + /** + * Adapts the remote video view + * @public + * @param {object} videobox_sel + * @return {undefined} + */ + self.adapt_remote = function(videobox_sel) { + + try { + var remote_video_sel, sizes; + + videobox_sel.find('.remote_video').each(function() { + remote_video_sel = $(this).find('video'); + + if(remote_video_sel.size()) { + // Process new sizes + sizes = Call._process_size( + $(this), + remote_video_sel + ); + + // Apply new sizes + remote_video_sel.css({ + 'height': sizes.height, + 'width': sizes.width, + 'margin-top': sizes.top, + 'margin-left': sizes.left + }); + } + }); + } catch(e) { + Console.error('Call.adapt_remote', e); + } + + }; + + + /** + * Start call elpsed time counter + * @public + * @return {boolean} + */ + self.start_counter = function() { + + try { + // Initialize counter + self.stop_counter(); + self._start_stamp = DateUtils.getTimeStamp(); + self._fire_clock(); + + // Fire it every second + $('#top-content .tools.call .counter').everyTime('1s', self._fire_clock); + + Console.info('Call counter started'); + } catch(e) { + Console.error('Call.start_counter', e); + } finally { + return false; + } + + }; + + + /** + * Stop call elpsed time counter + * @public + * @return {boolean} + */ + self.stop_counter = function() { + + try { + // Reset stamp storage + self._start_stamp = 0; + + // Reset counter + var counter_sel = $('#top-content .tools.call .counter'); + var default_count = counter_sel.attr('data-default'); + + counter_sel.stopTime(); + + $('#top-content .tools.call .counter').text(default_count); + $('#jingle, #muji').find('.elapsed').text(default_count); + + Console.info('Call counter stopped'); + } catch(e) { + Console.error('Call.stop_counter', e); + } finally { + return false; + } + + }; + + + /** + * Fires the counter clock (once more) + * @private + * @return {undefined} + */ + self._fire_clock = function() { + + try { + // Process updated time + var count = DateUtils.difference( + DateUtils.getTimeStamp(), + self._start_stamp + ); + + if(count.getHours()) { + count = count.toString('H:mm:ss'); + } else { + count = count.toString('mm:ss'); + } + + // Display updated counter + $('#top-content .tools.call .counter').text(count); + $('#jingle, #muji').find('.elapsed').text(count); + } catch(e) { + Console.error('Call._fire_clock', e); + } + + }; + + + /** + * Destroy the call interface + * @public + * @return {undefined} + */ + self.destroy_interface = function(container_sel) { + + try { + container_sel.stopTime(); + container_sel.find('*').stopTime(); + + container_sel.remove(); + } catch(e) { + Console.error('Call.destroy_interface', e); + } + + }; + + + /** + * Show the call interface + * @public + * @param {object} manager + * @param {object} call_sel + * @param {object} video_container_sel + * @return {boolean} + */ + self.show_interface = function(manager, call_sel, video_container_sel) { + + try { + if(manager.in_call()) { + call_sel.filter(':hidden').show(); + + // Launch back some events + video_container_sel.mousemove(); + } + } catch(e) { + Console.error('Call.show_interface', e); + } finally { + return false; + } + + }; + + + /** + * Hide the call interface + * @public + * @param {object} call_sel + * @param {object} video_container_sel + * @return {boolean} + */ + self.hide_interface = function(call_sel, video_container_sel) { + + try { + call_sel.filter(':visible').hide(); + + // Reset some events + video_container_sel.find('.topbar').stopTime().hide(); + } catch(e) { + Console.error('Call.hide_interface', e); + } finally { + return false; + } + + }; + + + /** + * Attaches interface events + * @public + * @param {object} manager + * @param {object} call_sel + * @param {object} video_container_sel + * @return {undefined} + */ + self.events_interface = function(manager, call_sel, video_container_sel) { + + try { + call_sel.everyTime(50, function() { + manager._adapt(); + }); + + // Close interface on click on semi-transparent background + call_sel.click(function(evt) { + try { + // Click on lock background? + if($(evt.target).is('.lock')) { + return manager._hide_interface(); + } + } catch(e) { + Console.error('Call.events_interface[async]', e); + } + }); + + // Click on a control or action button + call_sel.find('.topbar').find('.controls a, .actions a').click(function() { + try { + switch($(this).data('type')) { + case 'close': + manager._hide_interface(); break; + case 'stop': + case 'leave': + manager.stop(); break; + case 'mute': + manager.mute(); break; + case 'unmute': + manager.unmute(); break; + } + } catch(e) { + Console.error('Call.events_interface[async]', e); + } finally { + return false; + } + }); + + // Auto Hide/Show interface topbar + video_container_sel.mousemove(function() { + try { + var topbar_sel = $(this).find('.topbar'); + + if(topbar_sel.is(':hidden')) { + topbar_sel.stop(true).fadeIn(250); + } + + topbar_sel.stopTime(); + topbar_sel.oneTime('5s', function() { + topbar_sel.stop(true).fadeOut(250); + }); + } catch(e) { + Console.error('Call.events_interface[async]', e); + } + }); + } catch(e) { + Console.error('Call.events_interface', e); + } + + }; + + + /** + * Return class scope + */ + return self; + +})(); diff --git a/source/app/javascripts/caps.js b/source/app/javascripts/caps.js index 0e58f66..0538f43 100644 --- a/source/app/javascripts/caps.js +++ b/source/app/javascripts/caps.js @@ -20,6 +20,444 @@ var Caps = (function () { var self = {}; + /* Constants */ + self.disco_infos = { + 'identity': { + 'category': 'client', + 'type': 'web', + 'name': 'Jappix' + }, + + 'items': [ + NS_MUC, + NS_MUC_USER, + NS_MUC_ADMIN, + NS_MUC_OWNER, + NS_MUC_CONFIG, + NS_DISCO_INFO, + NS_DISCO_ITEMS, + NS_PUBSUB_RI, + NS_BOSH, + NS_CAPS, + NS_MOOD, + NS_ACTIVITY, + NS_TUNE, + NS_GEOLOC, + NS_NICK, + NS_URN_MBLOG, + NS_URN_INBOX, + NS_MOOD + NS_NOTIFY, + NS_ACTIVITY + NS_NOTIFY, + NS_TUNE + NS_NOTIFY, + NS_GEOLOC + NS_NOTIFY, + NS_URN_MBLOG + NS_NOTIFY, + NS_URN_INBOX + NS_NOTIFY, + NS_URN_DELAY, + NS_ROSTER, + NS_ROSTERX, + NS_HTTP_AUTH, + NS_CHATSTATES, + NS_XHTML_IM, + NS_URN_MAM, + NS_IPV6, + NS_LAST, + NS_PRIVATE, + NS_REGISTER, + NS_SEARCH, + NS_COMMANDS, + NS_VERSION, + NS_XDATA, + NS_VCARD, + NS_IETF_VCARD4, + NS_URN_ADATA, + NS_URN_AMETA, + NS_URN_TIME, + NS_URN_PING, + NS_URN_RECEIPTS, + NS_PRIVACY, + NS_IQOOB, + NS_XOOB, + NS_URN_CARBONS, + NS_URN_CORRECT, + NS_URN_MARKERS, + NS_URN_IDLE, + NS_URN_ATTENTION, + NS_URN_REACH, + NS_URN_HINTS + ] + }; + + + /** + * Parse identities from disco infos query response + * @private + * @param {object} query + * @return {object} + */ + self._parseDiscoIdentities = function(query) { + + var identities = []; + + try { + var cur_category, cur_type, cur_lang, cur_name; + + $(query).find('identity').each(function() { + cur_category = $(this).attr('category') || ''; + cur_type = $(this).attr('type') || ''; + cur_lang = $(this).attr('xml:lang') || ''; + cur_name = $(this).attr('name') || ''; + + identities.push(cur_category + '/' + cur_type + '/' + cur_lang + '/' + cur_name); + }); + } catch(e) { + Console.error('Caps._parseDiscoIdentities', e); + } finally { + return identities; + } + + }; + + + /** + * Parse features from disco infos query response + * @private + * @param {object} query + * @return {object} + */ + self._parseDiscoFeatures = function(query) { + + var features = []; + + try { + var cur_var; + + $(query).find('feature').each(function() { + cur_var = $(this).attr('var'); + + // Add the current value to the array + if(cur_var) { + features.push(cur_var); + } + }); + } catch(e) { + Console.error('Caps._parseDiscoFatures', e); + } finally { + return features; + } + + }; + + + /** + * Parse data form from disco infos query response + * @private + * @param {object} query + * @return {object} + */ + self._parseDiscoDataForms = function(query) { + + var data_forms = []; + + try { + var cur_string, cur_sort_var, + cur_text, cur_var, cur_sort_val; + + $(query).find('x[xmlns="' + NS_XDATA + '"]').each(function() { + // Initialize some stuffs + cur_string = ''; + cur_sort_var = []; + + // Add the form type field + $(this).find('field[var="FORM_TYPE"] value').each(function() { + cur_text = $(this).text(); + + if(cur_text) { + cur_string += cur_text + '<'; + } + }); + + // Add the var attributes into an array + $(this).find('field:not([var="FORM_TYPE"])').each(function() { + cur_var = $(this).attr('var'); + + if(cur_var) { + cur_sort_var.push(cur_var); + } + }); + + // Sort the var attributes + cur_sort_var = cur_sort_var.sort(); + + // Loop this sorted var attributes + $.each(cur_sort_var, function(i) { + // Initialize the value sorting + cur_sort_val = []; + + // Append it to the string + cur_string += cur_sort_var[i] + '<'; + + // Add each value to the array + $(this).find('field[var=' + cur_sort_var[i] + '] value').each(function() { + cur_sort_val.push($(this).text()); + }); + + // Sort the values + cur_sort_val = cur_sort_val.sort(); + + // Append the values to the string + for(var j in cur_sort_val) { + cur_string += cur_sort_val[j] + '<'; + } + }); + + // Any string? + if(cur_string) { + // Remove the undesired double '<' from the string + if(cur_string.match(/(.+)(<)+$/)) { + cur_string = cur_string.substring(0, cur_string.length - 1); + } + + // Add the current string to the array + data_forms.push(cur_string); + } + }); + } catch(e) { + Console.error('Caps._parseDiscoDataForms', e); + } finally { + return data_forms; + } + + }; + + + /** + * Apply XHTML-IM features from disco infos + * @private + * @param {string} xid + * @param {object} features + * @param {object} style_sel + * @param {object} message_area_sel + * @return {undefined} + */ + self._applyDiscoXHTMLIM = function(xid, features, style_sel, message_area_sel) { + + try { + // Apply + if(NS_XHTML_IM in features) { + style_sel.show(); + } else { + // Remove the tooltip elements + style_sel.hide(); + style_sel.find('.bubble-style').remove(); + + // Reset the markers + message_area_sel.removeAttr('style') + .removeAttr('data-font') + .removeAttr('data-fontsize') + .removeAttr('data-color') + .removeAttr('data-bold') + .removeAttr('data-italic') + .removeAttr('data-underline'); + } + + } catch(e) { + Console.error('Caps._applyDiscoXHTMLIM', e); + } + + }; + + + /** + * Apply Jingle features from disco infos + * @private + * @param {string} xid + * @param {object} path_sel + * @param {object} roster_sel + * @return {undefined} + */ + self._applyDiscoJingle = function(xid, path_sel, roster_sel) { + + try { + // Selectors + var roster_jingle_sel = roster_sel.find('.buddy-infos .call-jingle'); + var jingle_audio = path_sel.find('.tools-jingle-audio'); + var roster_jingle_audio = roster_jingle_sel.find('a.audio'); + var jingle_video = path_sel.find('.tools-jingle-video'); + var roster_jingle_video = roster_jingle_sel.find('a.video'); + var roster_jingle_separator = roster_jingle_sel.find('span.separator'); + + // Apply + var jingle_local_supported = JSJAC_JINGLE_AVAILABLE; + var jingle_audio_xid = self.getFeatureResource(xid, NS_JINGLE_APPS_RTP_AUDIO); + var jingle_video_xid = self.getFeatureResource(xid, NS_JINGLE_APPS_RTP_VIDEO); + + if(jingle_audio_xid && jingle_local_supported) { + jingle_audio.show(); + roster_jingle_audio.show(); + } else { + jingle_audio.hide(); + roster_jingle_audio.hide(); + } + + if(jingle_video_xid && jingle_local_supported) { + jingle_video.show(); + roster_jingle_video.show(); + } else { + jingle_video.hide(); + roster_jingle_video.hide(); + } + + if(jingle_audio_xid && jingle_video_xid && jingle_local_supported) { + roster_jingle_separator.show(); + } else { + roster_jingle_separator.hide(); + } + + if((jingle_audio_xid || jingle_video_xid) && jingle_local_supported) { + roster_jingle_sel.show(); + } else { + roster_jingle_sel.hide(); + } + } catch(e) { + Console.error('Caps._applyDiscoJingle', e); + } + + }; + + + /** + * Apply Out of Band Data features from disco infos + * @private + * @param {string} xid + * @param {object} features + * @param {object} file_sel + * @return {undefined} + */ + self._applyDiscoOOB = function(xid, features, file_sel) { + + try { + // Apply + var iq_oob_xid = self.getFeatureResource(xid, NS_IQOOB); + + if(iq_oob_xid || NS_XOOB in features) { + file_sel.show(); + + // Set a marker + file_sel.attr( + 'data-oob', + iq_oob_xid ? 'iq' : 'x' + ); + } else { + // Remove the tooltip elements + file_sel.hide(); + file_sel.find('.bubble-style').remove(); + + // Reset the marker + file_sel.removeAttr('data-oob'); + } + } catch(e) { + Console.error('Caps._applyDiscoOOB', e); + } + + }; + + + /** + * Apply Receipts features from disco infos + * @private + * @param {string} xid + * @param {object} features + * @param {object} message_area_sel + * @return {undefined} + */ + self._applyDiscoReceipts = function(xid, features, message_area_sel) { + + try { + // Apply + if(NS_URN_RECEIPTS in features) { + message_area_sel.attr('data-receipts', 'true'); + } else { + message_area_sel.removeAttr('data-receipts'); + } + } catch(e) { + Console.error('Caps._applyDiscoReceipts', e); + } + + }; + + + /** + * Apply Last Message Correction features from disco infos + * @private + * @param {string} xid + * @param {object} features + * @param {object} path_sel + * @return {undefined} + */ + self._applyDiscoCorrection = function(xid, features, path_sel) { + + try { + // Apply + if(NS_URN_CORRECT in features) { + path_sel.attr('data-correction', 'true'); + } else { + path_sel.removeAttr('data-correction'); + } + } catch(e) { + Console.error('Caps._applyDiscoCorrection', e); + } + + }; + + + /** + * Apply Chat Markers features from disco infos + * @private + * @param {string} xid + * @param {object} features + * @param {object} path_sel + * @return {undefined} + */ + self._applyDiscoMarkers = function(xid, features, path_sel) { + + try { + // Apply + if(NS_URN_MARKERS in features) { + path_sel.attr('data-markers', 'true'); + } else { + path_sel.removeAttr('data-markers'); + } + } catch(e) { + Console.error('Caps._applyDiscoMarkers', e); + } + + }; + + + /** + * Apply Attention features from disco infos + * @private + * @param {string} xid + * @param {object} features + * @param {object} path_sel + * @return {undefined} + */ + self._applyDiscoAttention = function(xid, features, path_sel) { + + try { + // Apply + if(NS_URN_ATTENTION in features) { + path_sel.attr('data-attention', 'true'); + } else { + path_sel.removeAttr('data-attention'); + } + } catch(e) { + Console.error('Caps._applyDiscoAttention', e); + } + + }; + + /** * Reads a stored Caps * @public @@ -47,62 +485,12 @@ var Caps = (function () { self.myDiscoInfos = function() { try { - var disco_base = [ - NS_MUC, - NS_MUC_USER, - NS_MUC_ADMIN, - NS_MUC_OWNER, - NS_MUC_CONFIG, - NS_DISCO_INFO, - NS_DISCO_ITEMS, - NS_PUBSUB_RI, - NS_BOSH, - NS_CAPS, - NS_MOOD, - NS_ACTIVITY, - NS_TUNE, - NS_GEOLOC, - NS_NICK, - NS_URN_MBLOG, - NS_URN_INBOX, - NS_MOOD + NS_NOTIFY, - NS_ACTIVITY + NS_NOTIFY, - NS_TUNE + NS_NOTIFY, - NS_GEOLOC + NS_NOTIFY, - NS_URN_MBLOG + NS_NOTIFY, - NS_URN_INBOX + NS_NOTIFY, - NS_URN_DELAY, - NS_ROSTER, - NS_ROSTERX, - NS_HTTP_AUTH, - NS_CHATSTATES, - NS_XHTML_IM, - NS_URN_MAM, - NS_IPV6, - NS_LAST, - NS_PRIVATE, - NS_REGISTER, - NS_SEARCH, - NS_COMMANDS, - NS_VERSION, - NS_XDATA, - NS_VCARD, - NS_IETF_VCARD4, - NS_URN_ADATA, - NS_URN_AMETA, - NS_URN_TIME, - NS_URN_PING, - NS_URN_RECEIPTS, - NS_PRIVACY, - NS_IQOOB, - NS_XOOB, - NS_URN_CARBONS - ]; + var disco_base = self.disco_infos.items; - var disco_jingle = JSJaCJingle_disco(); + var disco_jingle = JSJaCJingle.disco(); var disco_all = disco_base.concat(disco_jingle); - - return disco_all; + + return Utils.uniqueArrayValues(disco_all); } catch(e) { Console.error('Caps.myDiscoInfos', e); } @@ -123,35 +511,35 @@ var Caps = (function () { // No CAPS if(!caps) { Console.warn('No CAPS: ' + to); - + self.displayDiscoInfos(to, ''); - + return false; } - + // Get the stored disco infos var xml = self.read(caps); - + // Yet stored if(xml) { Console.info('CAPS from cache: ' + to); - + self.displayDiscoInfos(to, xml); - + return true; } - + Console.info('CAPS from the network: ' + to); - + // Not stored: get the disco#infos var iq = new JSJaCIQ(); - + iq.setTo(to); iq.setType('get'); iq.setQuery(NS_DISCO_INFO); - + con.send(iq, self.handleDiscoInfos); - + return true; } catch(e) { Console.error('Caps.getDiscoInfos', e); @@ -169,123 +557,37 @@ var Caps = (function () { self.handleDiscoInfos = function(iq) { try { - if(!iq || (iq.getType() == 'error')) + if(!iq || (iq.getType() == 'error')) { return; - - // IQ received, get some values + } + var from = Common.fullXID(Common.getStanzaFrom(iq)); var query = iq.getQuery(); - - // Generate the CAPS-processing values - var identities = []; - var features = []; - var data_forms = []; - - // Identity values - $(query).find('identity').each(function() { - var pCategory = $(this).attr('category'); - var pType = $(this).attr('type'); - var pLang = $(this).attr('xml:lang'); - var pName = $(this).attr('name'); - - if(!pCategory) - pCategory = ''; - if(!pType) - pType = ''; - if(!pLang) - pLang = ''; - if(!pName) - pName = ''; - - identities.push(pCategory + '/' + pType + '/' + pLang + '/' + pName); - }); - - // Feature values - $(query).find('feature').each(function() { - var pVar = $(this).attr('var'); - - // Add the current value to the array - if(pVar) - features.push(pVar); - }); - - // Data-form values - $(query).find('x[xmlns="' + NS_XDATA + '"]').each(function() { - // Initialize some stuffs - var pString = ''; - var sortVar = []; - - // Add the form type field - $(this).find('field[var="FORM_TYPE"] value').each(function() { - var cText = $(this).text(); - - if(cText) - pString += cText + '<'; - }); - - // Add the var attributes into an array - $(this).find('field:not([var="FORM_TYPE"])').each(function() { - var cVar = $(this).attr('var'); - - if(cVar) - sortVar.push(cVar); - }); - - // Sort the var attributes - sortVar = sortVar.sort(); - - // Loop this sorted var attributes - $.each(sortVar, function(i) { - // Initialize the value sorting - var sortVal = []; - - // Append it to the string - pString += sortVar[i] + '<'; - - // Add each value to the array - $(this).find('field[var=' + sortVar[i] + '] value').each(function() { - sortVal.push($(this).text()); - }); - - // Sort the values - sortVal = sortVal.sort(); - - // Append the values to the string - for(var j in sortVal) { - pString += sortVal[j] + '<'; - } - }); - - // Any string? - if(pString) { - // Remove the undesired double '<' from the string - if(pString.match(/(.+)(<)+$/)) - pString = pString.substring(0, pString.length - 1); - - // Add the current string to the array - data_forms.push(pString); - } - }); - + + // Parse values + var identities = self._parseDiscoIdentities(query); + var features = self._parseDiscoFeatures(query); + var data_forms = self._parseDiscoDataForms(query); + // Process the CAPS var caps = self.process(identities, features, data_forms); - + // Get the XML string var xml = Common.xmlToString(query); - + // Store the disco infos DataStore.setPersistent('global', 'caps', caps, xml); - + // This is our server if(from == Utils.getServer()) { // Handle the features Features.handle(xml); - + Console.info('Got our server CAPS'); } else { // Display the disco infos self.displayDiscoInfos(from, xml); - + Console.info('Got CAPS: ' + from); } } catch(e) { @@ -307,11 +609,12 @@ var Caps = (function () { try { // Generate the chat path var xid = Common.bareXID(from); - + // This comes from a private groupchat chat? - if(Utils.isPrivate(xid)) + if(Utils.isPrivate(xid)) { xid = from; - + } + hash = hex_md5(xid); // Display the supported features @@ -324,98 +627,22 @@ var Caps = (function () { features[current] = 1; } }); - + // Paths - var path = $('#' + hash); - var roster_path = $('#roster .buddy.' + hash); - var roster_jingle_path = roster_path.find('.buddy-infos .call-jingle'); + var path_sel = $('#' + hash); + var roster_sel = $('#roster .buddy.' + hash); + var message_area_sel = path_sel.find('.message-area'); + var style_sel = path_sel.find('.chat-tools-style'); + var file_sel = path_sel.find('.chat-tools-file'); - var message_area = path.find('.message-area'); - var style = path.find('.chat-tools-style'); - var jingle_audio = path.find('.tools-jingle-audio'); - var roster_jingle_audio = roster_jingle_path.find('a.audio'); - var jingle_video = path.find('.tools-jingle-video'); - var roster_jingle_video = roster_jingle_path.find('a.video'); - var roster_jingle_separator = roster_jingle_path.find('span.separator'); - var file = path.find('.chat-tools-file'); - - // Apply xHTML-IM - if(NS_XHTML_IM in features) { - style.show(); - } else { - // Remove the tooltip elements - style.hide(); - style.find('.bubble-style').remove(); - - // Reset the markers - message_area.removeAttr('style') - .removeAttr('data-font') - .removeAttr('data-fontsize') - .removeAttr('data-color') - .removeAttr('data-bold') - .removeAttr('data-italic') - .removeAttr('data-underline'); - } - - // Apply Jingle - var jingle_local_supported = JSJAC_JINGLE_AVAILABLE; - var jingle_audio_xid = self.getFeatureResource(xid, NS_JINGLE_APPS_RTP_AUDIO); - var jingle_video_xid = self.getFeatureResource(xid, NS_JINGLE_APPS_RTP_VIDEO); - - if(jingle_audio_xid && jingle_local_supported) { - jingle_audio.show(); - roster_jingle_audio.show(); - } else { - jingle_audio.hide(); - roster_jingle_audio.hide(); - } - - if(jingle_video_xid && jingle_local_supported) { - jingle_video.show(); - roster_jingle_video.show(); - } else { - jingle_video.hide(); - roster_jingle_video.hide(); - } - - if(jingle_audio_xid && jingle_video_xid && jingle_local_supported) { - roster_jingle_separator.show(); - } else { - roster_jingle_separator.hide(); - } - - if((jingle_audio_xid || jingle_video_xid) && jingle_local_supported) { - roster_jingle_path.show(); - } else { - roster_jingle_path.hide(); - } - - // Apply Out of Band Data - var iq_oob_xid = self.getFeatureResource(xid, NS_IQOOB); - - if(iq_oob_xid || NS_XOOB in features) { - file.show(); - - // Set a marker - file.attr( - 'data-oob', - iq_oob_xid ? 'iq' : 'x' - ); - } else { - // Remove the tooltip elements - file.hide(); - file.find('.bubble-style').remove(); - - // Reset the marker - file.removeAttr('data-oob'); - } - - // Apply receipts - if(NS_URN_RECEIPTS in features) { - message_area.attr('data-receipts', 'true'); - } else { - message_area.removeAttr('data-receipts'); - } + // Apply Features + self._applyDiscoXHTMLIM(xid, features, style_sel, message_area_sel); + self._applyDiscoJingle(xid, path_sel, roster_sel); + self._applyDiscoOOB(xid, features, file_sel); + self._applyDiscoReceipts(xid, features, message_area_sel); + self._applyDiscoCorrection(xid, features, path_sel); + self._applyDiscoMarkers(xid, features, path_sel); + self._applyDiscoAttention(xid, features, path_sel); } catch(e) { Console.error('Caps.displayDiscoInfos', e); } @@ -426,40 +653,40 @@ var Caps = (function () { /** * Generates the CAPS hash * @public - * @param {object} cIdentities - * @param {object} cFeatures - * @param {object} cDataForms + * @param {object} identities + * @param {object} features + * @param {object} dataforms * @return {string} */ - self.process = function(cIdentities, cFeatures, cDataForms) { + self.process = function(identities, features, dataforms) { try { // Initialize - var cString = ''; - + var caps_str = ''; + // Sort the arrays - cIdentities = cIdentities.sort(); - cFeatures = cFeatures.sort(); - cDataForms = cDataForms.sort(); - + identities = identities.sort(); + features = features.sort(); + dataforms = dataforms.sort(); + // Process the sorted identity string - for(var a in cIdentities) { - cString += cIdentities[a] + '<'; + for(var a in identities) { + caps_str += identities[a] + '<'; } - + // Process the sorted feature string - for(var b in cFeatures) { - cString += cFeatures[b] + '<'; + for(var b in features) { + caps_str += features[b] + '<'; } - + // Process the sorted data-form string - for(var c in cDataForms) { - cString += cDataForms[c] + '<'; + for(var c in dataforms) { + caps_str += dataforms[c] + '<'; } - + // Process the SHA-1 hash - var cHash = b64_sha1(cString); - + var cHash = b64_sha1(caps_str); + return cHash; } catch(e) { Console.error('Caps.process', e); @@ -477,7 +704,12 @@ var Caps = (function () { try { return self.process( - ['client/web//Jappix'], + [ + self.disco_infos.identity.category + '/' + + self.disco_infos.identity.type + '//' + + self.disco_infos.identity.name + ], + self.myDiscoInfos(), [] ); @@ -507,8 +739,14 @@ var Caps = (function () { var max_priority = null; var cur_xid_full, cur_presence_sel, cur_caps, cur_features, cur_priority; - for(var cur_resource in Presence.resources(xid)) { - cur_xid_full = xid + '/' + cur_resource; + var resources_obj = Presence.resources(xid); + var fn_parse_resource = function(cur_resource) { + cur_xid_full = xid; + + if(cur_resource) { + cur_xid_full += '/' + cur_resource; + } + cur_presence_sel = $(Presence.readStanza(cur_xid_full)); cur_priority = parseInt((cur_presence_sel.find('priority').text() || 0), 10); @@ -523,6 +761,14 @@ var Caps = (function () { selected_xid = cur_xid_full; } } + }; + + if(resources_obj.bare === 1) { + fn_parse_resource(null); + } + + for(var cur_resource in resources_obj.list) { + fn_parse_resource(cur_resource); } } catch(e) { Console.error('Caps.getFeatureResource', e); @@ -538,4 +784,4 @@ var Caps = (function () { */ return self; -})(); \ No newline at end of file +})(); diff --git a/source/app/javascripts/carbons.js b/source/app/javascripts/carbons.js index 7742883..4635b94 100644 --- a/source/app/javascripts/carbons.js +++ b/source/app/javascripts/carbons.js @@ -22,7 +22,7 @@ var Carbons = (function () { /** * Configures Message Carbons options - * @public + * @private * @param {string} type * @return {undefined} */ @@ -35,9 +35,9 @@ var Carbons = (function () { var iq = new JSJaCIQ(); iq.setType('set'); - + iq.appendNode(type, {'xmlns': NS_URN_CARBONS}); - + con.send(iq, function(iq) { self._handleConfigure(iq, type); }); @@ -50,7 +50,7 @@ var Carbons = (function () { /** * Configures Message Carbons options - * @public + * @private * @param {object} iq * @param {string} type * @return {undefined} @@ -193,6 +193,9 @@ var Carbons = (function () { } else { Console.debug('Got a sent message from another resource to: ' + (to || 'none') + ', was ignored because body empty'); } + + // Handle chat markers change + Markers.handleCarbonChange(forwarded_message); } else { Console.debug('Got a sent message from another resource to: ' + (to || 'none') + ', was ignored because chat not open'); } diff --git a/source/app/javascripts/chat.js b/source/app/javascripts/chat.js index 4b064dd..a9f0bd6 100644 --- a/source/app/javascripts/chat.js +++ b/source/app/javascripts/chat.js @@ -20,6 +20,290 @@ var Chat = (function () { var self = {}; + /** + * Apply generate events + * @private + * @param {string} path + * @param {string} id + * @param {string} xid + * @return {undefined} + */ + self._generateEvents = function(path, id, xid) { + + try { + // Click event: chat cleaner + $(path + 'tools-clear').click(function() { + self.clean(id); + }); + + // Click event: call (audio) + $(path + 'tools-jingle-audio').click(function() { + Jingle.start(xid, 'audio'); + }); + + // Click event: call (video) + $(path + 'tools-jingle-video').click(function() { + Jingle.start(xid, 'video'); + }); + + // Click event: user-infos + $(path + 'tools-infos').click(function() { + UserInfos.open(xid); + }); + } catch(e) { + Console.error('Chat._generateEvents', e); + } + + }; + + + /** + * Apply generate events + * @private + * @param {object} input_sel + * @param {string} xid + * @param {string} hash + * @return {undefined} + */ + self._createEvents = function(input_sel, xid, hash) { + + try { + self._createEventsInput(input_sel, hash); + self._createEventsKey(input_sel, xid, hash); + self._createEventsMouse(xid, hash); + } catch(e) { + Console.error('Chat._createEvents', e); + } + + }; + + + /** + * Apply generate events (input) + * @private + * @param {object} input_sel + * @param {string} hash + * @return {undefined} + */ + self._createEventsInput = function(input_sel, hash) { + + try { + input_sel.focus(function() { + // Clean notifications for this chat + Interface.chanCleanNotify(hash); + + // Store focus on this chat! + Interface.chat_focus_hash = hash; + }); + + input_sel.blur(function() { + // Reset storage about focus on this chat! + if(Interface.chat_focus_hash == hash) { + Interface.chat_focus_hash = null; + } + }); + } catch(e) { + Console.error('Chat._createEventsInput', e); + } + + }; + + + /** + * Apply generate events (key) + * @private + * @param {object} input_sel + * @param {string} xid + * @param {string} hash + * @return {undefined} + */ + self._createEventsKey = function(input_sel, xid, hash) { + + try { + input_sel.keydown(function(e) { + if(e.keyCode == 13) { + // Enter key + if(e.shiftKey || e.ctrlKey) { + // Add a new line + input_sel.val(input_sel.val() + '\n'); + } else { + if(Correction.isIn(xid) === true) { + var corrected_value = input_sel.val().trim(); + + if(corrected_value) { + // Send the corrected message + Correction.send(xid, 'chat', corrected_value); + } + + Correction.leave(xid); + } else { + // Send the message + Message.send(hash, 'chat'); + } + + // Reset the composing database entry + DataStore.setDB(Connection.desktop_hash, 'chatstate', xid, 'off'); + } + + return false; + } else if(e.keyCode == 8) { + // Leave correction mode? (another way, by flushing input value progressively) + if(Correction.isIn(xid) === true && !input_sel.val()) { + Correction.leave(xid); + } + } + }); + + input_sel.keyup(function(e) { + if(e.keyCode == 27) { + // Escape key + input_sel.val(''); + + // Leave correction mode? (simple escape way) + if(Correction.isIn(xid) === true) { + Correction.leave(xid); + } + } else { + Correction.detect(xid, input_sel); + } + }); + } catch(e) { + Console.error('Chat._createEventsKey', e); + } + + }; + + + /** + * Apply generate events (mouse) + * @private + * @param {string} xid + * @param {string} hash + * @return {undefined} + */ + self._createEventsMouse = function(xid, hash) { + + try { + // Scroll in chat content + $('#page-engine #' + hash + ' .content').scroll(function() { + var self = this; + + if(Features.enabledMAM() && !(xid in MAM.map_pending)) { + var has_state = xid in MAM.map_states; + var rsm_count = has_state ? MAM.map_states[xid].rsm.count : 1; + var rsm_before = has_state ? MAM.map_states[xid].rsm.first : ''; + + // Request more archives? + if(rsm_count > 0 && $(this).scrollTop() < MAM.SCROLL_THRESHOLD) { + var was_scroll_top = $(self).scrollTop() <= 32; + var wait_mam = $('#' + hash).find('.wait-mam'); + wait_mam.show(); + + MAM.getArchives({ + 'with': xid + }, { + 'max': MAM.REQ_MAX, + 'before': rsm_before + }, function() { + var wait_mam_height = was_scroll_top ? 0 : wait_mam.height(); + wait_mam.hide(); + + // Restore scroll? + if($(self).scrollTop() < MAM.SCROLL_THRESHOLD) { + var sel_mam_chunk = $(self).find('.mam-chunk:first'); + + var cont_padding_top = parseInt($(self).css('padding-top').replace(/[^-\d\.]/g, '')); + var cont_one_group_margin_bottom = parseInt(sel_mam_chunk.find('.one-group:last').css('margin-bottom').replace(/[^-\d\.]/g, '')); + var cont_mam_chunk_height = sel_mam_chunk.height(); + + $(self).scrollTop(wait_mam_height + cont_padding_top + cont_one_group_margin_bottom + cont_mam_chunk_height); + } + }); + } + } + }); + } catch(e) { + Console.error('Chat._createEventsMouse', e); + } + + }; + + + /** + * Apply generate events + * @private + * @param {string} type + * @param {string} id + * @param {string} xid + * @return {object} + */ + self._generateChatCode = function(type, id, xid) { + + var code_args = {}; + + try { + // Groupchat special code + if(type == 'groupchat') { + code_args.attributes = ' data-type="groupchat" data-correction="true"'; + code_args.avatar = ''; + code_args.name = '

' + Common._e("Subject") + ' ' + Common._e("no subject defined for this room.") + '

'; + code_args.code = '
' + + '

' + Common._e("Moderators") + '

' + + '

' + Common._e("Participants") + '

' + + '

' + Common._e("Visitors") + '

' + + '

' + Common._e("Others") + '

'; + code_args.link = ''; + code_args.style = ''; + + // Is this a gateway? + if(xid.match(/%/)) { + code_args.disabled = ''; + } else { + code_args.disabled = ' disabled=""'; + } + } else { + code_args.mam = '
'; + code_args.attributes = ' data-type="chat"'; + code_args.avatar = '
'; + code_args.name = '

'; + code_args.code = '
' + code_args.mam + '
'; + code_args.link = '' + + '' + + ''; + code_args.style = ' style="display: none;"'; + code_args.disabled = ''; + } + + // Not a groupchat private chat, we can use the buddy add icon + if((type == 'chat') || (type == 'groupchat')) { + var title; + + if(type == 'chat') { + title = Common._e("Add this contact to your friends"); + } else { + title = Common._e("Add this groupchat to your favorites"); + } + + code_args.link += ''; + } + + // IE DOM parsing bug fix + code_args.style_picker = '
' + + '' + + '
'; + + if((BrowserDetect.browser == 'Explorer') && (BrowserDetect.version < 9)) { + code_args.style_picker = ''; + } + } catch(e) { + Console.error('Chat._generateChatCode', e); + } finally { + return code_args; + } + + }; + + /** * Correctly opens a new chat * @public @@ -34,38 +318,40 @@ var Chat = (function () { try { // No XID? - if(!xid) + if(!xid) { return false; - + } + // We generate some stuffs var hash = hex_md5(xid); var name; - + // Gets the name of the user/title of the room - if(title) + if(title) { name = title; - - else { + } else { // Private groupchat chat - if(type == 'private') + if(type == 'private') { name = Common.thisResource(xid); - + } + // XMPP-ID - else if(xid.indexOf('@') != -1) + else if(xid.indexOf('@') != -1) { name = Name.getBuddy(xid); - + } + // Gateway - else + else { name = xid; + } } - + // If the target div does not exist if(!Common.exists('#' + hash)) { // We check the type of the chat to open - if((type == 'chat') || (type == 'private')) + if((type == 'chat') || (type == 'private')) { self.create(hash, xid, name, type); - - else if(type == 'groupchat') { + } else if(type == 'groupchat') { // Try to read the room stored configuration if(!Utils.isAnonymous() && (!nickname || !password || !title)) { // Catch the room data @@ -73,7 +359,7 @@ var Chat = (function () { var fNick = fData.find('nick').text(); var fPwd = fData.find('password').text(); var fName = fData.find('name').text(); - + // Apply the room data if(!nickname && fNick) nickname = fNick; @@ -82,11 +368,11 @@ var Chat = (function () { if(!title && fName) name = fName; } - + Groupchat.create(hash, xid, name, nickname, password); } } - + // Switch to the newly-created chat Interface.switchChan(hash); } catch(e) { @@ -113,121 +399,53 @@ var Chat = (function () { // Generate some stuffs var path = '#' + id + ' .'; var escaped_xid = escape(xid); - + // Special code - var specialAttributes, specialAvatar, specialName, specialCode, specialLink, specialDisabled, specialStyle, specialMAM; - - // Groupchat special code - if(type == 'groupchat') { - specialAttributes = ' data-type="groupchat"'; - specialAvatar = ''; - specialName = '

' + Common._e("Subject") + ' ' + Common._e("no subject defined for this room.") + '

'; - specialCode = '

' + Common._e("Moderators") + '

' + Common._e("Participants") + '

' + Common._e("Visitors") + '

' + Common._e("Others") + '

'; - specialLink = ''; - specialStyle = ''; - - // Is this a gateway? - if(xid.match(/%/)) - specialDisabled = ''; - else - specialDisabled = ' disabled=""'; - } - - // Chat (or other things?!) special code - else { - specialMAM = '
'; - specialAttributes = ' data-type="chat"'; - specialAvatar = '
'; - specialName = '

'; - specialCode = '
' + specialMAM + '
'; - specialLink = '' + - '' + - ''; - specialStyle = ' style="display: none;"'; - specialDisabled = ''; - } - - // Not a groupchat private chat, we can use the buddy add icon - if((type == 'chat') || (type == 'groupchat')) { - var addTitle; - - if(type == 'chat') - addTitle = Common._e("Add this contact to your friends"); - else - addTitle = Common._e("Add this groupchat to your favorites"); - - specialLink += ''; - } - - // IE DOM parsing bug fix - var specialStylePicker = '
' + - '' + - '
'; - - if((BrowserDetect.browser == 'Explorer') && (BrowserDetect.version < 9)) - specialStylePicker = ''; - + var chat_args = self._generateChatCode(type, id, xid); + // Append the chat HTML code $('#page-engine').append( - '
' + - '
' + - specialAvatar + - - '
' + - '

' + nick.htmlEnc() + '

' + - specialName + - '
' + - '
' + - - specialCode + - - '
' + - '' + - - '
' + - '' + - '
' + - '
' + + '
' + + '
' + + chat_args.avatar + + + '
' + + '

' + nick.htmlEnc() + '

' + + chat_args.name + + '
' + + '
' + + + chat_args.code + + + '
' + + '' + + + '
' + + '' + + '
' + + '
' + '
' ); - - // Click event: chat cleaner - $(path + 'tools-clear').click(function() { - self.clean(id); - }); - // Click event: call (audio) - $(path + 'tools-jingle-audio').click(function() { - Jingle.start(xid, 'audio'); - }); - - // Click event: call (video) - $(path + 'tools-jingle-video').click(function() { - Jingle.start(xid, 'video'); - }); - - // Click event: user-infos - $(path + 'tools-infos').click(function() { - UserInfos.open(xid); - }); + self._generateEvents(path, id, xid); } catch(e) { Console.error('Chat.generate', e); } @@ -249,32 +467,38 @@ var Chat = (function () { try { // Path to the element var chat_switch = '#page-switch .'; - + // Special code - var specialClass = ' unavailable'; + var special_class = ' unavailable'; var show_close = true; - + // Groupchat if(type == 'groupchat') { - specialClass = ' groupchat-default'; - - if(Utils.isAnonymous() && (xid == Common.generateXID(ANONYMOUS_ROOM, 'groupchat'))) + special_class = ' groupchat-default'; + + if(Utils.isAnonymous() && (xid == Common.generateXID(ANONYMOUS_ROOM, 'groupchat'))) { show_close = false; + } } - + // Generate the HTML code - var html = '
' + - '
' + - - '
' + nick.htmlEnc() + '
'; - + var html = '
' + + '
' + + + '
' + nick.htmlEnc() + '
'; + // Show the close button if not MUC and not anonymous - if(show_close) - html += '
x
'; - + if(show_close) { + html += '
' + + 'x' + + '
'; + } + // Close the HTML html += '
'; - + // Append the HTML code $(chat_switch + 'chans, ' + chat_switch + 'more-content').append(html); } catch(e) { @@ -295,10 +519,10 @@ var Chat = (function () { try { // Remove the messages $('#page-engine #' + chat + ' .content .one-group').remove(); - + // Clear the history database Message.removeLocalArchive(chat); - + // Focus again $(document).oneTime(10, function() { $('#page-engine #' + chat + ' .text .message-area').focus(); @@ -346,14 +570,14 @@ var Chat = (function () { try { Console.info('New chat: ' + xid); - + // Create the chat content self.generate(type, hash, xid, nick); - + // Create the chat switcher self.generateSwitch(type, hash, xid, nick); - - // If the user is not in our roster + + // Is this a chat? if(type == 'chat') { // MAM? Get archives from there! if(Features.enabledMAM()) { @@ -366,130 +590,73 @@ var Chat = (function () { } else { // Restore the chat history var chat_history = Message.readLocalArchive(hash); - + if(chat_history) { // Generate hashs var my_hash = hex_md5(Common.getXID()); var friend_hash = hex_md5(xid); - + // Add chat history HTML - $('#' + hash + ' .content').append(chat_history); - + var path_sel = $('#' + hash); + + path_sel.find('.content').append(chat_history); + // Filter old groups & messages - $('#' + hash + ' .one-group[data-type="user-message"]').addClass('from-history').attr('data-type', 'old-message'); - $('#' + hash + ' .user-message').removeClass('user-message').addClass('old-message'); - + var one_group_sel = path_sel.find('.one-group'); + one_group_sel.filter('[data-type="user-message"]').addClass('from-history').attr('data-type', 'old-message'); + path_sel.find('.user-message').removeClass('user-message').addClass('old-message'); + // Regenerate user names - $('#' + hash + ' .one-group.' + my_hash + ' b.name').text(Name.getBuddy(Common.getXID())); - $('#' + hash + ' .one-group.' + friend_hash + ' b.name').text(Name.getBuddy(xid)); - + one_group_sel.filter('.' + my_hash + ' b.name').text( + Name.getBuddy(Common.getXID()) + ); + + one_group_sel.filter('.' + friend_hash + ' b.name').text( + Name.getBuddy(xid) + ); + // Regenerate group dates - $('#' + hash + ' .one-group').each(function() { - var current_stamp = parseInt($(this).attr('data-stamp')); + one_group_sel.each(function() { + var current_stamp = parseInt($(this).attr('data-stamp'), 10); $(this).find('span.date').text(DateUtils.relative(current_stamp)); }); - + // Regenerate avatars - if(Common.exists('#' + hash + ' .one-group.' + my_hash + ' .avatar-container')) + if(Common.exists('#' + hash + ' .one-group.' + my_hash + ' .avatar-container')) { Avatar.get(Common.getXID(), 'cache', 'true', 'forget'); - if(Common.exists('#' + hash + ' .one-group.' + friend_hash + ' .avatar-container')) + } + + if(Common.exists('#' + hash + ' .one-group.' + friend_hash + ' .avatar-container')) { Avatar.get(xid, 'cache', 'true', 'forget'); + } } } // Add button - if(!Roster.isFriend(xid)) + if(!Roster.isFriend(xid)) { $('#' + hash + ' .tools-add').click(function() { // Hide the icon (to tell the user all is okay) $(this).hide(); - + // Send the subscribe request Roster.addThisContact(xid, nick); }).show(); + } } - + // We catch the user's informations (like this avatar, vcard, and so on...) UserInfos.get(hash, xid, nick, type); - + // The icons-hover functions Tooltip.icons(xid, hash); - + // The event handlers - var inputDetect = $('#page-engine #' + hash + ' .message-area'); - - inputDetect.focus(function() { - // Clean notifications for this chat - Interface.chanCleanNotify(hash); - - // Store focus on this chat! - Interface.chat_focus_hash = hash; - }); - - inputDetect.blur(function() { - // Reset storage about focus on this chat! - if(Interface.chat_focus_hash == hash) - Interface.chat_focus_hash = null; - }); - - inputDetect.keypress(function(e) { - // Enter key - if(e.keyCode == 13) { - // Add a new line - if(e.shiftKey || e.ctrlKey) { - inputDetect.val(inputDetect.val() + '\n'); - } else { - // Send the message - Message.send(hash, 'chat'); - - // Reset the composing database entry - DataStore.setDB(Connection.desktop_hash, 'chatstate', xid, 'off'); - } - - return false; - } - }); + var input_sel = $('#page-engine #' + hash + ' .message-area'); + self._createEvents(input_sel, xid, hash); - // Scroll in chat content - $('#page-engine #' + hash + ' .content').scroll(function() { - var self = this; - - if(Features.enabledMAM() && !(xid in MAM.map_pending)) { - var has_state = xid in MAM.map_states; - var rsm_count = has_state ? MAM.map_states[xid].rsm.count : 1; - var rsm_before = has_state ? MAM.map_states[xid].rsm.first : ''; - - // Request more archives? - if(rsm_count > 0 && $(this).scrollTop() < MAM.SCROLL_THRESHOLD) { - var was_scroll_top = $(self).scrollTop() <= 32; - var wait_mam = $('#' + hash).find('.wait-mam'); - wait_mam.show(); - - MAM.getArchives({ - 'with': xid - }, { - 'max': MAM.REQ_MAX, - 'before': rsm_before - }, function() { - var wait_mam_height = was_scroll_top ? 0 : wait_mam.height(); - wait_mam.hide(); - - // Restore scroll? - if($(self).scrollTop() < MAM.SCROLL_THRESHOLD) { - var sel_mam_chunk = $(self).find('.mam-chunk:first'); - - var cont_padding_top = parseInt($(self).css('padding-top').replace(/[^-\d\.]/g, '')); - var cont_one_group_margin_bottom = parseInt(sel_mam_chunk.find('.one-group:last').css('margin-bottom').replace(/[^-\d\.]/g, '')); - var cont_mam_chunk_height = sel_mam_chunk.height(); - - $(self).scrollTop(wait_mam_height + cont_padding_top + cont_one_group_margin_bottom + cont_mam_chunk_height); - } - }); - } - } - }); - - // Chatstate events - ChatState.events(inputDetect, xid, hash, 'chat'); + // Input events + ChatState.events(input_sel, xid, hash, 'chat'); + Markers.events(input_sel, xid, hash, 'chat'); } catch(e) { Console.error('Chat.create', e); } @@ -502,4 +669,4 @@ var Chat = (function () { */ return self; -})(); \ No newline at end of file +})(); diff --git a/source/app/javascripts/chatstate.js b/source/app/javascripts/chatstate.js index ad55101..d0353fb 100644 --- a/source/app/javascripts/chatstate.js +++ b/source/app/javascripts/chatstate.js @@ -32,24 +32,27 @@ var ChatState = (function () { try { var user_type = $('#' + hash).attr('data-type'); - + // If the friend client supports chatstates and is online if((user_type == 'groupchat') || ((user_type == 'chat') && $('#' + hash + ' .message-area').attr('data-chatstates') && !Common.exists('#page-switch .' + hash + ' .unavailable'))) { // Already sent? - if(DataStore.getDB(Connection.desktop_hash, 'currentchatstate', xid) == state) + if(DataStore.getDB(Connection.desktop_hash, 'currentchatstate', xid) == state) { return; - + } + // Write the state DataStore.setDB(Connection.desktop_hash, 'currentchatstate', xid, state); - + // New message stanza var aMsg = new JSJaCMessage(); aMsg.setTo(xid); aMsg.setType(user_type); - + // Append the chatstate node - aMsg.appendNode(state, {'xmlns': NS_CHATSTATES}); - + aMsg.appendNode(state, { + 'xmlns': NS_CHATSTATES + }); + // Send this! con.send(aMsg); } @@ -74,58 +77,61 @@ var ChatState = (function () { // Groupchat? if(type == 'groupchat') { self.reset(hash, type); - + // "gone" state not allowed - if(state != 'gone') + if(state != 'gone') { $('#page-engine .page-engine-chan .user.' + hash).addClass(state); + } } - + // Chat else { // We change the buddy name color in the page-switch self.reset(hash, type); $('#page-switch .' + hash + ' .name').addClass(state); - + // We generate the chatstate text var text = ''; - + switch(state) { // Active case 'active': text = Common._e("Your friend is paying attention to the conversation."); - + break; - + // Composing case 'composing': text = Common._e("Your friend is writing a message..."); - + break; - + // Paused case 'paused': text = Common._e("Your friend stopped writing a message."); - + break; - + // Inactive case 'inactive': text = Common._e("Your friend is doing something else."); - + break; - + // Gone case 'gone': text = Common._e("Your friend closed the chat."); - + break; } - + // We reset the previous state $('#' + hash + ' .chatstate').remove(); - + // We create the chatstate - $('#' + hash + ' .content').after('
' + text + '
'); + $('#' + hash + ' .content').after( + '
' + text + '
' + ); } } catch(e) { Console.error('ChatState.display', e); @@ -146,12 +152,13 @@ var ChatState = (function () { try { // Define the selector var selector; - - if(type == 'groupchat') + + if(type == 'groupchat') { selector = $('#page-engine .page-engine-chan .user.' + hash); - else + } else { selector = $('#page-switch .' + hash + ' .name'); - + } + // Reset! selector.removeClass('active composing paused inactive gone'); } catch(e) { @@ -179,53 +186,56 @@ var ChatState = (function () { if($(this).val() && (DataStore.getDB(Connection.desktop_hash, 'chatstate', xid) != 'on')) { // We change the state detect input DataStore.setDB(Connection.desktop_hash, 'chatstate', xid, 'on'); - + // We send the friend a "composing" chatstate self.send('composing', xid, hash); } - + // Flushed the message which was being composed else if(!$(this).val() && (DataStore.getDB(Connection.desktop_hash, 'chatstate', xid) == 'on')) { // We change the state detect input DataStore.setDB(Connection.desktop_hash, 'chatstate', xid, 'off'); - + // We send the friend an "active" chatstate self.send('active', xid, hash); } } }); - + target.change(function() { // Reset the composing database entry DataStore.setDB(Connection.desktop_hash, 'chatstate', xid, 'off'); }); - + target.focus(function() { // Not needed - if(target.is(':disabled')) + if(target.is(':disabled')) { return; - + } + // Something was written, user started writing again - if($(this).val()) + if($(this).val()) { self.send('composing', xid, hash); + } // Chat only: Nothing in the input, user is active - else if(type == 'chat') + else if(type == 'chat') { self.send('active', xid, hash); + } }); - + target.blur(function() { // Not needed - if(target.is(':disabled')) + if(target.is(':disabled')) { return; - - // Something was written, user paused - if($(this).val()) - self.send('paused', xid, hash); + } - // Chat only: Nothing in the input, user is inactive - else if(type == 'chat') + // Something was written, user paused + if($(this).val()) { + self.send('paused', xid, hash); + } else if(type == 'chat') { self.send('inactive', xid, hash); + } }); } catch(e) { Console.error('ChatState.events', e); diff --git a/source/app/javascripts/common.js b/source/app/javascripts/common.js index c6047a9..5874604 100644 --- a/source/app/javascripts/common.js +++ b/source/app/javascripts/common.js @@ -20,6 +20,10 @@ var Common = (function () { var self = {}; + /* Constants */ + self.R_DOMAIN_NAME = /^(([a-zA-Z0-9-\.]+)\.)?[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/i; + + /** * Checks if an element exists in the DOM * @public @@ -109,6 +113,29 @@ var Common = (function () { }; + /** + * Matches a domain name + * @public + * @param {string} xid + * @return {boolean} + */ + self.isDomain = function(xid) { + + is_domain = false; + + try { + if(xid.match(self.R_DOMAIN_NAME)) { + is_domain = true; + } + } catch(e) { + Console.error('Common.isDomain', e); + } finally { + return is_domain; + } + + }; + + /** * Generates the good XID * @public @@ -120,22 +147,23 @@ var Common = (function () { try { // XID needs to be transformed - // .. and made lowercase (uncertain though this is the right place...) xid = xid.toLowerCase(); - if(xid && (xid.indexOf('@') == -1)) { - // Groupchat - if(type == 'groupchat') + if(xid && (xid.indexOf('@') === -1)) { + // Groupchat XID + if(type == 'groupchat') { return xid + '@' + HOST_MUC; - - // One-to-one chat - if(xid.indexOf('.') == -1) - return xid + '@' + HOST_MAIN; - - // It might be a gateway? - return xid; + } + + // Gateway XID + if(self.isDomain(xid) === true) { + return xid; + } + + // User XID + return xid + '@' + HOST_MAIN; } - + // Nothing special (yet bare XID) return xid; } catch(e) { @@ -190,15 +218,17 @@ var Common = (function () { self.strAfterLast = function(given_char, str) { try { - if(!given_char || !str) + if(!given_char || !str) { return ''; - + } + var char_index = str.lastIndexOf(given_char); var str_return = str; - - if(char_index >= 0) + + if(char_index >= 0) { str_return = str.substr(char_index + 1); - + } + return str_return; } catch(e) { Console.error('Common.strAfterLast', e); @@ -220,15 +250,16 @@ var Common = (function () { try { // Get the index of our char to explode var index = toStr.indexOf(toEx); - + // We split if necessary the string if(index !== -1) { - if(i === 0) + if(i === 0) { toStr = toStr.substr(0, index); - else + } else { toStr = toStr.substr(index + 1); + } } - + // We return the value return toStr; } catch(e) { @@ -309,8 +340,9 @@ var Common = (function () { // Spec: http://tools.ietf.org/html/rfc6122#appendix-A try { - if(!node) + if(!node) { return node; + } // Remove prohibited chars var prohibited_chars = ['"', '&', '\'', '/', ':', '<', '>', '@']; @@ -347,6 +379,40 @@ var Common = (function () { }; + /** + * Escapes quotes in a string + * @public + * @param {string} str + * @return {string} + */ + self.escapeQuotes = function(str) { + + try { + return escape(self.encodeQuotes(str)); + } catch(e) { + Console.error('Common.escapeQuotes', e); + } + + }; + + + /** + * Unescapes quotes in a string + * @public + * @param {string} str + * @return {string} + */ + self.unescapeQuotes = function(str) { + + try { + return unescape(str); + } catch(e) { + Console.error('Common.unescapeQuotes', e); + } + + }; + + /** * Gets the bare XID from a XID * @public @@ -358,12 +424,12 @@ var Common = (function () { try { // Cut the resource xid = self.cutResource(xid); - + // Launch nodeprep - if(xid.indexOf('@') != -1) { - xid = self.nodeprep(self.getXIDNick(xid)) + '@' + self.getXIDHost(xid); + if(xid.indexOf('@') !== -1) { + xid = self.nodeprep(self.getXIDNick(xid, true)) + '@' + self.getXIDHost(xid); } - + return xid; } catch(e) { Console.error('Common.bareXID', e); @@ -384,11 +450,12 @@ var Common = (function () { // Normalizes the XID var full = self.bareXID(xid); var resource = self.thisResource(xid); - + // Any resource? - if(resource) + if(resource) { full += '/' + resource; - + } + return full; } catch(e) { Console.error('Common.fullXID', e); @@ -401,15 +468,19 @@ var Common = (function () { * Gets the nick from a XID * @public * @param {string} aXID + * @param {boolean} raw_explode * @return {string} */ - self.getXIDNick = function(aXID) { + self.getXIDNick = function(aXID, raw_explode) { try { - // Gateway nick? - if(aXID.match(/\\40/)) - return self.explodeThis('\\40', aXID, 0); - + if(raw_explode !== true) { + // Gateway nick? + if(aXID.match(/\\40/)) { + return self.explodeThis('\\40', aXID, 0); + } + } + return self.explodeThis('@', aXID, 0); } catch(e) { Console.error('Common.getXIDNick', e); @@ -484,7 +555,7 @@ var Common = (function () { /** - * Gets the full XID of the user + * Gets the bare XID of the user * @public * @return {string} */ @@ -495,7 +566,7 @@ var Common = (function () { if(con.username && con.domain) { return con.username + '@' + con.domain; } - + return ''; } catch(e) { Console.error('Common.getXID', e); @@ -504,6 +575,29 @@ var Common = (function () { }; + /** + * Gets the full XID of the user + * @public + * @return {string} + */ + self.getFullXID = function() { + + try { + var xid = self.getXID(); + + // Return the full XID of the user + if(xid) { + return xid + '/' + con.resource; + } + + return ''; + } catch(e) { + Console.error('Common.getFullXID', e); + } + + }; + + /** * Generates the colors for a given user XID * @public @@ -521,15 +615,15 @@ var Common = (function () { '00236b', '4e005c' ); - + var number = 0; - + for(var i = 0; i < xid.length; i++) { number += xid.charCodeAt(i); } - + var color = '#' + colors[number % (colors.length)]; - + return color; } catch(e) { Console.error('Common.generateColor', e); @@ -549,7 +643,7 @@ var Common = (function () { is_gateway = true; try { - if(xid.indexOf('@') != -1) { + if(xid.indexOf('@') !== -1) { is_gateway = false; } } catch(e) { @@ -571,12 +665,12 @@ var Common = (function () { try { var from = stanza.getFrom(); - + // No from, we assume this is our XID if(!from) { from = self.getXID(); } - + return from; } catch(e) { Console.error('Common.getStanzaFrom', e); @@ -618,13 +712,15 @@ var Common = (function () { try { // Negative number (without first 0) - if(i > -10 && i < 0) + if(i > -10 && i < 0) { return '-0' + (i * -1); - + } + // Positive number (without first 0) - if(i < 10 && i >= 0) + if(i < 10 && i >= 0) { return '0' + i; - + } + // All is okay return i; } catch(e) { @@ -643,23 +739,31 @@ var Common = (function () { */ self.escapeRegex = function(query) { - if (query instanceof Array) { - var result = new Array(query.length); - for(i=0; i' + - Common._e("You have been registered, here is your XMPP address:") + ' ' + username.htmlEnc() + '@' + domain.htmlEnc() + ' - ' + Common._e("Login") + '' + + '
' + + Common._e("You have been registered, here is your XMPP address:") + + ' ' + username.htmlEnc() + '@' + domain.htmlEnc() + ' - ' + + '' + Common._e("Login") + '' + '
' ); - + // Login link $('#home .homediv.registerer .success a').click(function() { return self.doLogin(username, domain, pass, '', '10', false); }); - + if((REGISTER_API == 'on') && (domain == HOST_MAIN) && captcha) { - // Show the waiting image - Interface.showGeneralWait(); - - // Change the page title - Interface.title('wait'); - - // Send request - $.post('./server/register.php', {username: username, domain: domain, password: pass, captcha: captcha}, function(data) { - // Error registering - Interface.removeGeneralWait(); - Interface.title('home'); - - // In all case, update CAPTCHA - $('#home img.captcha_img').attr('src', './server/captcha.php?id=' + genID()); - $('#home input.captcha').val(''); - - // Registration okay - if($(data).find('query status').text() == '1') { - self.handleRegistered(); - } else { - // Show error message - var error_message = ''; - - switch($(data).find('query message').text()) { - case 'CAPTCHA Not Matching': - error_message = Common._e("The security code you entered is invalid. Please retry with another one."); - - $('#home input.captcha').focus(); - - break; - - case 'Username Unavailable': - error_message = Common._e("The username you picked is not available. Please try another one."); - - $('#home input.nick').focus(); - - break; - - default: - error_message = Common._e("There was an error registering your account. Please retry."); - - break; - } - - if(error_message) - Errors.show('', error_message, ''); - } - }); + self._doRegisterAPI(username, domain, pass, captcha); } else { - try { - oArgs = {}; - - if(Common.hasWebSocket()) { - // WebSocket supported & configured - con = new JSJaCWebSocketConnection({ - httpbase: HOST_WEBSOCKET - }); - } else { - var httpbase = (HOST_BOSH_MAIN || HOST_BOSH); - - // Check BOSH origin - BOSH_SAME_ORIGIN = Origin.isSame(httpbase); - - // We create the new http-binding connection - con = new JSJaCHttpBindingConnection({ - httpbase: httpbase - }); - } - - // We setup the connection ! - con.registerHandler('onconnect', self.handleRegistered); - con.registerHandler('onerror', Errors.handle); - - // We retrieve what the user typed in the register inputs - oArgs = {}; - oArgs.domain = $.trim(domain); - oArgs.username = $.trim(username); - oArgs.resource = JAPPIX_RESOURCE + ' Register (' + (new Date()).getTime() + ')'; - oArgs.pass = pass; - oArgs.register = true; - oArgs.secure = true; - oArgs.xmllang = XML_LANG; - - con.connect(oArgs); - - // Show the waiting image - Interface.showGeneralWait(); - - // Change the page title - Interface.title('wait'); - } - - catch(e) { - // Logs errors - Console.error('doRegister', e); - } + self._doRegisterInBand(username, domain, pass); } } catch(e) { Console.error('Connection.doRegister', e); @@ -294,31 +402,29 @@ var Connection = (function () { try { Console.info('Trying to login anonymously...'); - - var aPath = '#home .anonymouser '; - var room = $(aPath + '.room').val(); - var nick = $(aPath + '.nick').val(); - - // If the form is correctly completed + + var path_sel = $('#home .anonymouser'); + var room = path_sel.find('.room').val(); + var nick = path_sel.find('.nick').val(); + + // Form correctly completed? if(room && nick) { // We remove the not completed class to avoid problems $('#home .anonymouser input').removeClass('please-complete'); - + // Redirect the user to the anonymous room window.location.href = JAPPIX_LOCATION + '?r=' + room + '&n=' + nick; - } - - // We check if the form is entirely completed - else { - $(aPath + 'input[type="text"]').each(function() { - var select = $(this); - - if(!select.val()) + } else { + path_sel.find('input[type="text"]').each(function() { + var this_sel = $(this); + + if(!this_sel.val()) { $(document).oneTime(10, function() { - select.addClass('please-complete').focus(); + this_sel.addClass('please-complete').focus(); }); - else - select.removeClass('please-complete'); + } else { + this_sel.removeClass('please-complete'); + } }); } } catch(e) { @@ -339,23 +445,23 @@ var Connection = (function () { try { Console.info('Jappix is now connected.'); - + // Connection markers self.connected = true; self.reconnect_try = 0; self.reconnect_timer = 0; - + // We hide the home page $('#home').hide(); - + // Any suggest to do before triggering connected event? Groupchat.suggestCheck(); - + // Remove the waiting item Interface.removeGeneralWait(); - // Init Jingle - Jingle.init(); + // Init call + Call.init(); } catch(e) { Console.error('Connection.handleConnected', e); } @@ -374,27 +480,28 @@ var Connection = (function () { // Not resumed? if(!self.resume) { // Remember the session? - if(DataStore.getDB(self.desktop_hash, 'remember', 'session')) + if(DataStore.getDB(self.desktop_hash, 'remember', 'session')) { DataStore.setPersistent('global', 'session', 1, self.current_session); - + } + // We show the chatting app. Talk.create(); - + // We reset the homepage Home.change('default'); - + // We get all the other things self.getEverything(); - + // Set last activity stamp DateUtils.last_activity = DateUtils.getTimeStamp(); } - + // Resumed else { // Send our presence Presence.sendActions(); - + // Change the title Interface.updateTitle(); } @@ -414,7 +521,10 @@ var Connection = (function () { try { Console.info('Jappix is now disconnected.'); - + + // Abort ongoing call (if any) + Call.stop(true); + // Normal disconnection if(!self.current_session && !self.connected) { Talk.destroy(); @@ -431,24 +541,32 @@ var Connection = (function () { * Setups the normal connection * @public * @param {object} con - * @param {object} oExtend + * @param {object} extend_obj * @return {undefined} */ - self.setupCon = function(con, oExtend) { + self.setupCon = function(con, extend_obj) { try { - // Setup connection handlers - con.registerHandler('message', Message.handle); - con.registerHandler('presence', Presence.handle); - con.registerHandler('iq', IQ.handle); - con.registerHandler('onconnect', self.handleConnected); - con.registerHandler('onerror', Errors.handle); - con.registerHandler('ondisconnect', self.handleDisconnected); - + var connection_handlers = { + 'message': Message.handle, + 'presence': Presence.handle, + 'iq': IQ.handle, + 'onconnect': self.handleConnected, + 'onerror': Errors.handle, + 'ondisconnect': self.handleDisconnected + }; + + for(var cur_handler in connection_handlers) { + con.registerHandler( + cur_handler, + connection_handlers[cur_handler] + ); + } + // Extended handlers - oExtend = oExtend || {}; - - jQuery.each(oExtend, function(keywd,funct) { + extend_obj = extend_obj || {}; + + jQuery.each(extend_obj, function(keywd,funct) { con.registerHandler(keywd, funct); }); } catch(e) { @@ -496,13 +614,13 @@ var Connection = (function () { if(Common.isConnected()) { // Clear temporary session storage self.resetConMarkers(); - + // Show the waiting item (useful if BOSH is sloooow) Interface.showGeneralWait(); - + // Change the page title Interface.title('wait'); - + // Disconnect from the XMPP server self.logout(); } @@ -525,13 +643,13 @@ var Connection = (function () { if(!Common.isConnected()) { return; } - + // We show the waiting image Interface.showGeneralWait(); - + // Change the page title Interface.title('wait'); - + // We disconnect from the XMPP server self.logout(); } catch(e) { @@ -551,60 +669,34 @@ var Connection = (function () { try { Console.error('This is not a normal disconnection, show the reconnect pane...'); - + // Reconnect pane not yet displayed? if(!Common.exists('#reconnect')) { // Blur the focused input/textarea/select $('input, select, textarea').blur(); - + // Create the HTML code - var html = '
' + - '
' + - Common._e("Due to a network issue, you were disconnected. What do you want to do now?"); - + var html = '
' + + '
' + + Common._e("Due to a network issue, you were disconnected. What do you want to do now?"); + // Can we cancel reconnection? - if(mode == 'normal') + if(mode == 'normal') { html += '' + Common._e("Cancel") + ''; - - html += '' + Common._e("Reconnect") + '' + - '
'; - + } + + html += '' + Common._e("Reconnect") + '' + + '
'; + // Append the code $('body').append(html); - - // Click events - if(mode == 'normal') - $('#reconnect a.finish.cancel').click(function() { - return self.cancelReconnect(); - }); - - $('#reconnect a.finish.reconnect').click(function() { - return self.acceptReconnect(mode); - }); - - // Try to reconnect automatically after a while - if(self.reconnect_try < 5) - self.reconnect_timer = 5 + (5 * self.reconnect_try); - else - self.reconnect_timer = 120; - - // Change the try number - self.reconnect_try++; - - // Fire the event! - $('#reconnect a.finish.reconnect').everyTime('1s', function() { - // We can reconnect! - if(self.reconnect_timer === 0) - return self.acceptReconnect(mode); - - // Button text - if(self.reconnect_timer <= 10) - $(this).text(Common._e("Reconnect") + ' (' + self.reconnect_timer + ')'); - - // Remove 1 second - self.reconnect_timer--; - }); - + + // Attach events + self._eventsReconnect(mode); + + // Schedule next reconnect + self._scheduleReconnect(mode); + // Page title Interface.updateTitle(); } @@ -625,30 +717,33 @@ var Connection = (function () { try { Console.info('Trying to reconnect the user...'); - + // Resume marker self.resume = true; - + // Show waiting item Interface.showGeneralWait(); - + // Reset some various stuffs var groupchats = '#page-engine .page-engine-chan[data-type="groupchat"]'; - $(groupchats + ' .list .role').hide(); - $(groupchats + ' .one-group, ' + groupchats + ' .list .user').remove(); - $(groupchats).attr('data-initial', 'false'); - + var groupchats_sel = $(groupchats); + + groupchats_sel.find('.list .role').hide(); + groupchats_sel.find('.one-group, ' + groupchats + ' .list .user').remove(); + groupchats_sel.attr('data-initial', 'false'); + // Stop the timer $('#reconnect a.finish.reconnect').stopTime(); - + // Remove the reconnect pane $('#reconnect').remove(); - + // Try to login again - if(mode == 'normal') + if(mode == 'normal') { self.loginFromSession(Common.XMLFromString(self.current_session)); - else if(mode == 'anonymous') + } else if(mode == 'anonymous') { Anonymous.login(HOST_ANONYMOUS); + } } catch(e) { Console.error('Connection.acceptReconnect', e); } finally { @@ -667,16 +762,16 @@ var Connection = (function () { try { Console.info('User has canceled automatic reconnection...'); - + // Stop the timer $('#reconnect a.finish.reconnect').stopTime(); - + // Remove the reconnect pane $('#reconnect').remove(); - + // Destroy the talk page Talk.destroy(); - + // Renitialize the previous session parameters self.resetConMarkers(); } catch(e) { @@ -698,7 +793,7 @@ var Connection = (function () { try { // Clear temporary storage self.resetConMarkers(); - + // Clear persistent storage if($(Common.XMLFromString(DataStore.getPersistent('global', 'session', 1))).find('stored').text() == 'true') { DataStore.removePersistent('global', 'session', 1); @@ -741,7 +836,7 @@ var Connection = (function () { try { // Select the data var session = $(data); - + // Fire the login event self.doLogin( session.find('username').text(), @@ -768,10 +863,10 @@ var Connection = (function () { try { // Reset our database self.clearLastSession(); - + // We quit the current session self.quit(); - + // We show an info Board.openThisInfo(3); } catch(e) { @@ -817,16 +912,23 @@ var Connection = (function () { try { // Generate a session XML to be stored - session_xml = 'true' + lServer.htmlEnc() + '' + lNick.htmlEnc() + '' + lResource.htmlEnc() + '' + lPass.htmlEnc() + '' + lPriority.htmlEnc() + ''; - + session_xml = '' + + 'true' + + '' + lServer.htmlEnc() + '' + + '' + lNick.htmlEnc() + '' + + '' + lResource.htmlEnc() + '' + + '' + lPass.htmlEnc() + '' + + '' + lPriority.htmlEnc() + '' + + ''; + // Save the session parameters (for reconnect if network issue) self.current_session = session_xml; - + // Remember me? if(lRemember) { DataStore.setDB(self.desktop_hash, 'remember', 'session', 1); } - + return session_xml; } catch(e) { Console.error('Connection.storeSession', e); @@ -846,11 +948,12 @@ var Connection = (function () { $(document).ready(function() { // Logouts when Jappix is closed $(window).bind('beforeunload', Connection.terminate); - + // Nothing to do when anonymous! - if(Utils.isAnonymous()) + if(Utils.isAnonymous()) { return; - + } + // Connection params submitted in URL? if(XMPPLinks.links_var.u && XMPPLinks.links_var.q) { // Generate login data @@ -861,51 +964,56 @@ var Connection = (function () { var login_resource = JAPPIX_RESOURCE + ' (' + (new Date()).getTime() + ')'; var login_priority = '10'; var login_remember = 1; - + // Must store session? if(XMPPLinks.links_var.h && (XMPPLinks.links_var.h === '1')) { // Store session - var session_xml = self.storeSession(login_nick, login_server, login_pwd, login_resource, login_priority, true); + var session_xml = self.storeSession( + login_nick, + login_server, + login_pwd, + login_resource, + login_priority, + true + ); + DataStore.setPersistent('global', 'session', 1, session_xml); - + // Redirect to a clean URL document.location.href = './'; } else { // Hide the homepage $('#home').hide(); - + // Show the waiting icon Interface.showGeneralWait(); - + // Proceed login self.doLogin(login_nick, login_server, login_pwd, login_resource, login_priority, login_remember); } - + return; } - + // Try to resume a stored session, if not anonymous var session = Common.XMLFromString( DataStore.getPersistent('global', 'session', 1) ); - + if($(session).find('stored').text() == 'true') { // Hide the homepage $('#home').hide(); - + // Show the waiting icon Interface.showGeneralWait(); - + // Login! self.loginFromSession(session); - + Console.info('Saved session found, resuming it...'); - } - - // Not connected, maybe a XMPP link is submitted? - else if((parent.location.hash != '#OK') && XMPPLinks.links_var.x) { + } else if((parent.location.hash != '#OK') && XMPPLinks.links_var.x) { Home.change('loginer'); - + Console.info('A XMPP link is set, switch to login page.'); } }); diff --git a/source/app/javascripts/constants.js b/source/app/javascripts/constants.js index 1e55ac5..69b71d1 100644 --- a/source/app/javascripts/constants.js +++ b/source/app/javascripts/constants.js @@ -11,103 +11,109 @@ Authors: Stefan Strigler, Valérian Saliou, Kloadut, Maranda */ // XMPP XMLNS attributes -var NS_PROTOCOL = 'http://jabber.org/protocol/'; -var NS_FEATURES = 'http://jabber.org/features/'; -var NS_CLIENT = 'jabber:client'; -var NS_IQ = 'jabber:iq:'; -var NS_X = 'jabber:x:'; -var NS_IETF = 'urn:ietf:params:xml:ns:'; -var NS_IETF_XMPP = NS_IETF + 'xmpp-'; -var NS_XMPP = 'urn:xmpp:'; +var NS_PROTOCOL = 'http://jabber.org/protocol/'; +var NS_FEATURES = 'http://jabber.org/features/'; +var NS_CLIENT = 'jabber:client'; +var NS_IQ = 'jabber:iq:'; +var NS_X = 'jabber:x:'; +var NS_IETF = 'urn:ietf:params:xml:ns:'; +var NS_IETF_XMPP = NS_IETF + 'xmpp-'; +var NS_XMPP = 'urn:xmpp:'; -var NS_STORAGE = 'storage:'; -var NS_BOOKMARKS = NS_STORAGE + 'bookmarks'; -var NS_ROSTERNOTES = NS_STORAGE + 'rosternotes'; +var NS_STORAGE = 'storage:'; +var NS_BOOKMARKS = NS_STORAGE + 'bookmarks'; +var NS_ROSTERNOTES = NS_STORAGE + 'rosternotes'; -var NS_JAPPIX = 'jappix:'; -var NS_INBOX = NS_JAPPIX + 'inbox'; -var NS_OPTIONS = NS_JAPPIX + 'options'; +var NS_JAPPIX = 'jappix:'; +var NS_INBOX = NS_JAPPIX + 'inbox'; +var NS_OPTIONS = NS_JAPPIX + 'options'; -var NS_DISCO_ITEMS = NS_PROTOCOL + 'disco#items'; -var NS_DISCO_INFO = NS_PROTOCOL + 'disco#info'; -var NS_VCARD = 'vcard-temp'; -var NS_VCARD_P = NS_VCARD + ':x:update'; -var NS_IETF_VCARD4 = NS_IETF + 'vcard-4.0'; -var NS_XMPP_VCARD4 = NS_XMPP + 'vcard4'; -var NS_URN_ADATA = NS_XMPP + 'avatar:data'; -var NS_URN_AMETA = NS_XMPP + 'avatar:metadata'; -var NS_AUTH = NS_IQ + 'auth'; -var NS_AUTH_ERROR = NS_IQ + 'auth:error'; -var NS_REGISTER = NS_IQ + 'register'; -var NS_SEARCH = NS_IQ + 'search'; -var NS_ROSTER = NS_IQ + 'roster'; -var NS_PRIVACY = NS_IQ + 'privacy'; -var NS_PRIVATE = NS_IQ + 'private'; -var NS_VERSION = NS_IQ + 'version'; -var NS_TIME = NS_IQ + 'time'; -var NS_LAST = NS_IQ + 'last'; -var NS_IQDATA = NS_IQ + 'data'; -var NS_XDATA = NS_X + 'data'; -var NS_IQOOB = NS_IQ + 'oob'; -var NS_XOOB = NS_X + 'oob'; -var NS_DELAY = NS_X + 'delay'; -var NS_EXPIRE = NS_X + 'expire'; -var NS_EVENT = NS_X + 'event'; -var NS_XCONFERENCE = NS_X + 'conference'; -var NS_STATS = NS_PROTOCOL + 'stats'; -var NS_MUC = NS_PROTOCOL + 'muc'; -var NS_MUC_USER = NS_MUC + '#user'; -var NS_MUC_ADMIN = NS_MUC + '#admin'; -var NS_MUC_OWNER = NS_MUC + '#owner'; -var NS_MUC_CONFIG = NS_MUC + '#roomconfig'; -var NS_PUBSUB = NS_PROTOCOL + 'pubsub'; -var NS_PUBSUB_EVENT = NS_PUBSUB + '#event'; -var NS_PUBSUB_OWNER = NS_PUBSUB + '#owner'; -var NS_PUBSUB_NMI = NS_PUBSUB + '#node-meta-info'; -var NS_PUBSUB_NC = NS_PUBSUB + '#node_config'; -var NS_PUBSUB_CN = NS_PUBSUB + '#config-node'; -var NS_PUBSUB_RI = NS_PUBSUB + '#retrieve-items'; -var NS_COMMANDS = NS_PROTOCOL + 'commands'; -var NS_BOSH = NS_PROTOCOL + 'httpbind'; +var NS_DISCO_ITEMS = NS_PROTOCOL + 'disco#items'; +var NS_DISCO_INFO = NS_PROTOCOL + 'disco#info'; +var NS_VCARD = 'vcard-temp'; +var NS_VCARD_P = NS_VCARD + ':x:update'; +var NS_IETF_VCARD4 = NS_IETF + 'vcard-4.0'; +var NS_XMPP_VCARD4 = NS_XMPP + 'vcard4'; +var NS_URN_ADATA = NS_XMPP + 'avatar:data'; +var NS_URN_AMETA = NS_XMPP + 'avatar:metadata'; +var NS_AUTH = NS_IQ + 'auth'; +var NS_AUTH_ERROR = NS_IQ + 'auth:error'; +var NS_REGISTER = NS_IQ + 'register'; +var NS_SEARCH = NS_IQ + 'search'; +var NS_ROSTER = NS_IQ + 'roster'; +var NS_PRIVACY = NS_IQ + 'privacy'; +var NS_PRIVATE = NS_IQ + 'private'; +var NS_VERSION = NS_IQ + 'version'; +var NS_TIME = NS_IQ + 'time'; +var NS_LAST = NS_IQ + 'last'; +var NS_IQDATA = NS_IQ + 'data'; +var NS_XDATA = NS_X + 'data'; +var NS_IQOOB = NS_IQ + 'oob'; +var NS_XOOB = NS_X + 'oob'; +var NS_DELAY = NS_X + 'delay'; +var NS_EXPIRE = NS_X + 'expire'; +var NS_EVENT = NS_X + 'event'; +var NS_XCONFERENCE = NS_X + 'conference'; +var NS_STATS = NS_PROTOCOL + 'stats'; +var NS_MUC = NS_PROTOCOL + 'muc'; +var NS_MUC_USER = NS_MUC + '#user'; +var NS_MUC_ADMIN = NS_MUC + '#admin'; +var NS_MUC_OWNER = NS_MUC + '#owner'; +var NS_MUC_CONFIG = NS_MUC + '#roomconfig'; +var NS_PUBSUB = NS_PROTOCOL + 'pubsub'; +var NS_PUBSUB_EVENT = NS_PUBSUB + '#event'; +var NS_PUBSUB_OWNER = NS_PUBSUB + '#owner'; +var NS_PUBSUB_NMI = NS_PUBSUB + '#node-meta-info'; +var NS_PUBSUB_NC = NS_PUBSUB + '#node_config'; +var NS_PUBSUB_CN = NS_PUBSUB + '#config-node'; +var NS_PUBSUB_RI = NS_PUBSUB + '#retrieve-items'; +var NS_COMMANDS = NS_PROTOCOL + 'commands'; +var NS_BOSH = NS_PROTOCOL + 'httpbind'; var NS_STREAM = 'http://etherx.jabber.org/streams'; -var NS_URN_TIME = NS_XMPP + 'time'; -var NS_URN_PING = NS_XMPP + 'ping'; -var NS_URN_MBLOG = NS_XMPP + 'microblog:0'; -var NS_URN_INBOX = NS_XMPP + 'inbox'; -var NS_URN_FORWARD = NS_XMPP + 'forward:0'; -var NS_URN_MAM = NS_XMPP + 'mam:tmp'; -var NS_URN_DELAY = NS_XMPP + 'delay'; -var NS_URN_RECEIPTS = NS_XMPP + 'receipts'; -var NS_URN_CARBONS = NS_XMPP + 'carbons:2'; -var NS_RSM = NS_PROTOCOL + 'rsm'; -var NS_IPV6 = 'ipv6'; -var NS_XHTML = 'http://www.w3.org/1999/xhtml'; -var NS_XHTML_IM = NS_PROTOCOL + 'xhtml-im'; -var NS_CHATSTATES = NS_PROTOCOL + 'chatstates'; -var NS_HTTP_AUTH = NS_PROTOCOL + 'http-auth'; -var NS_ROSTERX = NS_PROTOCOL + 'rosterx'; -var NS_MOOD = NS_PROTOCOL + 'mood'; -var NS_ACTIVITY = NS_PROTOCOL + 'activity'; -var NS_TUNE = NS_PROTOCOL + 'tune'; -var NS_GEOLOC = NS_PROTOCOL + 'geoloc'; -var NS_NICK = NS_PROTOCOL + 'nick'; -var NS_NOTIFY = '+notify'; -var NS_CAPS = NS_PROTOCOL + 'caps'; -var NS_ATOM = 'http://www.w3.org/2005/Atom'; +var NS_URN_TIME = NS_XMPP + 'time'; +var NS_URN_PING = NS_XMPP + 'ping'; +var NS_URN_MBLOG = NS_XMPP + 'microblog:0'; +var NS_URN_INBOX = NS_XMPP + 'inbox'; +var NS_URN_FORWARD = NS_XMPP + 'forward:0'; +var NS_URN_MAM = NS_XMPP + 'mam:tmp'; +var NS_URN_DELAY = NS_XMPP + 'delay'; +var NS_URN_RECEIPTS = NS_XMPP + 'receipts'; +var NS_URN_CARBONS = NS_XMPP + 'carbons:2'; +var NS_URN_CORRECT = NS_XMPP + 'message-correct:0'; +var NS_URN_IDLE = NS_XMPP + 'idle:1'; +var NS_URN_REACH = NS_XMPP + 'reach:0'; +var NS_URN_MARKERS = NS_XMPP + 'chat-markers:0'; +var NS_URN_ATTENTION = NS_XMPP + 'attention:0'; +var NS_URN_HINTS = NS_XMPP + 'hints'; +var NS_RSM = NS_PROTOCOL + 'rsm'; +var NS_IPV6 = 'ipv6'; +var NS_XHTML = 'http://www.w3.org/1999/xhtml'; +var NS_XHTML_IM = NS_PROTOCOL + 'xhtml-im'; +var NS_CHATSTATES = NS_PROTOCOL + 'chatstates'; +var NS_HTTP_AUTH = NS_PROTOCOL + 'http-auth'; +var NS_ROSTERX = NS_PROTOCOL + 'rosterx'; +var NS_MOOD = NS_PROTOCOL + 'mood'; +var NS_ACTIVITY = NS_PROTOCOL + 'activity'; +var NS_TUNE = NS_PROTOCOL + 'tune'; +var NS_GEOLOC = NS_PROTOCOL + 'geoloc'; +var NS_NICK = NS_PROTOCOL + 'nick'; +var NS_NOTIFY = '+notify'; +var NS_CAPS = NS_PROTOCOL + 'caps'; +var NS_ATOM = 'http://www.w3.org/2005/Atom'; -var NS_STANZAS = NS_IETF_XMPP + 'stanzas'; -var NS_STREAMS = NS_IETF_XMPP + 'streams'; +var NS_STANZAS = NS_IETF_XMPP + 'stanzas'; +var NS_STREAMS = NS_IETF_XMPP + 'streams'; -var NS_TLS = NS_IETF_XMPP + 'tls'; -var NS_SASL = NS_IETF_XMPP + 'sasl'; -var NS_SESSION = NS_IETF_XMPP + 'session'; -var NS_BIND = NS_IETF_XMPP + 'bind'; +var NS_TLS = NS_IETF_XMPP + 'tls'; +var NS_SASL = NS_IETF_XMPP + 'sasl'; +var NS_SESSION = NS_IETF_XMPP + 'session'; +var NS_BIND = NS_IETF_XMPP + 'bind'; -var NS_FEATURE_IQAUTH = NS_FEATURES + 'iq-auth'; +var NS_FEATURE_IQAUTH = NS_FEATURES + 'iq-auth'; var NS_FEATURE_IQREGISTER = NS_FEATURES + 'iq-register'; -var NS_FEATURE_COMPRESS = NS_FEATURES + 'compress'; +var NS_FEATURE_COMPRESS = NS_FEATURES + 'compress'; -var NS_COMPRESS = NS_PROTOCOL + 'compress'; +var NS_COMPRESS = NS_PROTOCOL + 'compress'; var NS_METRONOME_MAM_PURGE = 'http://metronome.im/protocol/mam-purge'; @@ -181,7 +187,7 @@ function STANZA_ERROR(code, type, cond) { if(window == this) { return new STANZA_ERROR(code, type, cond); } - + this.code = code; this.type = type; this.cond = cond; diff --git a/source/app/javascripts/correction.js b/source/app/javascripts/correction.js new file mode 100644 index 0000000..deea70c --- /dev/null +++ b/source/app/javascripts/correction.js @@ -0,0 +1,509 @@ +/* + +Jappix - An open social platform +Implementation of XEP-0308: Last Message Correction + +------------------------------------------------- + +License: AGPL +Author: Valérian Saliou + +*/ + +// Bundle +var Correction = (function () { + + /** + * Alias of this + * @private + */ + var self = {}; + + + /** + * @private + * @param {string} xid + * @return {boolean} + */ + self._hasSupport = function(xid) { + + var support = false; + + try { + if($('#' + hex_md5(xid) + '[data-correction="true"]').size()) { + support = true; + } + } catch(e) { + Console.error('Correction._hasSupport', e); + } finally { + return support; + } + + }; + + + /** + * @private + * @param {string} xid + * @return {string} + */ + self._getLastID = function(xid) { + + var last_id = null; + + try { + if(self._hasSupport(xid) === true) { + // Check last message from ourselves + last_id = $('#' + hex_md5(xid) + ' .content .one-line.user-message[data-mode="me"]:last').attr('data-id') || null; + } + } catch(e) { + Console.error('Correction._getLastID', e); + } finally { + return last_id; + } + + }; + + + /** + * @private + * @param {string} xid + * @return {string} + */ + self._getCurrentID = function(xid) { + + var current_id = null; + + try { + if(self._hasSupport(xid) === true) { + // Check the ID of the message being edited (if any) + current_id = $('#' + hex_md5(xid) + ' .message-area').attr('data-correction-current') || null; + } + } catch(e) { + Console.error('Correction._getCurrentID', e); + } finally { + return current_id; + } + + }; + + + /** + * @private + * @param {string} xid + * @return {object} + */ + self._getLastMessage = function(xid) { + + var last_message_val = null; + var last_message_sel = null; + + try { + if(self._hasSupport(xid) === true) { + // Check last message from ourselves + last_message_sel = $('#' + hex_md5(xid) + ' .content .one-line.user-message[data-mode="me"]:last'); + last_message_val = last_message_sel.find('.message-content').text() || null; + + if(last_message_val === null) { + last_message_sel = null; + } + } + } catch(e) { + Console.error('Correction._getLastMessage', e); + } finally { + return { + 'value': last_message_val, + 'selector': last_message_sel + }; + } + + }; + + + /** + * @private + * @param {string} xid + * @param {object} message_sel + * @return {undefined} + */ + self._bindInterface = function(xid, message_sel) { + + try { + // Add message area elements + var text_sel = $('#' + hex_md5(xid) + ' .text'); + + text_sel.addClass('correction-active'); + text_sel.prepend( + '
' + + '' + Common._e("Editing") + '' + + '' + Common._e("Cancel") + '' + + '
' + ); + + // Add message correction marker + message_sel.addClass('correction-active'); + message_sel.find('.correction-label').remove(); + message_sel.find('.correction-edit').hide(); + + message_sel.append( + '' + + Common._e("Being edited") + + '' + ); + + // Bind click events + text_sel.find('.correction-cancel').click(function() { + self.leave(xid); + return false; + }); + } catch(e) { + Console.error('Correction._bindInterface', e); + } + + }; + + + /** + * @private + * @param {string} xid + * @param {object} message_sel + * @return {undefined} + */ + self._unbindInterface = function(xid, message_sel) { + + try { + // Remove message area elements + var text_sel = $('#' + hex_md5(xid) + ' .text'); + text_sel.removeClass('correction-active'); + text_sel.find('.correction-toolbox, .correction-label').remove(); + + if(message_sel.size()) { + message_sel.find('.correction-edit').css('display', ''); + + // Remove message correction marker + message_sel.removeClass('correction-active'); + message_sel.find('.correction-label').remove(); + } + } catch(e) { + Console.error('Correction._unbindInterface', e); + } + + }; + + + /** + * @private + * @param {string} xid + * @param {string} full_xid + * @param {string} type + * @param {string} message_id + * @param {string} message_body + * @return {string} + */ + self._sendStanza = function(xid, full_xid, type, message_id, message_body) { + + var args = { + 'id': null, + 'xhtml': false, + 'message': null + }; + + try { + var hash = hex_md5(xid); + var id = genID(); + args.id = id; + + // Initialize message stanza + var message = new JSJaCMessage(); + args.message = message; + + message.setType(type); + message.setTo(full_xid); + message.setID(id); + + // Generates the correct message depending of the choosen style + var generate_message = Message.generate(message, message_body, hash); + args.xhtml = (generate_message === 'XHTML'); + + // Receipt request + var receipt_request = Receipts.request(hash); + + if(receipt_request) { + message.appendNode('request', {'xmlns': NS_URN_RECEIPTS}); + } + + // Chatstate + message.appendNode('active', {'xmlns': NS_CHATSTATES}); + + if(message_id !== null) { + message.appendNode('replace', { + 'xmlns': NS_URN_CORRECT, + 'id': message_id + }); + } + + con.send(message, Errors.handleReply); + } catch(e) { + Console.error('Correction._sendStanza', e); + } finally { + return args; + } + + }; + + + /** + * Detects correction mode request (in input) + * @public + * @param {string} xid + * @param {object} input_sel + * @return {undefined} + */ + self.detect = function(xid, input_sel) { + + try { + // Other keys + if(input_sel.val().match(/^\/correct/) && self.isIn(xid) === false) { + // Enter correction mode? + self.enter(xid); + } + } catch(e) { + Console.error('Correction.detect', e); + } + + }; + + + /** + * Enter correction mode (for last message) + * @public + * @param {string} xid + * @return {undefined} + */ + self.enter = function(xid) { + + try { + Console.debug('Correction.enter', 'Requested to enter the correction mode with: ' + xid); + + if(self._hasSupport(xid) === true && self.isIn(xid) === false) { + var last_message = self._getLastMessage(xid); + + if(last_message.value && last_message.selector) { + Console.info('Correction.enter', 'Valid last message found for correction mode with: ' + xid); + + var message_area_sel = $('#' + hex_md5(xid) + ' .message-area'); + message_area_sel.val(last_message.value); + + self._bindInterface( + xid, + last_message.selector + ); + + // Focus hack (to get cursor at the end of textarea) + message_area_sel.oneTime(10, function() { + message_area_sel[0].select(); + message_area_sel[0].selectionStart = message_area_sel[0].selectionEnd; + }); + } + } + } catch(e) { + Console.error('Correction.enter', e); + } + + }; + + + /** + * Leave correction mode + * @public + * @param {string} xid + * @return {undefined} + */ + self.leave = function(xid) { + + try { + if(self.isIn(xid) === true) { + var base_sel = $('#' + hex_md5(xid)); + var active_message_sel = base_sel.find('.content .one-line.user-message.correction-active'); + + self._unbindInterface(xid, active_message_sel); + + var message_area_sel = base_sel.find('.message-area'); + message_area_sel.val(''); + message_area_sel.focus(); + } + } catch(e) { + Console.error('Correction.leave', e); + } + + }; + + + /** + * Send corrected message + * @public + * @param {string} xid + * @param {string} type + * @param {string} replacement + * @return {undefined} + */ + self.send = function(xid, type, replacement) { + + try { + if(self._hasSupport(xid) === true) { + if(self._getLastMessage(xid).value != replacement) { + var own_xid = Common.getXID(); + var hash = hex_md5(xid); + var replace_id = self._getLastID(xid); + + Console.info('Correction.send', 'Sending replacement message for: ' + xid + ' "' + replacement + '" with ID: ' + (replace_id || 'none')); + + // Send the stanza itself + var full_xid = Presence.highestPriority(xid) || xid; + var stanza_args = self._sendStanza( + xid, + full_xid, + type, + replace_id, + replacement + ); + + // Update DOM (for chat only) + if(type == 'chat') { + // Filter the xHTML message (for us!) + var replacement_formatted = replacement; + + if(stanza_args.xhtml) { + replacement_formatted = Filter.xhtml(stanza_args.message.getNode()); + } + + // Remove old message + old_message_sel = $('#' + hash + ' .content .one-line.user-message[data-mode="me"]').filter(function() { + return ($(this).attr('data-id') + '') === (replace_id + ''); + }).filter(':last'); + + var edit_count = old_message_sel.attr('data-edit-count') || 0; + edit_count = isNaN(edit_count) ? 0 : parseInt(edit_count, 10); + + if(type == 'chat') { + old_message_sel.remove(); + } + + // Display edited message + Message.display( + 'chat', + own_xid, + hash, + Name.getBuddy(own_xid).htmlEnc(), + replacement_formatted, + DateUtils.getCompleteTime(), + DateUtils.getTimeStamp(), + 'user-message', + !stanza_args.xhtml, + '', + 'me', + stanza_args.id, + undefined, + undefined, + true, + (edit_count + 1) + ); + } + } + } + } catch(e) { + Console.error('Correction.send', e); + } + + }; + + + /** + * Catches a replace message + * @public + * @param {object} message + * @param {string} hash + * @param {string} type + * @return {object} + */ + self.catch = function(message, hash, type) { + + var edit_results = { + 'has_replace': false, + 'is_edited': false, + 'count': 0, + 'next_count': 0 + }; + + try { + var replace_node = message.getChild('replace', NS_URN_CORRECT); + + if(replace_node) { + edit_results.has_replace = true; + var message_edit_id = $(replace_node).attr('id'); + + if(typeof message_edit_id != 'undefined') { + var message_edit_sel = $('#' + hash + ' .one-line.user-message').filter(function() { + var this_sel = $(this); + var is_valid_mode = true; + + if(type == 'chat') { + is_valid_mode = true ? this_sel.attr('data-mode') == 'him' : false; + } + + return is_valid_mode && ((this_sel.attr('data-id') + '') === (message_edit_id + '')); + }).filter(':last'); + + if(message_edit_sel.size()) { + edit_results.count = message_edit_sel.attr('data-edit-count') || 0; + edit_results.count = isNaN(edit_results.count) ? 0 : parseInt(edit_results.count, 10); + edit_results.next_count = edit_results.count + 1; + edit_results.is_edited = true; + + // Empty group? + var message_edit_group_sel = message_edit_sel.parents('.one-group'); + + if(message_edit_group_sel.find('.one-line').size() <= 1) { + message_edit_group_sel.remove(); + } else { + message_edit_sel.remove(); + } + } + } + } + } catch(e) { + Console.error('Correction.catch', e); + } finally { + return edit_results; + } + + }; + + + /** + * Returns whether we are in correction mode or not + * @public + * @param {string} xid + * @return {boolean} + */ + self.isIn = function(xid) { + + var is_in = false; + + try { + is_in = $('#' + hex_md5(xid) + ' .text').hasClass('correction-active'); + } catch(e) { + Console.error('Correction.isIn', e); + } finally { + return is_in; + } + + }; + + + /** + * Return class scope + */ + return self; + +})(); diff --git a/source/app/javascripts/dataform.js b/source/app/javascripts/dataform.js index cd5ceed..b409768 100644 --- a/source/app/javascripts/dataform.js +++ b/source/app/javascripts/dataform.js @@ -1,1160 +1,1200 @@ -/* - -Jappix - An open social platform -These are the dataform JS scripts for Jappix - -------------------------------------------------- - -License: AGPL -Authors: Valérian Saliou, Maranda - -*/ - -// Bundle -var DataForm = (function () { - - /** - * Alias of this - * @private - */ - var self = {}; - - - /** - * Gets the defined dataform elements - * @public - * @param {string} host - * @param {string} type - * @param {string} node - * @param {string} action - * @param {string} target - * @return {boolean} - */ - self.go = function(host, type, node, action, target) { - - try { - // Clean the current session - self.clean(target); - - // We tell the user that a search has been launched - $('#' + target + ' .wait').show(); - - // If we have enough data - if(host && type) { - // Generate a session ID - var sessionID = Math.round(100000.5 + (((900000.49999) - (100000.5)) * Math.random())); - var id = target + '-' + sessionID + '-' + genID(); - $('.' + target + '-results').attr('data-session', target + '-' + sessionID); - - // We request the service item - var iq = new JSJaCIQ(); - iq.setID(id); - iq.setTo(host); - iq.setType('get'); - - // MUC admin query - if(type == 'muc') { - iq.setQuery(NS_MUC_OWNER); - con.send(iq, self.handleMUC); - } - - // Browse query - else if(type == 'browse') { - var iqQuery = iq.setQuery(NS_DISCO_ITEMS); - - if(node) - iqQuery.setAttribute('node', node); - - con.send(iq, self.handleBrowse); - } - - // Command - else if(type == 'command') { - var items; - - if(node) - items = iq.appendNode('command', {'node': node, 'xmlns': NS_COMMANDS}); - - else { - items = iq.setQuery(NS_DISCO_ITEMS); - items.setAttribute('node', NS_COMMANDS); - } - - if(action && node) { - iq.setType('set'); - items.setAttribute('action', action); - } - - con.send(iq, self.handleCommand); - } - - // Search query - else if(type == 'search') { - iq.setQuery(NS_SEARCH); - con.send(iq, self.handleSearch); - } - - // Subscribe query - else if(type == 'subscribe') { - iq.setQuery(NS_REGISTER); - con.send(iq, self.handleSubscribe); - } - - // Join - else if(type == 'join') { - if(target == 'discovery') - Discovery.close(); - - Chat.checkCreate(host, 'groupchat'); - } - } - } catch(e) { - Console.error('DataForm.go', e); - } finally { - return false; - } - - }; - - - /** - * Sends a given dataform - * @public - * @param {string} type - * @param {string} action - * @param {string} x_type - * @param {string} id - * @param {string} xid - * @param {string} node - * @param {string} sessionid - * @param {string} target - * @return {boolean} - */ - self.send = function(type, action, x_type, id, xid, node, sessionid, target) { - - try { - // Path - var pathID = '#' + target + ' .results[data-session="' + id + '"]'; - - // New IQ - var iq = new JSJaCIQ(); - iq.setTo(xid); - iq.setType('set'); - - // Set the correct query - var query; - - if(type == 'subscribe') - iqQuery = iq.setQuery(NS_REGISTER); - else if(type == 'search') - iqQuery = iq.setQuery(NS_SEARCH); - else if(type == 'command') - iqQuery = iq.appendNode('command', {'xmlns': NS_COMMANDS, 'node': node, 'sessionid': sessionid, 'action': action}); - else if(type == 'x') - iqQuery = iq.setQuery(NS_MUC_OWNER); - - // Build the XML document - if(action != 'cancel') { - // No X node - if(Common.exists('input.register-special') && (type == 'subscribe')) { - $('input.register-special').each(function() { - var iName = $(this).attr('name'); - var iValue = $(this).val(); - - iqQuery.appendChild(iq.buildNode(iName, {'xmlns': NS_REGISTER}, iValue)); - }); - } - - // Can create the X node - else { - var iqX = iqQuery.appendChild(iq.buildNode('x', {'xmlns': NS_XDATA, 'type': x_type})); - - // Each input - $(pathID + ' .oneresult input, ' + pathID + ' .oneresult textarea, ' + pathID + ' .oneresult select').each(function() { - // Get the current input value - var iVar = $(this).attr('name'); - var iType = $(this).attr('data-type'); - var iValue = $(this).val(); - - // Build a new field node - var field = iqX.appendChild(iq.buildNode('field', {'var': iVar, 'type': iType, 'xmlns': NS_XDATA})); - - // Boolean input? - if(iType == 'boolean') { - if($(this).filter(':checked').size()) - iValue = '1'; - else - iValue = '0'; - } - - // JID-multi input? - if(iType == 'jid-multi') { - // Values array - var xid_arr = [iValue]; - var xid_check = []; - - // Try to split it - if(iValue.indexOf(',') != -1) - xid_arr = iValue.split(','); - - // Append each value to the XML document - for(var i in xid_arr) { - // Get the current value - xid_current = $.trim(xid_arr[i]); - - // No current value? - if(!xid_current) - continue; - - // Add the current value - if(!Utils.existArrayValue(xid_check, xid_current)) { - xid_check.push(xid_current); - field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, xid_current)); - } - } - } - - // List-multi selector? - else if(iType == 'list-multi') { - // Any value? - if(iValue && iValue.length) { - for(var j in iValue) { - field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, iValue[j])); - } - } - } - - // Other inputs? - else - field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, iValue)); - }); - } - } - - // Clean the current session - self.clean(target); - - // Show the waiting item - $('#' + target + ' .wait').show(); - - // Change the ID of the current discovered item - var iqID = target + '-' + genID(); - $('#' + target + ' .' + target + '-results').attr('data-session', iqID); - iq.setID(iqID); - - // Send the IQ - if(type == 'subscribe') - con.send(iq, self.handleSubscribe); - else if(type == 'search') - con.send(iq, self.handleSearch); - else if(type == 'command') - con.send(iq, self.handleCommand); - else - con.send(iq); - } catch(e) { - Console.error('DataForm.send', e); - } finally { - return false; - } - - }; - - - /** - * Displays the good dataform buttons - * @public - * @param {string} type - * @param {string} action - * @param {string} id - * @param {string} xid - * @param {string} node - * @param {string} sessionid - * @param {string} target - * @param {string} pathID - * @return {undefined} - */ - self.buttons = function(type, action, id, xid, node, sessionid, target, pathID) { - - try { - // No need to use buttons? - if(type == 'muc') - return; - - // Override the "undefined" output - if(!id) - id = ''; - if(!xid) - xid = ''; - if(!node) - node = ''; - if(!sessionid) - sessionid = ''; - - // We generate the buttons code - var buttonsCode = '
'; - - if(action == 'submit') { - if((target == 'adhoc') && (type == 'command')) { - buttonsCode += '' + Common._e("Submit") + ''; - - // When keyup on one text input - $(pathID + ' input').keyup(function(e) { - if(e.keyCode == 13) { - self.send(type, 'execute', 'submit', id, xid, node, sessionid, target); - - return false; - } - }); - } - - else { - buttonsCode += '' + Common._e("Submit") + ''; - - // When keyup on one text input - $(pathID + ' input').keyup(function(e) { - if(e.keyCode == 13) { - self.send(type, 'submit', 'submit', id, xid, node, sessionid, target); - - return false; - } - }); - } - } - - if((action == 'submit') && (type != 'subscribe') && (type != 'search')) - buttonsCode += '' + Common._e("Cancel") + ''; - - if(((action == 'back') || (type == 'subscribe') || (type == 'search')) && (target == 'discovery')) - buttonsCode += '' + Common._e("Close") + ''; - - if((action == 'back') && ((target == 'welcome') || (target == 'directory'))) - buttonsCode += '' + Common._e("Previous") + ''; - - if((action == 'back') && (target == 'adhoc')) - buttonsCode += '' + Common._e("Previous") + ''; - - buttonsCode += '
'; - - // We display the buttons code - $(pathID).append(buttonsCode); - - // If no submit link, lock the form - if(!Common.exists(pathID + ' a.submit')) - $(pathID + ' input, ' + pathID + ' textarea').attr('readonly', true); - } catch(e) { - Console.error('DataForm.buttons', e); - } - - }; - - - /** - * Handles the MUC dataform - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleMUC = function(iq) { - - try { - Errors.handleReply(iq); - self.handleContent(iq, 'muc'); - } catch(e) { - Console.error('DataForm.handleMUC', e); - } - - }; - - - /** - * Handles the browse dataform - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleBrowse = function(iq) { - - try { - Errors.handleReply(iq); - self.handleContent(iq, 'browse'); - } catch(e) { - Console.error('DataForm.handleBrowse', e); - } - - }; - - - /** - * Handles the command dataform - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleCommand = function(iq) { - - try { - Errors.handleReply(iq); - self.handleContent(iq, 'command'); - } catch(e) { - Console.error('DataForm.handleCommand', e); - } - - }; - - - /** - * Handles the subscribe dataform - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleSubscribe = function(iq) { - - try { - Errors.handleReply(iq); - self.handleContent(iq, 'subscribe'); - } catch(e) { - Console.error('DataForm.handleSubscribe', e); - } - - }; - - - /** - * Handles the search dataform - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleSearch = function(iq) { - - try { - Errors.handleReply(iq); - self.handleContent(iq, 'search'); - } catch(e) { - Console.error('DataForm.handleSearch', e); - } - - }; - - - /** - * Handles the dataform content - * @public - * @param {object} iq - * @param {string} type - * @return {undefined} - */ - self.handleContent = function(iq, type) { - - try { - // Get the ID - var sID = iq.getID(); - - // Get the target - var splitted = sID.split('-'); - var target = splitted[0]; - var sessionID = target + '-' + splitted[1]; - var from = Common.fullXID(Common.getStanzaFrom(iq)); - var pathID = '#' + target + ' .results[data-session="' + sessionID + '"]'; - - // If an error occured - if(!iq || (iq.getType() != 'result')) - self.noResult(pathID); - - // If we got something okay - else { - var handleXML = iq.getNode(); - - if(type == 'browse') { - if($(handleXML).find('item').attr('jid')) { - // Get the query node - var queryNode = $(handleXML).find('query').attr('node'); - - $(handleXML).find('item').each(function() { - // We parse the received xml - var itemHost = $(this).attr('jid'); - var itemNode = $(this).attr('node'); - var itemName = $(this).attr('name'); - var itemHash = hex_md5(itemHost); - - // Node - if(itemNode) - $(pathID).append( - '
' + - '
' + itemNode.htmlEnc() + '
' + - '
' - ); - - // Item - else if(queryNode && itemName) - $(pathID).append( - '
' + - '
' + itemName.htmlEnc() + '
' + - '
' - ); - - // Item with children - else { - // We display the waiting element - $(pathID + ' .disco-wait .disco-category-title').after( - '
' + - '
' + - '
' + itemHost + '
' + - '
' + Common._e("Requesting this service...") + '
' + - '
' - ); - - // We display the category - $('#' + target + ' .disco-wait').show(); - - // We ask the server what's the service type - self.getType(itemHost, itemNode, sessionID); - } - }); - } - - // Else, there are no items for this query - else - self.noResult(pathID); - } - - else if((type == 'muc') || (type == 'search') || (type == 'subscribe') || ((type == 'command') && $(handleXML).find('command').attr('xmlns'))) { - // Get some values - var xCommand = $(handleXML).find('command'); - var bNode = xCommand.attr('node'); - var bSession = xCommand.attr('sessionid'); - var bStatus = xCommand.attr('status'); - var xRegister = $(handleXML).find('query[xmlns="' + NS_REGISTER + '"]').text(); - var xElement = $(handleXML).find('x'); - - // Search done - if((xElement.attr('type') == 'result') && (type == 'search')) { - var bPath = pathID; - - // Display the result - $(handleXML).find('item').each(function() { - // Have some "flexibility" for what regards field names, it would be better to return the whole original DF - // layout, but on a large amount of result which have many fields, there's a very high chance the browser can - // choke on old systems or new ones even. - - // Search for useful fields, return first result. This is rather hacky, but jQuery is horrible when it comes to - // matching st. using patterns. (TODO: Improve and return the full DF layout without choking the browser) - var bName; - var bCountry; - var doneName, doneCountry; - - $.each($(this).find('field'), function(i, item) - { - var $item = $(item); - if ($(item).attr('var').match(/^(fn|name|[^n][^i][^c][^k]name)$/gi) && doneName !== true) { - bName = $item.children('value:first').text(); - doneName = true; - } else if ($(item).attr('var').match(/^(ctry|country.*)$/gi) && doneCountry !== true) { - bCountry = $item.children('value:first').text(); - doneCountry = true; - } - }); - - var bXID = $(this).find('field[var="jid"] value:first').text(); - var dName = bName; - - // Override "undefined" value - if(!bXID) - bXID = ''; - if(!bName) - bName = Common._e("Unknown name"); - if(!bCountry) - bCountry = Common._e("Unknown country"); - - // User hash - var bHash = hex_md5(bXID); - - // HTML code - var bHTML = '
' + - '
' + - '' + - '
' + - '
' + bName + '
' + - '
' + bCountry + '
' + - '
' + bXID + '
' + - '
'; - - // The buddy is not in our buddy list? - if(!Common.exists('#roster .buddy[data-xid="' + escape(bXID) + '"]')) - bHTML += '' + Common._e("Add") + ''; - - // Chat button, if not in welcome/directory mode - if(target == 'discovery') - bHTML += '' + Common._e("Chat") + ''; - - // Profile button, if not in discovery mode - else - bHTML += '' + Common._e("Profile") + ''; - - // Close the HTML element - bHTML += '
'; - - $(bPath).append(bHTML); - - // Click events - $(bPath + ' .' + bHash + ' a').click(function() { - // Buddy add - if($(this).is('.one-add')) { - $(this).hide(); - - Roster.addThisContact(bXID, dName); - } - - // Buddy chat - if($(this).is('.one-chat')) { - if(target == 'discovery') - Discovery.close(); - - Chat.checkCreate(bXID, 'chat', '', '', dName); - } - - // Buddy profile - if($(this).is('.one-profile')) - UserInfos.open(bXID); - - return false; - }); - - // Get the user's avatar - if(bXID) - Avatar.get(bXID, 'cache', 'true', 'forget'); - }); - - // No result? - if(!$(handleXML).find('item').size()) - self.noResult(pathID); - - // Previous button - self.buttons(type, 'back', sessionID, from, bNode, bSession, target, pathID); - } - - // Command to complete - else if(xElement.attr('xmlns') || ((type == 'subscribe') && xRegister)) { - // We display the elements - self.fill(handleXML, sessionID); - - // We display the buttons - if(bStatus != 'completed') - self.buttons(type, 'submit', sessionID, from, bNode, bSession, target, pathID); - else - self.buttons(type, 'back', sessionID, from, bNode, bSession, target, pathID); - } - - // Command completed or subscription done - else if(((bStatus == 'completed') && (type == 'command')) || (!xRegister && (type == 'subscribe'))) { - // Display the good text - var cNote = $(xCommand).find('note'); - - // Any note? - if(cNote.size()) { - cNote.each(function() { - $(pathID).append( - '
' + $(this).text().htmlEnc() + '
' - ); - }); - } - - // Default text - else - $(pathID).append('
' + Common._e("Your form has been sent.") + '
'); - - // Display the back button - self.buttons(type, 'back', sessionID, from, '', '', target, pathID); - - // Add the gateway to our roster if subscribed - if(type == 'subscribe') - Roster.addThisContact(from); - } - - // Command canceled - else if((bStatus == 'canceled') && (type == 'command')) { - if(target == 'discovery') - Discovery.start(); - else if(target == 'adhoc') - dataForm(from, 'command', '', '', 'adhoc'); - } - - // No items for this query - else - self.noResult(pathID); - } - - else if(type == 'command') { - if($(handleXML).find('item').attr('jid')) { - // We display the elements - $(handleXML).find('item').each(function() { - // We parse the received xml - var itemHost = $(this).attr('jid'); - var itemNode = $(this).attr('node'); - var itemName = $(this).attr('name'); - var itemHash = hex_md5(itemHost); - - // We display the waiting element - $(pathID).prepend( - '
' + - '
' + itemName + '
' + - '
»
' + - '
' - ); - }); - } - - // Else, there are no items for this query - else - self.noResult(pathID); - } - } - - // Focus on the first input - $(document).oneTime(10, function() { - $(pathID + ' input:visible:first').focus(); - }); - - // Hide the wait icon - $('#' + target + ' .wait').hide(); - } catch(e) { - Console.error('DataForm.handleContent', e); - } - - }; - - - /** - * Fills the dataform elements - * @public - * @param {type} xml - * @param {type} id - * @return {boolean} - */ - self.fill = function(xml, id) { - - /* REF: http://xmpp.org/extensions/xep-0004.html */ - - try { - // Initialize new vars - var target = id.split('-')[0]; - var pathID = '#' + target + ' .results[data-session="' + id + '"]'; - var selector, is_dataform; - - // Is it a dataform? - if($(xml).find('x[xmlns="' + NS_XDATA + '"]').size()) - is_dataform = true; - else - is_dataform = false; - - // Determines the good selector to use - if(is_dataform) - selector = $(xml).find('x[xmlns="' + NS_XDATA + '"]'); - else - selector = $(xml); - - // Form title - selector.find('title').each(function() { - $(pathID).append( - '
' + $(this).text().htmlEnc() + '
' - ); - }); - - // Form instructions - selector.find('instructions').each(function() { - $(pathID).append( - '
' + $(this).text().htmlEnc() + '
' - ); - }); - - // Register? - if(!is_dataform) { - // Items to detect - var reg_names = [Common._e("Nickname"), Common._e("Name"), Common._e("Password"), Common._e("E-mail")]; - var reg_ids = ['username', 'name', 'password', 'email']; - - // Append these inputs - $.each(reg_names, function(a) { - selector.find(reg_ids[a]).each(function() { - $(pathID).append( - '
' + - '' + - '' + - '
' - ); - }); - }); - - return false; - } - - // Dataform? - selector.find('field').each(function() { - // We parse the received xml - var type = $(this).attr('type'); - var label = $(this).attr('label'); - var field = $(this).attr('var'); - var value = $(this).find('value:first').text(); - var required = ''; - - // No value? - if(!field) - return; - - // Required input? - if($(this).find('required').size()) - required = ' required=""'; - - // Compatibility fix - if(!label) - label = field; - - if(!type) - type = ''; - - // Generate some values - var input; - var hideThis = ''; - - // Fixed field - if(type == 'fixed') - $(pathID).append('
' + value.htmlEnc() + '
'); - - else { - // Hidden field - if(type == 'hidden') { - hideThis = ' style="display: none;"'; - input = ''; - } - - // Boolean field - else if(type == 'boolean') { - var checked; - - if(value == '1') - checked = 'checked'; - else - checked = ''; - - input = ''; - } - - // List-single/list-multi field - else if((type == 'list-single') || (type == 'list-multi')) { - var multiple = ''; - - // Multiple options? - if(type == 'list-multi') - multiple = ' multiple=""'; - - // Append the select field - input = ''; - } - - // Text-multi field - else if(type == 'text-multi') - input = ''; - - // JID-multi field - else if(type == 'jid-multi') { - // Put the XID into an array - var xid_arr = []; - - $(this).find('value').each(function() { - var cValue = $(this).text(); - - if(!Utils.existArrayValue(xid_arr, cValue)) - xid_arr.push(cValue); - }); - - // Sort the array - xid_arr.sort(); - - // Create the input - var xid_value = ''; - - if(xid_arr.length) { - for(var i in xid_arr) { - // Any pre-value - if(xid_value) - xid_value += ', '; - - // Add the current XID - xid_value += xid_arr[i]; - } - } - - input = ''; - } - - // Other stuffs that are similar - else { - // Text-single field - var iType = 'text'; - - // Text-private field - if(type == 'text-private') - iType = 'password'; - - // JID-single field - else if(type == 'jid-single') - iType = 'email'; - - input = ''; - } - - // Append the HTML markup for this field - $(pathID).append( - '
' + - '' + - input + - '
' - ); - } - }); - } catch(e) { - Console.error('DataForm.fill', e); - } finally { - return false; - } - - }; - - - /** - * Gets the dataform type - * @public - * @param {string} host - * @param {string} node - * @param {string} id - * @return {undefined} - */ - self.getType = function(host, node, id) { - - try { - var iq = new JSJaCIQ(); - iq.setID(id + '-' + genID()); - iq.setTo(host); - iq.setType('get'); - - var iqQuery = iq.setQuery(NS_DISCO_INFO); - - if(node) { - iqQuery.setAttribute('node', node); - } - - con.send(iq, self.handleThisBrowse); - } catch(e) { - Console.error('DataForm.getType', e); - } - - }; - - - /** - * Handles the browse stanza - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleThisBrowse = function(iq) { - - /* REF: http://xmpp.org/registrar/disco-categories.html */ - - try { - var id = iq.getID(); - var splitted = id.split('-'); - var target = splitted[0]; - var sessionID = target + '-' + splitted[1]; - var from = Common.fullXID(Common.getStanzaFrom(iq)); - var hash = hex_md5(from); - var handleXML = iq.getQuery(); - var pathID = '#' + target + ' .results[data-session="' + sessionID + '"]'; - - // We first remove the waiting element - $(pathID + ' .disco-wait .' + hash).remove(); - - if($(handleXML).find('identity').attr('type')) { - var category = $(handleXML).find('identity').attr('category'); - var type = $(handleXML).find('identity').attr('type'); - var named = $(handleXML).find('identity').attr('name'); - - if(named) - gName = named; - else - gName = ''; - - var one, two, three, four, five; - - // Get the features that this entity supports - var findFeature = $(handleXML).find('feature'); - - for(var i in findFeature) { - var current = findFeature.eq(i).attr('var'); - - switch(current) { - case NS_SEARCH: - one = 1; - break; - - case NS_MUC: - two = 1; - break; - - case NS_REGISTER: - three = 1; - break; - - case NS_COMMANDS: - four = 1; - break; - - case NS_DISCO_ITEMS: - five = 1; - break; - - default: - break; - } - } - - var buttons = Array(one, two, three, four, five); - - // We define the toolbox links depending on the supported features - var tools = ''; - var aTools = Array('search', 'join', 'subscribe', 'command', 'browse'); - var bTools = Array(Common._e("Search"), Common._e("Join"), Common._e("Subscribe"), Common._e("Command"), Common._e("Browse")); - - for(var b in buttons) { - if(buttons[b]) { - tools += ''; - } - } - - // As defined in the ref, we detect the type of each category to put an icon - switch(category) { - case 'account': - case 'auth': - case 'automation': - case 'client': - case 'collaboration': - case 'component': - case 'conference': - case 'directory': - case 'gateway': - case 'headline': - case 'hierarchy': - case 'proxy': - case 'pubsub': - case 'server': - case 'store': - break; - - default: - category = 'others'; - } - - // We display the item we found - $(pathID + ' .disco-' + category + ' .disco-category-title').after( - '
' + - '
' + - '
' + from + '
' + - '
' + gName + '
' + - '
' + tools + '
' + - '
' - ); - - // We display the category - $(pathID + ' .disco-' + category).show(); - } - - else { - $(pathID + ' .disco-others .disco-category-title').after( - '
' + - '
' + - '
' + from + '
' + - '
' + Common._e("Service offline or broken") + '
' + - '
' - ); - - // We display the category - $(pathID + ' .disco-others').show(); - } - - // We hide the waiting stuffs if there's no remaining loading items - if(!$(pathID + ' .disco-wait .' + target + '-oneresult').size()) { - $(pathID + ' .disco-wait, #' + target + ' .wait').hide(); - } - } catch(e) { - Console.error('DataForm.handleThisBrowse', e); - } - - }; - - - /** - * Cleans the current data-form popup - * @public - * @param {string} target - * @return {undefined} - */ - self.clean = function(target) { - - try { - if(target == 'discovery') { - Discovery.clean(); - } else { - $('#' + target + ' div.results').empty(); - } - } catch(e) { - Console.error('DataForm.clean', e); - } - - }; - - - /** - * Displays the no result indicator - * @public - * @param {string} path - * @return {undefined} - */ - self.noResult = function(path) { - - try { - $(path).prepend('

' + Common._e("Sorry, but the entity didn't return any result!") + '

'); - } catch(e) { - Console.error('DataForm.noResult', e); - } - - }; - - - /** - * Return class scope - */ - return self; - +/* + +Jappix - An open social platform +These are the dataform JS scripts for Jappix + +------------------------------------------------- + +License: AGPL +Authors: Valérian Saliou, Maranda + +*/ + +// Bundle +var DataForm = (function () { + + /** + * Alias of this + * @private + */ + var self = {}; + + + /** + * Gets the defined dataform elements + * @public + * @param {string} host + * @param {string} type + * @param {string} node + * @param {string} action + * @param {string} target + * @return {boolean} + */ + self.go = function(host, type, node, action, target) { + + try { + // Clean the current session + self.clean(target); + + // We tell the user that a search has been launched + $('#' + target + ' .wait').show(); + + // If we have enough data + if(host && type) { + // Generate a session ID + var sessionID = Math.round(100000.5 + (((900000.49999) - (100000.5)) * Math.random())); + var id = target + '-' + sessionID + '-' + genID(); + $('.' + target + '-results').attr('data-session', target + '-' + sessionID); + + // We request the service item + var iq = new JSJaCIQ(); + iq.setID(id); + iq.setTo(host); + iq.setType('get'); + + // MUC admin query + if(type == 'muc') { + iq.setQuery(NS_MUC_OWNER); + con.send(iq, self.handleMUC); + } + + // Browse query + else if(type == 'browse') { + var iqQuery = iq.setQuery(NS_DISCO_ITEMS); + + if(node) { + iqQuery.setAttribute('node', node); + } + + con.send(iq, self.handleBrowse); + } + + // Command + else if(type == 'command') { + var items; + + if(node) { + items = iq.appendNode('command', {'node': node, 'xmlns': NS_COMMANDS}); + } + + else { + items = iq.setQuery(NS_DISCO_ITEMS); + items.setAttribute('node', NS_COMMANDS); + } + + if(action && node) { + iq.setType('set'); + items.setAttribute('action', action); + } + + con.send(iq, self.handleCommand); + } + + // Search query + else if(type == 'search') { + iq.setQuery(NS_SEARCH); + con.send(iq, self.handleSearch); + } + + // Subscribe query + else if(type == 'subscribe') { + iq.setQuery(NS_REGISTER); + con.send(iq, self.handleSubscribe); + } + + // Join + else if(type == 'join') { + if(target == 'discovery') { + Discovery.close(); + } + + Chat.checkCreate(host, 'groupchat'); + } + } + } catch(e) { + Console.error('DataForm.go', e); + } finally { + return false; + } + + }; + + + /** + * Sends a given dataform + * @public + * @param {string} type + * @param {string} action + * @param {string} x_type + * @param {string} id + * @param {string} xid + * @param {string} node + * @param {string} sessionid + * @param {string} target + * @return {boolean} + */ + self.send = function(type, action, x_type, id, xid, node, sessionid, target) { + + try { + // Path + var pathID = '#' + target + ' .results[data-session="' + id + '"]'; + + // New IQ + var iq = new JSJaCIQ(); + iq.setTo(xid); + iq.setType('set'); + + // Set the correct query + var query; + + if(type == 'subscribe') { + iqQuery = iq.setQuery(NS_REGISTER); + } else if(type == 'search') { + iqQuery = iq.setQuery(NS_SEARCH); + } else if(type == 'command') { + iqQuery = iq.appendNode('command', {'xmlns': NS_COMMANDS, 'node': node, 'sessionid': sessionid, 'action': action}); + } else if(type == 'x') { + iqQuery = iq.setQuery(NS_MUC_OWNER); + } + + // Build the XML document + if(action != 'cancel') { + // No X node + if(Common.exists('input.register-special') && (type == 'subscribe')) { + $('input.register-special').each(function() { + var iName = $(this).attr('name'); + var iValue = $(this).val(); + + iqQuery.appendChild(iq.buildNode(iName, {'xmlns': NS_REGISTER}, iValue)); + }); + } + + // Can create the X node + else { + var iqX = iqQuery.appendChild(iq.buildNode('x', {'xmlns': NS_XDATA, 'type': x_type})); + + // Each input + $(pathID + ' .oneresult input, ' + pathID + ' .oneresult textarea, ' + pathID + ' .oneresult select').each(function() { + // Get the current input value + var iVar = $(this).attr('name'); + var iType = $(this).attr('data-type'); + var iValue = $(this).val(); + + // Build a new field node + var field = iqX.appendChild(iq.buildNode('field', {'var': iVar, 'type': iType, 'xmlns': NS_XDATA})); + + // Boolean input? + if(iType == 'boolean') { + if($(this).filter(':checked').size()) { + iValue = '1'; + } else { + iValue = '0'; + } + } + + // JID-multi input? + if(iType == 'jid-multi') { + // Values array + var xid_arr = [iValue]; + var xid_check = []; + + // Try to split it + if(iValue.indexOf(',') != -1) { + xid_arr = iValue.split(','); + } + + // Append each value to the XML document + for(var i in xid_arr) { + // Get the current value + xid_current = $.trim(xid_arr[i]); + + // No current value? + if(!xid_current) { + continue; + } + + // Add the current value + if(!Utils.existArrayValue(xid_check, xid_current)) { + xid_check.push(xid_current); + field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, xid_current)); + } + } + } + + // List-multi selector? + else if(iType == 'list-multi') { + // Any value? + if(iValue && iValue.length) { + for(var j in iValue) { + field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, iValue[j])); + } + } + } + + // Other inputs? + else { + field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, iValue)); + } + }); + } + } + + // Clean the current session + self.clean(target); + + // Show the waiting item + $('#' + target + ' .wait').show(); + + // Change the ID of the current discovered item + var iqID = target + '-' + genID(); + $('#' + target + ' .' + target + '-results').attr('data-session', iqID); + iq.setID(iqID); + + // Send the IQ + if(type == 'subscribe') { + con.send(iq, self.handleSubscribe); + } else if(type == 'search') { + con.send(iq, self.handleSearch); + } else if(type == 'command') { + con.send(iq, self.handleCommand); + } else { + con.send(iq); + } + } catch(e) { + Console.error('DataForm.send', e); + } finally { + return false; + } + + }; + + + /** + * Displays the good dataform buttons + * @public + * @param {string} type + * @param {string} action + * @param {string} id + * @param {string} xid + * @param {string} node + * @param {string} sessionid + * @param {string} target + * @param {string} pathID + * @return {undefined} + */ + self.buttons = function(type, action, id, xid, node, sessionid, target, pathID) { + + try { + // No need to use buttons? + if(type == 'muc') { + return; + } + + // Override the "undefined" output + if(!id) + id = ''; + if(!xid) + xid = ''; + if(!node) + node = ''; + if(!sessionid) + sessionid = ''; + + // We generate the buttons code + var buttonsCode = '
'; + + if(action == 'submit') { + if((target == 'adhoc') && (type == 'command')) { + buttonsCode += '' + Common._e("Submit") + ''; + + // When keyup on one text input + $(pathID + ' input').keyup(function(e) { + if(e.keyCode == 13) { + self.send(type, 'execute', 'submit', id, xid, node, sessionid, target); + + return false; + } + }); + } else { + buttonsCode += '' + Common._e("Submit") + ''; + + // When keyup on one text input + $(pathID + ' input').keyup(function(e) { + if(e.keyCode == 13) { + self.send(type, 'submit', 'submit', id, xid, node, sessionid, target); + + return false; + } + }); + } + } + + if((action == 'submit') && (type != 'subscribe') && (type != 'search')) { + buttonsCode += '' + Common._e("Cancel") + ''; + } + + if(((action == 'back') || (type == 'subscribe') || (type == 'search')) && (target == 'discovery')) { + buttonsCode += '' + Common._e("Close") + ''; + } + + if((action == 'back') && ((target == 'welcome') || (target == 'directory'))) { + buttonsCode += '' + Common._e("Previous") + ''; + } + + if((action == 'back') && (target == 'adhoc')) { + buttonsCode += '' + Common._e("Previous") + ''; + } + + buttonsCode += '
'; + + // We display the buttons code + $(pathID).append(buttonsCode); + + // If no submit link, lock the form + if(!Common.exists(pathID + ' a.submit')) { + $(pathID + ' input, ' + pathID + ' textarea').attr('readonly', true); + } + } catch(e) { + Console.error('DataForm.buttons', e); + } + + }; + + + /** + * Handles the MUC dataform + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleMUC = function(iq) { + + try { + Errors.handleReply(iq); + self.handleContent(iq, 'muc'); + } catch(e) { + Console.error('DataForm.handleMUC', e); + } + + }; + + + /** + * Handles the browse dataform + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleBrowse = function(iq) { + + try { + Errors.handleReply(iq); + self.handleContent(iq, 'browse'); + } catch(e) { + Console.error('DataForm.handleBrowse', e); + } + + }; + + + /** + * Handles the command dataform + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleCommand = function(iq) { + + try { + Errors.handleReply(iq); + self.handleContent(iq, 'command'); + } catch(e) { + Console.error('DataForm.handleCommand', e); + } + + }; + + + /** + * Handles the subscribe dataform + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleSubscribe = function(iq) { + + try { + Errors.handleReply(iq); + self.handleContent(iq, 'subscribe'); + } catch(e) { + Console.error('DataForm.handleSubscribe', e); + } + + }; + + + /** + * Handles the search dataform + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleSearch = function(iq) { + + try { + Errors.handleReply(iq); + self.handleContent(iq, 'search'); + } catch(e) { + Console.error('DataForm.handleSearch', e); + } + + }; + + + /** + * Handles the dataform content + * @public + * @param {object} iq + * @param {string} type + * @return {undefined} + */ + self.handleContent = function(iq, type) { + + try { + // Get the ID + var sID = iq.getID(); + + // Get the target + var splitted = sID.split('-'); + var target = splitted[0]; + var sessionID = target + '-' + splitted[1]; + var from = Common.fullXID(Common.getStanzaFrom(iq)); + var pathID = '#' + target + ' .results[data-session="' + sessionID + '"]'; + + // If an error occured + if(!iq || (iq.getType() != 'result')) { + self.noResult(pathID); + } + + // If we got something okay + else { + var handleXML = iq.getNode(); + + if(type == 'browse') { + if($(handleXML).find('item').attr('jid')) { + // Get the query node + var queryNode = $(handleXML).find('query').attr('node'); + + $(handleXML).find('item').each(function() { + // We parse the received xml + var itemHost = $(this).attr('jid'); + var itemNode = $(this).attr('node'); + var itemName = $(this).attr('name'); + var itemHash = hex_md5(itemHost); + + // Node + if(itemNode) { + $(pathID).append( + '
' + + '
' + itemNode.htmlEnc() + '
' + + '
' + ); + } + + // Item + else if(queryNode && itemName) { + $(pathID).append( + '
' + + '
' + itemName.htmlEnc() + '
' + + '
' + ); + } + + // Item with children + else { + // We display the waiting element + $(pathID + ' .disco-wait .disco-category-title').after( + '
' + + '
' + + '
' + itemHost + '
' + + '
' + Common._e("Requesting this service...") + '
' + + '
' + ); + + // We display the category + $('#' + target + ' .disco-wait').show(); + + // We ask the server what's the service type + self.getType(itemHost, itemNode, sessionID); + } + }); + } + + // Else, there are no items for this query + else + self.noResult(pathID); + } + + else if((type == 'muc') || (type == 'search') || (type == 'subscribe') || ((type == 'command') && $(handleXML).find('command').attr('xmlns'))) { + // Get some values + var xCommand = $(handleXML).find('command'); + var bNode = xCommand.attr('node'); + var bSession = xCommand.attr('sessionid'); + var bStatus = xCommand.attr('status'); + var xRegister = $(handleXML).find('query[xmlns="' + NS_REGISTER + '"]').text(); + var xElement = $(handleXML).find('x'); + + // Search done + if((xElement.attr('type') == 'result') && (type == 'search')) { + var bPath = pathID; + + // Display the result + $(handleXML).find('item').each(function() { + // Have some "flexibility" for what regards field names, it would be better to return the whole original DF + // layout, but on a large amount of result which have many fields, there's a very high chance the browser can + // choke on old systems or new ones even. + + // Search for useful fields, return first result. This is rather hacky, but jQuery is horrible when it comes to + // matching st. using patterns. (TODO: Improve and return the full DF layout without choking the browser) + var bName; + var bCountry; + var doneName, doneCountry; + + $.each($(this).find('field'), function(i, item) { + var $item = $(item); + + if($(item).attr('var').match(/^(fn|name|[^n][^i][^c][^k]name)$/gi) && doneName !== true) { + bName = $item.children('value:first').text(); + doneName = true; + } else if($(item).attr('var').match(/^(ctry|country.*)$/gi) && doneCountry !== true) { + bCountry = $item.children('value:first').text(); + doneCountry = true; + } + }); + + var bXID = $(this).find('field[var="jid"] value:first').text(); + var dName = bName; + + // Override "undefined" value + if(!bXID) + bXID = ''; + if(!bName) + bName = Common._e("Unknown name"); + if(!bCountry) + bCountry = Common._e("Unknown country"); + + // User hash + var bHash = hex_md5(bXID); + + // HTML code + var bHTML = '
' + + '
' + + '' + + '
' + + '
' + bName + '
' + + '
' + bCountry + '
' + + '
' + bXID + '
' + + '
'; + + // The buddy is not in our buddy list? + if(!Common.exists('#roster .buddy[data-xid="' + escape(bXID) + '"]')) { + bHTML += '' + Common._e("Add") + ''; + } + + // Chat button, if not in welcome/directory mode + if(target == 'discovery') { + bHTML += '' + Common._e("Chat") + ''; + } + + // Profile button, if not in discovery mode + else { + bHTML += '' + Common._e("Profile") + ''; + } + + // Close the HTML element + bHTML += '
'; + + $(bPath).append(bHTML); + + // Click events + $(bPath + ' .' + bHash + ' a').click(function() { + // Buddy add + if($(this).is('.one-add')) { + $(this).hide(); + + Roster.addThisContact(bXID, dName); + } + + // Buddy chat + if($(this).is('.one-chat')) { + if(target == 'discovery') + Discovery.close(); + + Chat.checkCreate(bXID, 'chat', '', '', dName); + } + + // Buddy profile + if($(this).is('.one-profile')) { + UserInfos.open(bXID); + } + + return false; + }); + + // Get the user's avatar + if(bXID) { + Avatar.get(bXID, 'cache', 'true', 'forget'); + } + }); + + // No result? + if(!$(handleXML).find('item').size()) + self.noResult(pathID); + + // Previous button + self.buttons(type, 'back', sessionID, from, bNode, bSession, target, pathID); + } + + // Command to complete + else if(xElement.attr('xmlns') || ((type == 'subscribe') && xRegister)) { + // We display the elements + self.fill(handleXML, sessionID); + + // We display the buttons + if(bStatus != 'completed') { + self.buttons(type, 'submit', sessionID, from, bNode, bSession, target, pathID); + } else { + self.buttons(type, 'back', sessionID, from, bNode, bSession, target, pathID); + } + } + + // Command completed or subscription done + else if(((bStatus == 'completed') && (type == 'command')) || (!xRegister && (type == 'subscribe'))) { + // Display the good text + var cNote = $(xCommand).find('note'); + + // Any note? + if(cNote.size()) { + cNote.each(function() { + $(pathID).append( + '
' + $(this).text().htmlEnc() + '
' + ); + }); + } + + // Default text + else { + $(pathID).append('
' + Common._e("Your form has been sent.") + '
'); + } + + // Display the back button + self.buttons(type, 'back', sessionID, from, '', '', target, pathID); + + // Add the gateway to our roster if subscribed + if(type == 'subscribe') { + Roster.addThisContact(from); + } + } + + // Command canceled + else if((bStatus == 'canceled') && (type == 'command')) { + if(target == 'discovery') { + Discovery.start(); + } else if(target == 'adhoc') { + dataForm(from, 'command', '', '', 'adhoc'); + } + } + + // No items for this query + else + self.noResult(pathID); + } + + else if(type == 'command') { + if($(handleXML).find('item').attr('jid')) { + // We display the elements + $(handleXML).find('item').each(function() { + // We parse the received xml + var itemHost = $(this).attr('jid'); + var itemNode = $(this).attr('node'); + var itemName = $(this).attr('name'); + var itemHash = hex_md5(itemHost); + + // We display the waiting element + $(pathID).prepend( + '
' + + '
' + itemName + '
' + + '
»
' + + '
' + ); + }); + } + + // Else, there are no items for this query + else { + self.noResult(pathID); + } + } + } + + // Focus on the first input + $(document).oneTime(10, function() { + $(pathID + ' input:visible:first').focus(); + }); + + // Hide the wait icon + $('#' + target + ' .wait').hide(); + } catch(e) { + Console.error('DataForm.handleContent', e); + } + + }; + + + /** + * Fills the dataform elements + * @public + * @param {type} xml + * @param {type} id + * @return {boolean} + */ + self.fill = function(xml, id) { + + /* REF: http://xmpp.org/extensions/xep-0004.html */ + + try { + // Initialize new vars + var target = id.split('-')[0]; + var pathID = '#' + target + ' .results[data-session="' + id + '"]'; + var selector, is_dataform; + + // Is it a dataform? + if($(xml).find('x[xmlns="' + NS_XDATA + '"]').size()) { + is_dataform = true; + } else { + is_dataform = false; + } + + // Determines the good selector to use + if(is_dataform) { + selector = $(xml).find('x[xmlns="' + NS_XDATA + '"]'); + } else { + selector = $(xml); + } + + // Form title + selector.find('title').each(function() { + $(pathID).append( + '
' + $(this).text().htmlEnc() + '
' + ); + }); + + // Form instructions + selector.find('instructions').each(function() { + $(pathID).append( + '
' + $(this).text().htmlEnc() + '
' + ); + }); + + // Register? + if(!is_dataform) { + // Items to detect + var reg_names = [Common._e("Nickname"), Common._e("Name"), Common._e("Password"), Common._e("E-mail")]; + var reg_ids = ['username', 'name', 'password', 'email']; + + // Append these inputs + $.each(reg_names, function(a) { + selector.find(reg_ids[a]).each(function() { + $(pathID).append( + '
' + + '' + + '' + + '
' + ); + }); + }); + + return false; + } + + // Dataform? + selector.find('field').each(function() { + // We parse the received xml + var type = $(this).attr('type'); + var label = $(this).attr('label'); + var field = $(this).attr('var'); + var value = $(this).find('value:first').text(); + var required = ''; + + // No value? + if(!field) { + return; + } + + // Required input? + if($(this).find('required').size()) { + required = ' required=""'; + } + + // Compatibility fix + if(!label) { + label = field; + } + + if(!type) { + type = ''; + } + + // Generate some values + var input; + var hideThis = ''; + + // Fixed field + if(type == 'fixed') { + $(pathID).append('
' + value.htmlEnc() + '
'); + } else { + // Hidden field + if(type == 'hidden') { + hideThis = ' style="display: none;"'; + input = ''; + } + + // Boolean field + else if(type == 'boolean') { + var checked; + + if(value == '1') + checked = 'checked'; + else + checked = ''; + + input = ''; + } + + // List-single/list-multi field + else if((type == 'list-single') || (type == 'list-multi')) { + var multiple = ''; + + // Multiple options? + if(type == 'list-multi') { + multiple = ' multiple=""'; + } + + // Append the select field + input = ''; + } + + // Text-multi field + else if(type == 'text-multi') { + input = ''; + } + + // JID-multi field + else if(type == 'jid-multi') { + // Put the XID into an array + var xid_arr = []; + + $(this).find('value').each(function() { + var cValue = $(this).text(); + + if(!Utils.existArrayValue(xid_arr, cValue)) { + xid_arr.push(cValue); + } + }); + + // Sort the array + xid_arr.sort(); + + // Create the input + var xid_value = ''; + + if(xid_arr.length) { + for(var i in xid_arr) { + // Any pre-value + if(xid_value) { + xid_value += ', '; + } + + // Add the current XID + xid_value += xid_arr[i]; + } + } + + input = ''; + } + + // Other stuffs that are similar + else { + // Text-single field + var iType = 'text'; + + // Text-private field + if(type == 'text-private') { + iType = 'password'; + } + + // JID-single field + else if(type == 'jid-single') { + iType = 'email'; + } + + input = ''; + } + + // Append the HTML markup for this field + $(pathID).append( + '
' + + '' + + input + + '
' + ); + } + }); + } catch(e) { + Console.error('DataForm.fill', e); + } finally { + return false; + } + + }; + + + /** + * Gets the dataform type + * @public + * @param {string} host + * @param {string} node + * @param {string} id + * @return {undefined} + */ + self.getType = function(host, node, id) { + + try { + var iq = new JSJaCIQ(); + iq.setID(id + '-' + genID()); + iq.setTo(host); + iq.setType('get'); + + var iqQuery = iq.setQuery(NS_DISCO_INFO); + + if(node) { + iqQuery.setAttribute('node', node); + } + + con.send(iq, self.handleThisBrowse); + } catch(e) { + Console.error('DataForm.getType', e); + } + + }; + + + /** + * Handles the browse stanza + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleThisBrowse = function(iq) { + + /* REF: http://xmpp.org/registrar/disco-categories.html */ + + try { + var id = iq.getID(); + var splitted = id.split('-'); + var target = splitted[0]; + var sessionID = target + '-' + splitted[1]; + var from = Common.fullXID(Common.getStanzaFrom(iq)); + var hash = hex_md5(from); + var handleXML = iq.getQuery(); + var pathID = '#' + target + ' .results[data-session="' + sessionID + '"]'; + + // We first remove the waiting element + $(pathID + ' .disco-wait .' + hash).remove(); + + if($(handleXML).find('identity').attr('type')) { + var category = $(handleXML).find('identity').attr('category'); + var type = $(handleXML).find('identity').attr('type'); + var named = $(handleXML).find('identity').attr('name'); + + if(named) { + gName = named; + } else { + gName = ''; + } + + var one, two, three, four, five; + + // Get the features that this entity supports + var findFeature = $(handleXML).find('feature'); + + for(var i in findFeature) { + var current = findFeature.eq(i).attr('var'); + + switch(current) { + case NS_SEARCH: + one = 1; + break; + + case NS_MUC: + two = 1; + break; + + case NS_REGISTER: + three = 1; + break; + + case NS_COMMANDS: + four = 1; + break; + + case NS_DISCO_ITEMS: + five = 1; + break; + + default: + break; + } + } + + var buttons = Array(one, two, three, four, five); + + // We define the toolbox links depending on the supported features + var tools = ''; + var aTools = Array('search', 'join', 'subscribe', 'command', 'browse'); + var bTools = Array(Common._e("Search"), Common._e("Join"), Common._e("Subscribe"), Common._e("Command"), Common._e("Browse")); + + for(var b in buttons) { + if(buttons[b]) { + tools += ''; + } + } + + // As defined in the ref, we detect the type of each category to put an icon + switch(category) { + case 'account': + case 'auth': + case 'automation': + case 'client': + case 'collaboration': + case 'component': + case 'conference': + case 'directory': + case 'gateway': + case 'headline': + case 'hierarchy': + case 'proxy': + case 'pubsub': + case 'server': + case 'store': + break; + + default: + category = 'others'; + } + + // We display the item we found + $(pathID + ' .disco-' + category + ' .disco-category-title').after( + '
' + + '
' + + '
' + from + '
' + + '
' + gName + '
' + + '
' + tools + '
' + + '
' + ); + + // We display the category + $(pathID + ' .disco-' + category).show(); + } + + else { + $(pathID + ' .disco-others .disco-category-title').after( + '
' + + '
' + + '
' + from + '
' + + '
' + Common._e("Service offline or broken") + '
' + + '
' + ); + + // We display the category + $(pathID + ' .disco-others').show(); + } + + // We hide the waiting stuffs if there's no remaining loading items + if(!$(pathID + ' .disco-wait .' + target + '-oneresult').size()) { + $(pathID + ' .disco-wait, #' + target + ' .wait').hide(); + } + } catch(e) { + Console.error('DataForm.handleThisBrowse', e); + } + + }; + + + /** + * Cleans the current data-form popup + * @public + * @param {string} target + * @return {undefined} + */ + self.clean = function(target) { + + try { + if(target == 'discovery') { + Discovery.clean(); + } else { + $('#' + target + ' div.results').empty(); + } + } catch(e) { + Console.error('DataForm.clean', e); + } + + }; + + + /** + * Displays the no result indicator + * @public + * @param {string} path + * @return {undefined} + */ + self.noResult = function(path) { + + try { + $(path).prepend('

' + Common._e("Sorry, but the entity didn't return any result!") + '

'); + } catch(e) { + Console.error('DataForm.noResult', e); + } + + }; + + + /** + * Return class scope + */ + return self; + })(); \ No newline at end of file diff --git a/source/app/javascripts/datastore.js b/source/app/javascripts/datastore.js index 5e8f927..1f4b64f 100644 --- a/source/app/javascripts/datastore.js +++ b/source/app/javascripts/datastore.js @@ -39,8 +39,9 @@ var DataStore = (function () { this.key = function(key) { if(legacy) { - if(key >= this.length) + if(key >= this.length) { return null; + } var c = 0; @@ -56,8 +57,9 @@ var DataStore = (function () { this.getItem = function(key) { if(legacy) { - if(storage_emulated[key] !== undefined) + if(storage_emulated[key] !== undefined) { return storage_emulated[key]; + } return null; } else { @@ -67,8 +69,9 @@ var DataStore = (function () { this.setItem = function(key, data) { if(legacy) { - if(!(key in storage_emulated)) + if(!(key in storage_emulated)) { this.length++; + } storage_emulated[key] = (data + ''); } else { @@ -162,7 +165,7 @@ var DataStore = (function () { try { return self.storageDB.getItem(dbID + '_' + type + '_' + id); } - + catch(e) { Console.error('Error while getting a temporary database entry (' + dbID + ' -> ' + type + ' -> ' + id + ')', e); } @@ -192,7 +195,7 @@ var DataStore = (function () { return true; } - + catch(e) { Console.error('Error while writing a temporary database entry (' + dbID + ' -> ' + type + ' -> ' + id + ')', e); } @@ -218,10 +221,10 @@ var DataStore = (function () { try { try { self.storageDB.removeItem(dbID + '_' + type + '_' + id); - + return true; } - + catch(e) { Console.error('Error while removing a temporary database entry (' + dbID + ' -> ' + type + ' -> ' + id + ')', e); } @@ -263,15 +266,15 @@ var DataStore = (function () { try { try { self.storageDB.clear(); - + Console.info('Temporary database cleared.'); - + return true; } - + catch(e) { Console.error('Error while clearing temporary database', e); - + return false; } } catch(e) { @@ -294,7 +297,7 @@ var DataStore = (function () { // Try to write something self.storagePersistent.setItem('haspersistent_check', 'ok'); self.storagePersistent.removeItem('haspersistent_check'); - + has_persistent = true; } catch(e) { Console.error('DataStore.hasPersistent', e); @@ -319,10 +322,10 @@ var DataStore = (function () { try { return self.storagePersistent.getItem(dbID + '_' + type + '_' + id); } - + catch(e) { Console.error('Error while getting a persistent database entry (' + dbID + ' -> ' + type + ' -> ' + id + ')', e); - + return null; } } catch(e) { @@ -346,24 +349,24 @@ var DataStore = (function () { try { try { self.storagePersistent.setItem(dbID + '_' + type + '_' + id, value); - + return true; } - + // Database might be full catch(e) { Console.warn('Retrying: could not write a persistent database entry (' + dbID + ' -> ' + type + ' -> ' + id + ')', e); - + // Flush it! self.flushPersistent(); - + // Set the item again try { self.storagePersistent.setItem(dbID + ' -> ' + type + '_' + id, value); - + return true; } - + // New error! catch(_e) { Console.error('Aborted: error while writing a persistent database entry (' + dbID + ' -> ' + type + ' -> ' + id + ')', _e); @@ -394,7 +397,7 @@ var DataStore = (function () { return true; } - + catch(e) { Console.error('Error while removing a persistent database entry (' + dbID + ' -> ' + type + ' -> ' + id + ')', e); } @@ -439,10 +442,10 @@ var DataStore = (function () { self.storagePersistent.clear(); Console.info('Persistent database cleared.'); - + return true; } - + catch(e) { Console.error('Error while clearing persistent database', e); } @@ -467,19 +470,20 @@ var DataStore = (function () { try { // Get the stored session entry var session = self.getPersistent('global', 'session', 1); - + // Reset the persistent database self.resetPersistent(); - + // Restaure the stored session entry - if(session) + if(session) { self.setPersistent('global', 'session', 1, session); - + } + Console.info('Persistent database flushed.'); - + return true; } - + catch(e) { Console.error('Error while flushing persistent database', e); } diff --git a/source/app/javascripts/date.js b/source/app/javascripts/date.js index 289eb13..977b677 100644 --- a/source/app/javascripts/date.js +++ b/source/app/javascripts/date.js @@ -84,9 +84,10 @@ var DateUtils = (function () { try { // Last activity not yet initialized? - if(self.last_activity === 0) + if(self.last_activity === 0) { return 0; - + } + return self.getTimeStamp() - self.last_activity; } catch(e) { Console.error('DateUtils.getLastActivity', e); @@ -95,6 +96,27 @@ var DateUtils = (function () { }; + /** + * Gets the last user activity as a date + * @public + * @return {string} + */ + self.getLastActivityDate = function() { + + try { + var last_activity = self.last_activity || self.getTimeStamp(); + + var last_date = new Date(); + last_date.setTime(last_activity * 1000); + + return self.getDatetime(last_date, 'utc'); + } catch(e) { + Console.error('DateUtils.getLastActivityDate', e); + } + + }; + + /** * Gets the last user available presence in seconds * @public @@ -104,9 +126,10 @@ var DateUtils = (function () { try { // Last presence stamp not yet initialized? - if(self.presence_last_activity === 0) + if(self.presence_last_activity === 0) { return 0; - + } + return self.getTimeStamp() - self.presence_last_activity; } catch(e) { Console.error('DateUtils.getPresenceLast', e); @@ -115,6 +138,56 @@ var DateUtils = (function () { }; + /** + * Generates a normalized datetime + * @public + * @param {Date} date + * @param {string} location + * @return {string} + */ + self.getDatetime = function(date, location) { + + /* FROM : http://trac.jwchat.org/jsjac/browser/branches/jsjac_1.0/jsextras.js?rev=221 */ + + var year, month, day, hours, minutes, seconds; + var date_string = null; + + try { + if(location == 'utc') { + // UTC date + year = date.getUTCFullYear(); + month = date.getUTCMonth(); + day = date.getUTCDate(); + hours = date.getUTCHours(); + minutes = date.getUTCMinutes(); + seconds = date.getUTCSeconds(); + } else { + // Local date + year = date.getFullYear(); + month = date.getMonth(); + day = date.getDate(); + hours = date.getHours(); + minutes = date.getMinutes(); + seconds = date.getSeconds(); + } + + // Generates the date string + date_string = year + '-'; + date_string += Common.padZero(month + 1) + '-'; + date_string += Common.padZero(day) + 'T'; + date_string += Common.padZero(hours) + ':'; + date_string += Common.padZero(minutes) + ':'; + date_string += Common.padZero(seconds) + 'Z'; + + // Returns the date string + return date_string; + } catch(e) { + Console.error('DateUtils.getDatetime', e); + } + + }; + + /** * Generates the time for XMPP * @public @@ -123,43 +196,11 @@ var DateUtils = (function () { */ self.getXMPPTime = function(location) { - /* FROM : http://trac.jwchat.org/jsjac/browser/branches/jsjac_1.0/jsextras.js?rev=221 */ - try { - // Initialize - var jInit = new Date(); - var year, month, day, hours, minutes, seconds; - - // Gets the UTC date - if(location == 'utc') { - year = jInit.getUTCFullYear(); - month = jInit.getUTCMonth(); - day = jInit.getUTCDate(); - hours = jInit.getUTCHours(); - minutes = jInit.getUTCMinutes(); - seconds = jInit.getUTCSeconds(); - } - - // Gets the local date - else { - year = jInit.getFullYear(); - month = jInit.getMonth(); - day = jInit.getDate(); - hours = jInit.getHours(); - minutes = jInit.getMinutes(); - seconds = jInit.getSeconds(); - } - - // Generates the date string - var jDate = year + '-'; - jDate += Common.padZero(month + 1) + '-'; - jDate += Common.padZero(day) + 'T'; - jDate += Common.padZero(hours) + ':'; - jDate += Common.padZero(minutes) + ':'; - jDate += Common.padZero(seconds) + 'Z'; - - // Returns the date string - return jDate; + return self.getDatetime( + (new Date()), + location + ); } catch(e) { Console.error('DateUtils.getXMPPTime', e); } @@ -176,10 +217,11 @@ var DateUtils = (function () { try { var init = new Date(); + var time = Common.padZero(init.getHours()) + ':'; time += Common.padZero(init.getMinutes()) + ':'; time += Common.padZero(init.getSeconds()); - + return time; } catch(e) { Console.error('DateUtils.getCompleteTime', e); @@ -199,26 +241,26 @@ var DateUtils = (function () { // Get the date var date = new Date(); var offset = date.getTimezoneOffset(); - + // Default vars var sign = ''; var hours = 0; var minutes = 0; - + // Process a neutral offset if(offset < 0) { offset = offset * -1; sign = '+'; } - + // Get the values var n_date = new Date(offset * 60 * 1000); hours = n_date.getHours() - 1; minutes = n_date.getMinutes(); - + // Process the TZO tzo = sign + Common.padZero(hours) + ':' + Common.padZero(minutes); - + // Return the processed value return tzo; } catch(e) { @@ -259,7 +301,7 @@ var DateUtils = (function () { try { var date = Date.jab2date(to_parse); var parsed = date.toLocaleDateString() + ' (' + date.toLocaleTimeString() + ')'; - + return parsed; } catch(e) { Console.error('DateUtils.parse', e); @@ -279,7 +321,7 @@ var DateUtils = (function () { try { var date = Date.jab2date(to_parse); var parsed = date.toLocaleDateString(); - + return parsed; } catch(e) { Console.error('DateUtils.parseDay', e); @@ -299,7 +341,7 @@ var DateUtils = (function () { try { var date = Date.jab2date(to_parse); var parsed = date.toLocaleTimeString(); - + return parsed; } catch(e) { Console.error('DateUtils.parseTime', e); @@ -321,32 +363,36 @@ var DateUtils = (function () { var current_date = Date.jab2date(self.getXMPPTime('utc')); var current_day = current_date.getDate(); var current_stamp = current_date.getTime(); - + // Parse the given date var old_date = Date.jab2date(to_parse); var old_day = old_date.getDate(); var old_stamp = old_date.getTime(); var old_time = old_date.toLocaleTimeString(); - + // Get the day number between the two dates var days = Math.round((current_stamp - old_stamp) / 86400000); - + // Invalid date? - if(isNaN(old_stamp) || isNaN(days)) + if(isNaN(old_stamp) || isNaN(days)) { return self.getCompleteTime(); - + } + // Is it today? - if(current_day == old_day) + if(current_day == old_day) { return old_time; - + } + // It is yesterday? - if(days <= 1) + if(days <= 1) { return Common._e("Yesterday") + ' - ' + old_time; - + } + // Is it less than a week ago? - if(days <= 7) + if(days <= 7) { return Common.printf(Common._e("%s days ago"), days) + ' - ' + old_time; - + } + // Another longer period return old_date.toLocaleDateString() + ' - ' + old_time; } catch(e) { @@ -360,30 +406,36 @@ var DateUtils = (function () { * Reads a message delay * @public * @param {string} node - * @return {string} + * @param {boolean} return_date + * @return {string|Date} */ - self.readMessageDelay = function(node) { + self.readMessageDelay = function(node, return_date) { try { // Initialize var delay, d_delay; - + // Read the delay d_delay = jQuery(node).find('delay[xmlns="' + NS_URN_DELAY + '"]:first').attr('stamp'); - - // New delay (valid XEP) - if(d_delay) + + // Get delay + if(d_delay) { + // New delay (valid XEP) delay = d_delay; - - // Old delay (obsolete XEP!) - else { - // Try to read the old-school delay + } else { + // Old delay (obsolete XEP!) var x_delay = jQuery(node).find('x[xmlns="' + NS_DELAY + '"]:first').attr('stamp'); - - if(x_delay) + + if(x_delay) { delay = x_delay.replace(/^(\w{4})(\w{2})(\w{2})T(\w{2}):(\w{2}):(\w{2})Z?(\S+)?/, '$1-$2-$3T$4:$5:$6Z$7'); + } } - + + // Return a date object? + if(return_date === true && delay) { + return Date.jab2date(delay); + } + return delay; } catch(e) { Console.error('DateUtils.readMessageDelay', e); diff --git a/source/app/javascripts/datejs.js b/source/app/javascripts/datejs.js index 77f4986..2d52e9a 100644 --- a/source/app/javascripts/datejs.js +++ b/source/app/javascripts/datejs.js @@ -1,10 +1,10 @@ -/** - * Version: 1.0 Alpha-1 - * Build Date: 13-Nov-2007 - * Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved. - * License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/. - * Website: http://www.datejs.com/ or http://www.coolite.com/datejs/ - */ +/** + * Version: 1.0 Alpha-1 + * Build Date: 13-Nov-2007 + * Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved. + * License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/. + * Website: http://www.datejs.com/ or http://www.coolite.com/datejs/ + */ Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|after|from)/i,subtract:/^(\-|before|ago)/i,yesterday:/^yesterday/i,today:/^t(oday)?/i,tomorrow:/^tomorrow/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^min(ute)?s?/i,hour:/^h(ou)?rs?/i,week:/^w(ee)?k/i,month:/^m(o(nth)?s?)?/i,day:/^d(ays?)?/i,year:/^y((ea)?rs?)?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a|p)/i},abbreviatedTimeZoneStandard:{GMT:"-000",EST:"-0400",CST:"-0500",MST:"-0600",PST:"-0700"},abbreviatedTimeZoneDST:{GMT:"-000",EDT:"-0500",CDT:"-0600",MDT:"-0700",PDT:"-0800"}}; Date.getMonthNumberFromName=function(name){var n=Date.CultureInfo.monthNames,m=Date.CultureInfo.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;i' + Common._e("User directory") + '
' + - - '
' + - '
' + - '
' + Common._e("Server to query") + '
' + - - '' + - '
' + - - '
' + - '
' + - - '
' + - '
' + - - '' + Common._e("Close") + '' + + var html = + '
' + Common._e("User directory") + '
' + + + '
' + + '
' + + '
' + Common._e("Server to query") + '
' + + + '' + + '
' + + + '
' + + '
' + + + '
' + + '
' + + + '' + Common._e("Close") + '' + '
'; - + // Create the popup Popup.create('directory', html); - + // Associate the events self.instance(); - + // Start a search! self.start(); } catch(e) { @@ -94,10 +94,10 @@ var Directory = (function () { try { // Get the server to query var server = $('#directory .directory-server-input').val(); - + // Launch the search! DataForm.go($('#directory .directory-server-input').val(), 'search', '', '', 'directory'); - + Console.log('Directory search launched: ' + server); } catch(e) { Console.error('Directory.start', e); @@ -118,17 +118,18 @@ var Directory = (function () { try { // Click event $('#directory .bottom .finish').click(self.close); - + // Keyboard event $('#directory .directory-server-input').keyup(function(e) { if(e.keyCode == 13) { // No value? - if(!$(this).val()) + if(!$(this).val()) { $(this).val(HOST_VJUD); - + } + // Start the directory search self.start(); - + return false; } }); diff --git a/source/app/javascripts/discovery.js b/source/app/javascripts/discovery.js index ebdce27..ef419ba 100644 --- a/source/app/javascripts/discovery.js +++ b/source/app/javascripts/discovery.js @@ -29,99 +29,99 @@ var Discovery = (function () { try { // Popup HTML content - var html = - '
' + Common._e("Service discovery") + '
' + - - '
' + - '
' + - '
' + Common._e("Server to query") + '
' + - - '' + - '
' + - - '
' + - '' + - - '
' + - '

' + Common._e("Authentications") + '

' + - '
' + - - '
' + - '

' + Common._e("Automation") + '

' + - '
' + - - '
' + - '

' + Common._e("Clients") + '

' + - '
' + - - '
' + - '

' + Common._e("Collaboration") + '

' + - '
' + - - '
' + - '

' + Common._e("Components") + '

' + - '
' + - - '
' + - '

' + Common._e("Rooms") + '

' + - '
' + - - '
' + - '

' + Common._e("Directories") + '

' + - '
' + - - '
' + - '

' + Common._e("Gateways") + '

' + - '
' + - - '
' + - '

' + Common._e("News") + '

' + - '
' + - - '
' + - '

' + Common._e("Hierarchy") + '

' + - '
' + - - '
' + - '

' + Common._e("Proxies") + '

' + - '
' + - - '
' + - '

' + Common._e("Publication/Subscription") + '

' + - '
' + - - '
' + - '

' + Common._e("Server") + '

' + - '
' + - - '
' + - '

' + Common._e("Storage") + '

' + - '
' + - - '
' + - '

' + Common._e("Others") + '

' + - '
' + - - '
' + - '

' + Common._e("Loading") + '

' + - '
' + - '
' + - '
' + - - '
' + - '
' + - - '' + Common._e("Close") + '' + + var html = + '
' + Common._e("Service discovery") + '
' + + + '
' + + '
' + + '
' + Common._e("Server to query") + '
' + + + '' + + '
' + + + '
' + + '' + + + '
' + + '

' + Common._e("Authentications") + '

' + + '
' + + + '
' + + '

' + Common._e("Automation") + '

' + + '
' + + + '
' + + '

' + Common._e("Clients") + '

' + + '
' + + + '
' + + '

' + Common._e("Collaboration") + '

' + + '
' + + + '
' + + '

' + Common._e("Components") + '

' + + '
' + + + '
' + + '

' + Common._e("Rooms") + '

' + + '
' + + + '
' + + '

' + Common._e("Directories") + '

' + + '
' + + + '
' + + '

' + Common._e("Gateways") + '

' + + '
' + + + '
' + + '

' + Common._e("News") + '

' + + '
' + + + '
' + + '

' + Common._e("Hierarchy") + '

' + + '
' + + + '
' + + '

' + Common._e("Proxies") + '

' + + '
' + + + '
' + + '

' + Common._e("Publication/Subscription") + '

' + + '
' + + + '
' + + '

' + Common._e("Server") + '

' + + '
' + + + '
' + + '

' + Common._e("Storage") + '

' + + '
' + + + '
' + + '

' + Common._e("Others") + '

' + + '
' + + + '
' + + '

' + Common._e("Loading") + '

' + + '
' + + '
' + + '
' + + + '
' + + '
' + + + '' + Common._e("Close") + '' + '
'; - + // Create the popup Popup.create('discovery', html); - + // Associate the events self.instance(); - + // We request a disco to the default server self.start(); } catch(e) { @@ -164,10 +164,10 @@ var Discovery = (function () { try { // We get the server to query var discoServer = $('#discovery .disco-server-input').val(); - + // We launch the items query DataForm.go(discoServer, 'browse', '', '', 'discovery'); - + Console.log('Service discovery launched: ' + discoServer); } catch(e) { Console.error('Discovery.start', e); @@ -188,10 +188,10 @@ var Discovery = (function () { try { // We remove the results $('#discovery .discovery-oneresult, #discovery .oneinstructions, #discovery .onetitle, #discovery .no-results').remove(); - + // We clean the user info $('#discovery .disco-server-info').text(''); - + // We hide the wait icon, the no result alert and the results $('#discovery .wait, #discovery .disco-category').hide(); } catch(e) { @@ -211,17 +211,18 @@ var Discovery = (function () { try { // Click event $('#discovery .bottom .finish').click(self.close); - + // Keyboard event $('#discovery .disco-server-input').keyup(function(e) { if(e.keyCode == 13) { // No value? - if(!$(this).val()) + if(!$(this).val()) { $(this).val(HOST_MAIN); - + } + // Start the discovery self.start(); - + return false; } }); diff --git a/source/app/javascripts/errors.js b/source/app/javascripts/errors.js index d3c3346..f1a651c 100644 --- a/source/app/javascripts/errors.js +++ b/source/app/javascripts/errors.js @@ -35,30 +35,33 @@ var Errors = (function () { if(condition || reason) { // Initialize the error text var eText = ''; - + // Any error condition - if(condition) + if(condition) { eText += condition; - + } + // Any error type - if(type && eText) + if(type && eText) { eText += ' (' + type + ')'; - + } + // Any error reason if(reason) { - if(eText) + if(eText) { eText += ' - '; - + } + eText += reason; } - + // We reveal the error Board.openThisError(1); - + // Create the error text $('#board .one-board.error[data-id="1"] span').text(eText); } - + // Not enough data to output the error: output a generic board else { Board.openThisError(2); @@ -84,75 +87,76 @@ var Errors = (function () { // Initialize var type, code, reason, condition; var node = $(packet); - + // First level error (connection error) if(node.is('error')) { // Get the value code = node.attr('code'); - + // Specific error reason switch(code) { case '401': reason = Common._e("Authorization failed"); break; - + case '409': reason = Common._e("Registration failed, please choose a different username"); break; - + case '503': reason = Common._e("Service unavailable"); break; - + case '500': reason = Common._e("Internal server error, try later"); break; - + default: reason = node.find('text').text(); break; } - + // Remove the general wait item (security) Interface.removeGeneralWait(); - + // Show reconnect pane if(Connection.current_session && Connection.connected) { // Anonymous? - if(Utils.isAnonymous()) + if(Utils.isAnonymous()) { Connection.createReconnect('anonymous'); - else + } else { Connection.createReconnect('normal'); + } } - + // Show the homepage (security) else if(!Connection.current_session || !Connection.connected) { $('#home').show(); Interface.title('home'); } - + // Still connected? (security) if(Common.isConnected()) { con.disconnect(); } - + Console.error('First level error received.'); } - + // Second level error (another error) else if(node.find('error').size()) { type = node.find('error').attr('type'); reason = node.find('error text').text(); condition = packet.getElementsByTagName('error').item(0).childNodes.item(0).nodeName.replace(/-/g, ' '); - + Console.error('Second level error received.'); } else { return false; } - + // Show the error board self.show(condition, reason, type); - + // Return there's an error return true; } catch(e) { diff --git a/source/app/javascripts/favorites.js b/source/app/javascripts/favorites.js index 146d3fc..ffbbe17 100644 --- a/source/app/javascripts/favorites.js +++ b/source/app/javascripts/favorites.js @@ -29,103 +29,103 @@ var Favorites = (function () { try { // Popup HTML content - var html = - '
' + Common._e("Manage favorite rooms") + '
' + - - '
' + - '
' + - '
' + - '
' + - - Common._e("Change favorites") + - '
' + - - '' + - '
' + - - '
' + - '
' + - '
' + - '
' + Common._e("Select a favorite") + '
' + - - '' + - '
' + - - '
' + - '
' + - '' + - - '' + - '
' + - - '
' + - '' + - - '' + - '
' + - - '
' + - '' + - - '' + - '
' + - - '
' + - '' + - - '' + - '
' + - - '
' + - '' + - - '' + - '
' + - - '
' + - '' + - - '' + - '
' + - - '' + - '
' + - '
' + - - '' + - '
' + - '
' + - - '
' + - '
' + - - '' + Common._e("Close") + '' + + var html = + '
' + Common._e("Manage favorite rooms") + '
' + + + '
' + + '
' + + '
' + + '
' + + + Common._e("Change favorites") + + '
' + + + '' + + '
' + + + '
' + + '
' + + '
' + + '
' + Common._e("Select a favorite") + '
' + + + '' + + '
' + + + '
' + + '
' + + '' + + + '' + + '
' + + + '
' + + '' + + + '' + + '
' + + + '
' + + '' + + + '' + + '
' + + + '
' + + '' + + + '' + + '
' + + + '
' + + '' + + + '' + + '
' + + + '
' + + '' + + + '' + + '
' + + + '' + + '
' + + '
' + + + '' + + '
' + + '
' + + + '
' + + '
' + + + '' + Common._e("Close") + '' + '
'; - + // Create the popup Popup.create('favorites', html); - + // Load the favorites self.load(); - + // Associate the events self.instance(); } catch(e) { @@ -143,16 +143,21 @@ var Favorites = (function () { self.reset = function() { try { - var path = '#favorites '; - - $(path + '.wait, ' + path + '.fedit-terminate').hide(); - $(path + '.fedit-add').show(); - $(path + '.fsearch-oneresult').remove(); - $(path + 'input').val(''); - $(path + '.please-complete').removeClass('please-complete'); - $(path + '.fedit-nick').val(Name.getNick()); - $(path + '.fsearch-head-server, ' + path + '.fedit-server').val(HOST_MUC); - $(path + '.fedit-autojoin').removeAttr('checked'); + var path_sel = $('#favorites'); + + path_sel.find('.wait'); + path_sel.find('.fedit-terminate').hide(); + path_sel.find('.fedit-add').show(); + + path_sel.find('.fsearch-oneresult').remove(); + path_sel.find('input').val(''); + path_sel.find('.please-complete').removeClass('please-complete'); + + path_sel.find('.fedit-nick').val(Name.getNick()); + path_sel.find('.fsearch-head-server').val(HOST_MUC); + path_sel.find('.fedit-server').val(HOST_MUC); + + path_sel.find('.fedit-autojoin').removeAttr('checked'); } catch(e) { Console.error('Favorites.reset', e); } @@ -182,30 +187,35 @@ var Favorites = (function () { /** * Adds a room to the favorites * @public - * @param {string} roomXID - * @param {string} roomName + * @param {string} room_xid + * @param {string} room_name * @return {boolean} */ - self.addThis = function(roomXID, roomName) { + self.addThis = function(room_xid, room_name) { try { // Button path - var button = '#favorites .fsearch-results div[data-xid="' + escape(roomXID) + '"] a.one-button'; - + var button_sel = $('#favorites .fsearch-results div[data-xid="' + escape(room_xid) + '"] a.one-button'); + // Add a remove button instead of the add one - $(button + '.add').replaceWith('' + Common._e("Remove") + ''); - + button_sel.filter('.add').replaceWith( + '' + Common._e("Remove") + '' + ); + // Click event - $(button + '.remove').click(function() { - return self.removeThis(roomXID, roomName); + button_sel.filter('.remove').click(function() { + return self.removeThis(room_xid, room_name); }); - + // Hide the add button in the (opened?) groupchat - $('#' + hex_md5(roomXID) + ' .tools-add').hide(); - + $('#' + hex_md5(room_xid) + ' .tools-add').hide(); + // Add the database entry - self.display(roomXID, Common.explodeThis(' (', roomName, 0), Name.getNick(), '0', ''); - + self.display( + room_xid, + Common.explodeThis(' (', room_name, 0), Name.getNick(), '0', '' + ); + // Publish the favorites self.publish(); } catch(e) { @@ -220,30 +230,30 @@ var Favorites = (function () { /** * Removes a room from the favorites * @public - * @param {string} roomXID - * @param {string} roomName + * @param {string} room_xid + * @param {string} room_name * @return {boolean} */ - self.removeThis = function(roomXID, roomName) { + self.removeThis = function(room_xid, room_name) { try { // Button path - var button = '#favorites .fsearch-results div[data-xid="' + escape(roomXID) + '"] a.one-button'; - + var button_sel = $('#favorites .fsearch-results div[data-xid="' + escape(room_xid) + '"] a.one-button'); + // Add a remove button instead of the add one - $(button + '.remove').replaceWith('' + Common._e("Add") + ''); - + button_sel.filter('.remove').replaceWith('' + Common._e("Add") + ''); + // Click event - $(button + '.add').click(function() { - return self.addThis(roomXID, roomName); + button_sel.filter('.add').click(function() { + return self.addThis(room_xid, room_name); }); - + // Show the add button in the (opened?) groupchat - $('#' + hex_md5(roomXID) + ' .tools-add').show(); - + $('#' + hex_md5(room_xid) + ' .tools-add').show(); + // Remove the favorite - self.remove(roomXID, true); - + self.remove(room_xid, true); + // Publish the favorites self.publish(); } catch(e) { @@ -264,31 +274,34 @@ var Favorites = (function () { try { // Path to favorites - var favorites = '#favorites .'; - + var favorites_sel = $('#favorites'); + // Reset the favorites self.reset(); - + // Show the edit/remove button, hide the others - $(favorites + 'fedit-terminate').hide(); - $(favorites + 'fedit-edit').show(); - $(favorites + 'fedit-remove').show(); - + favorites_sel.find('.fedit-terminate').hide(); + favorites_sel.find('.fedit-edit').show(); + favorites_sel.find('.fedit-remove').show(); + // We retrieve the values - var xid = $(favorites + 'fedit-head-select').val(); - var data = Common.XMLFromString(DataStore.getDB(Connection.desktop_hash, 'favorites', xid)); - + var xid = favorites_sel.find('.fedit-head-select').val(); + var data_sel = $(Common.XMLFromString( + DataStore.getDB(Connection.desktop_hash, 'favorites', xid) + )); + // If this is not the default room if(xid != 'none') { // We apply the values - $(favorites + 'fedit-title').val($(data).find('name').text()); - $(favorites + 'fedit-nick').val($(data).find('nick').text()); - $(favorites + 'fedit-chan').val(Common.getXIDNick(xid)); - $(favorites + 'fedit-server').val(Common.getXIDHost(xid)); - $(favorites + 'fedit-password').val($(data).find('password').text()); - - if($(data).find('autojoin').text() == 'true') - $(favorites + 'fedit-autojoin').attr('checked', true); + favorites_sel.find('.fedit-title').val(data_sel.find('name').text()); + favorites_sel.find('.fedit-nick').val(data_sel.find('nick').text()); + favorites_sel.find('.fedit-chan').val(Common.getXIDNick(xid)); + favorites_sel.find('.fedit-server').val(Common.getXIDHost(xid)); + favorites_sel.find('.fedit-password').val(data_sel.find('password').text()); + + if(data_sel.find('autojoin').text() == 'true') { + favorites_sel.find('.fedit-autojoin').attr('checked', true); + } } } catch(e) { Console.error('Favorites.edit', e); @@ -307,61 +320,59 @@ var Favorites = (function () { try { // Path to favorites - var favorites = '#favorites '; - + var favorites_sel = $('#favorites'); + // We get the values of the current edited groupchat - var old_xid = $(favorites + '.fedit-head-select').val(); - - var title = $(favorites + '.fedit-title').val(); - var nick = $(favorites + '.fedit-nick').val(); - var room = $(favorites + '.fedit-chan').val(); - var server = $(favorites + '.fedit-server').val(); + var old_xid = favorites_sel.find('.fedit-head-select').val(); + + var title = favorites_sel.find('.fedit-title').val(); + var nick = favorites_sel.find('.fedit-nick').val(); + var room = favorites_sel.find('.fedit-chan').val(); + var server = favorites_sel.find('.fedit-server').val(); var xid = room + '@' + server; - var password = $(favorites + '.fedit-password').val(); + var password = favorites_sel.find('.fedit-password').val(); var autojoin = 'false'; - - if($(favorites + '.fedit-autojoin').filter(':checked').size()) + + if(favorites_sel.find('.fedit-autojoin').filter(':checked').size()) { autojoin = 'true'; - + } + // We check the missing values and send this if okay if((type == 'add') || (type == 'edit')) { if(title && nick && room && server) { // Remove the edited room - if(type == 'edit') + if(type == 'edit') { self.remove(old_xid, true); - + } + // Display the favorites self.display(xid, title, nick, autojoin, password); - + // Reset the inputs self.reset(); - } - - else { - $(favorites + 'input[required]').each(function() { + } else { + favorites_sel.find('input[required]').each(function() { var select = $(this); - - if(!select.val()) + + if(!select.val()) { $(document).oneTime(10, function() { select.addClass('please-complete').focus(); }); - else - select.removeClass('please-complete'); + } else { + select.removeClass('please-complete'); + } }); } - } - - // Must remove a favorite? - else if(type == 'remove') { + } else if(type == 'remove') { self.remove(old_xid, true); - + // Reset the inputs self.reset(); } - + // Publish the new favorites self.publish(); - + Console.info('Action on this bookmark: ' + room + '@' + server + ' / ' + type); } catch(e) { Console.error('Favorites.terminateThis', e); @@ -385,7 +396,7 @@ var Favorites = (function () { // We remove the target favorite everywhere needed $('.buddy-conf-groupchat-select option[value="' + xid + '"]').remove(); $('.fedit-head-select option[value="' + xid + '"]').remove(); - + // Must remove it from database? if(database) { DataStore.removeDB(Connection.desktop_hash, 'favorites', xid); @@ -407,37 +418,55 @@ var Favorites = (function () { try { var iq = new JSJaCIQ(); iq.setType('set'); - + var query = iq.setQuery(NS_PRIVATE); - var storage = query.appendChild(iq.buildNode('storage', {'xmlns': NS_BOOKMARKS})); - + var storage = query.appendChild(iq.buildNode('storage', { + 'xmlns': NS_BOOKMARKS + })); + // We generate the XML var db_regex = new RegExp(('^' + Connection.desktop_hash + '_') + 'favorites_(.+)'); for(var i = 0; i < DataStore.storageDB.length; i++) { // Get the pointer values var current = DataStore.storageDB.key(i); - + // If the pointer is on a stored favorite if(current.match(db_regex)) { - var data = Common.XMLFromString(DataStore.storageDB.getItem(current)); - var xid = $(data).find('xid').text(); - var rName = $(data).find('name').text(); - var nick = $(data).find('nick').text(); - var password = $(data).find('password').text(); - var autojoin = $(data).find('autojoin').text(); - + var data_sel = $(Common.XMLFromString( + DataStore.storageDB.getItem(current) + )); + + var xid = data_sel.find('xid').text(); + var rName = data_sel.find('name').text(); + var nick = data_sel.find('nick').text(); + var password = data_sel.find('password').text(); + var autojoin = data_sel.find('autojoin').text(); + // We create the node for this groupchat - var item = storage.appendChild(iq.buildNode('conference', {'name': rName, 'jid': xid, 'autojoin': autojoin, xmlns: NS_BOOKMARKS})); - item.appendChild(iq.buildNode('nick', {xmlns: NS_BOOKMARKS}, nick)); - - if(password) - item.appendChild(iq.buildNode('password', {xmlns: NS_BOOKMARKS}, password)); - + var item = storage.appendChild( + iq.buildNode('conference', { + 'name': rName, + 'jid': xid, + 'autojoin': autojoin, + xmlns: NS_BOOKMARKS + }) + ); + + item.appendChild(iq.buildNode('nick', { + xmlns: NS_BOOKMARKS + }, nick)); + + if(password) { + item.appendChild(iq.buildNode('password', { + xmlns: NS_BOOKMARKS + }, password)); + } + Console.info('Bookmark sent: ' + xid); } } - + con.send(iq); } catch(e) { Console.error('Favorites.publish', e); @@ -454,20 +483,20 @@ var Favorites = (function () { self.getGCList = function() { try { - var path = '#favorites .'; - var gcServer = $('.fsearch-head-server').val(); - + var path_sel = $('#favorites'); + var groupchat_server = $('.fsearch-head-server').val(); + // We reset some things - $(path + 'fsearch-oneresult').remove(); - $(path + 'fsearch-noresults').hide(); - $(path + 'wait').show(); - + path_sel.find('.fsearch-oneresult').remove(); + path_sel.find('.fsearch-noresults').hide(); + path_sel.find('.wait').show(); + var iq = new JSJaCIQ(); iq.setType('get'); - iq.setTo(gcServer); - + iq.setTo(groupchat_server); + iq.setQuery(NS_DISCO_ITEMS); - + con.send(iq, self.handleGCList); } catch(e) { Console.error('Favorites.getGCList', e); @@ -485,60 +514,72 @@ var Favorites = (function () { self.handleGCList = function(iq) { try { - var path = '#favorites .'; + var path_sel = $('#favorites'); var from = Common.fullXID(Common.getStanzaFrom(iq)); - + if(!iq || (iq.getType() != 'result')) { Board.openThisError(3); - - $(path + 'wait').hide(); - + + path_sel.find('.wait').hide(); + Console.error('Error while retrieving the rooms: ' + from); } - + else { var handleXML = iq.getQuery(); - + if($(handleXML).find('item').size()) { // Initialize the HTML code var html = ''; - + $(handleXML).find('item').each(function() { - var roomXID = $(this).attr('jid'); - var roomName = $(this).attr('name'); - - if(roomXID && roomName) { + var this_sel = $(this); + + var room_xid = this_sel.attr('jid'); + var room_name = this_sel.attr('name'); + + if(room_xid && room_name) { // Escaped values - var escaped_xid = Utils.encodeOnclick(roomXID); - var escaped_name = Utils.encodeOnclick(roomName); - + var escaped_xid = Utils.encodeOnclick(room_xid); + var escaped_name = Utils.encodeOnclick(room_name); + // Initialize the room HTML - html += '
' + - '
' + roomName.htmlEnc() + '
' + + html += '
' + + '
' + room_name.htmlEnc() + '
' + '' + Common._e("Join") + ''; - + // This room is yet a favorite - if(DataStore.existDB('favorites', roomXID)) - html += '' + Common._e("Remove") + ''; - else - html += '' + Common._e("Add") + ''; - + if(DataStore.existDB(Connection.desktop_hash, 'favorites', room_xid)) { + html += '' + + Common._e("Remove") + + ''; + } else { + html += '' + + Common._e("Add") + + ''; + } + // Close the room HTML html += '
'; } }); - + // Append this code to the popup - $(path + 'fsearch-results').append(html); + path_sel.find('.fsearch-results').append(html); + } else { + path_sel.find('.fsearch-noresults').show(); } - - else - $(path + 'fsearch-noresults').show(); - + Console.info('Rooms retrieved: ' + from); } - - $(path + 'wait').hide(); + + path_sel.find('.wait').hide(); } catch(e) { Console.error('Favorites.handleGCList', e); } @@ -556,7 +597,14 @@ var Favorites = (function () { try { self.quit(); - Chat.checkCreate(room, 'groupchat', '', '', Common.getXIDNick(room)); + + Chat.checkCreate( + room, + 'groupchat', + '', + '', + Common.getXIDNick(room) + ); } catch(e) { Console.error('Favorites.join', e); } finally { @@ -581,15 +629,22 @@ var Favorites = (function () { try { // Generate the HTML code var html = ''; - + // Remove the existing favorite self.remove(xid, false); - + // We complete the select forms $('#roster .gc-join-first-option, #favorites .fedit-head-select-first-option').after(html); - + // We store the informations - var value = '' + xid.htmlEnc() + '' + name.htmlEnc() + '' + nick.htmlEnc() + '' + autojoin.htmlEnc() + '' + password.htmlEnc() + ''; + var value = '' + + '' + xid.htmlEnc() + '' + + '' + name.htmlEnc() + '' + + '' + nick.htmlEnc() + '' + + '' + autojoin.htmlEnc() + '' + + '' + password.htmlEnc() + '' + + ''; + DataStore.setDB(Connection.desktop_hash, 'favorites', xid, value); } catch(e) { Console.error('Favorites.display', e); @@ -608,27 +663,34 @@ var Favorites = (function () { try { // Initialize the HTML code var html = ''; - + // Read the database var db_regex = new RegExp(('^' + Connection.desktop_hash + '_') + 'favorites_(.+)'); for(var i = 0; i < DataStore.storageDB.length; i++) { // Get the pointer values var current = DataStore.storageDB.key(i); - + // If the pointer is on a stored favorite if(current.match(db_regex)) { var data = Common.XMLFromString(DataStore.storageDB.getItem(current)); - + // Add the current favorite to the HTML code - html += ''; + html += ''; } } - + // Generate specific HTML code - var favorites_bubble = '' + html; - var favorites_popup = '' + html; - + var favorites_bubble = '' + html; + + var favorites_popup = '' + html; + // Append the HTML code $('#roster .buddy-conf-groupchat-select').html(favorites_bubble); $('#favorites .fedit-head-select').html(favorites_popup); @@ -647,63 +709,65 @@ var Favorites = (function () { self.instance = function() { try { - var path = '#favorites .'; - + var favorites_sel = $('#favorites'); + // Keyboard events - $(path + 'fsearch-head-server').keyup(function(e) { + favorites_sel.find('.fsearch-head-server').keyup(function(e) { if(e.keyCode == 13) { + var this_sel = $(this); + // No value? - if(!$(this).val()) - $(this).val(HOST_MUC); - + if(!this_sel.val()) { + this_sel.val(HOST_MUC); + } + // Get the list self.getGCList(); } }); - - $(path + 'fedit-line input').keyup(function(e) { + + favorites_sel.find('.fedit-line input').keyup(function(e) { if(e.keyCode == 13) { // Edit a favorite - if($(path + 'fedit-edit').is(':visible')) + if(favorites_sel.find('.fedit-edit').is(':visible')) { self.terminateThis('edit'); - - // Add a favorite - else + } else { self.terminateThis('add'); + } } }); - + // Change events $('.fedit-head-select').change(self.edit); - + // Click events - $(path + 'room-switcher').click(function() { - $(path + 'favorites-content').hide(); + favorites_sel.find('.room-switcher').click(function() { + favorites_sel.find('.favorites-content').hide(); self.reset(); }); - - $(path + 'room-list').click(function() { - $(path + 'favorites-edit').show(); + + favorites_sel.find('.room-list').click(function() { + favorites_sel.find('.favorites-edit').show(); }); - - $(path + 'room-search').click(function() { - $(path + 'favorites-search').show(); + + favorites_sel.find('.room-search').click(function() { + favorites_sel.find('.favorites-search').show(); self.getGCList(); }); - - $(path + 'fedit-add').click(function() { + + favorites_sel.find('.fedit-add').click(function() { return self.terminateThis('add'); }); - - $(path + 'fedit-edit').click(function() { + + favorites_sel.find('.fedit-edit').click(function() { return self.terminateThis('edit'); }); - - $(path + 'fedit-remove').click(function() { + + favorites_sel.find('.fedit-remove').click(function() { return self.terminateThis('remove'); }); - - $(path + 'bottom .finish').click(function() { + + favorites_sel.find('.bottom .finish').click(function() { return self.quit(); }); } catch(e) { diff --git a/source/app/javascripts/features.js b/source/app/javascripts/features.js index e24bb5d..2e7ba1e 100644 --- a/source/app/javascripts/features.js +++ b/source/app/javascripts/features.js @@ -50,29 +50,29 @@ var Features = (function () { var to = Utils.getServer(); var caps = con.server_caps; var xml = null; - + // Try to get the stored data if(caps) { xml = Common.XMLFromString( DataStore.getPersistent('global', 'caps', caps) ); } - + // Any stored data? if(xml) { self.handle(xml); - + Console.log('Read server CAPS from cache.'); } else { // Not stored (or no CAPS)! var iq = new JSJaCIQ(); - + iq.setTo(to); iq.setType('get'); iq.setQuery(NS_DISCO_INFO); - + con.send(iq, Caps.handleDiscoInfos); - + Console.log('Read server CAPS from network.'); } } catch(e) { @@ -93,7 +93,7 @@ var Features = (function () { try { // Selector var selector = $(xml); - + // Functions var check_feature_fn = function(namespace) { // This weird selector fixes an IE8 bug... @@ -103,7 +103,7 @@ var Features = (function () { }; // Markers - var namespaces = [NS_PUBSUB, NS_PUBSUB_CN, NS_URN_MAM, NS_COMMANDS, NS_URN_CARBONS]; + var namespaces = [NS_PUBSUB, NS_PUBSUB_CN, NS_URN_MAM, NS_COMMANDS, NS_URN_CARBONS, NS_URN_CORRECT]; var identity = selector.find('identity'); @@ -137,21 +137,21 @@ var Features = (function () { // Get the PEP nodes to initiate Microblog.getInit(); PEP.getInitGeoloc(); - + // Get the notifications Notification.get(); - + // Geolocate the user PEP.geolocate(); - + // Enable microblogging send tools Microblog.wait('sync'); $('.postit.attach').css('display', 'block'); - + Console.info('XMPP server supports PEP.'); } else { Microblog.wait('unsync'); - + Console.warn('XMPP server does not support PEP.'); } @@ -159,10 +159,10 @@ var Features = (function () { if(features.pep === false && features[NS_URN_MAM] === false) { $('#options fieldset.privacy').hide(); } - + // Apply the features self.apply('talk'); - + // Process the roster height if(features.pep === true) { Roster.adapt(); @@ -192,12 +192,12 @@ var Features = (function () { try { // Path to the elements var path = '#' + id + ' .'; - + // PEP features if(self.enabledPEP()) { $(path + 'pep-hidable').show(); } - + // PubSub features if(self.enabledPubSub()) { $(path + 'pubsub-hidable').show(); @@ -207,7 +207,7 @@ var Features = (function () { if(self.enabledPubSubCN()) { $(path + 'pubsub-hidable-cn').show(); } - + // MAM features if(self.enabledMAM()) { $(path + 'mam-hidable').show(); @@ -218,12 +218,17 @@ var Features = (function () { if(self.enabledMAMPurge()) { $(path + 'mam-purge-hidable').show(); } - + + // Message correction features + if(self.enabledCorrection()) { + $(path + 'correction-hidable').show(); + } + // Commands features if(self.enabledCommands()) { $(path + 'commands-hidable').show(); } - + // XMPP links (browser feature) if(navigator.registerProtocolHandler) { $(path + 'xmpplinks-hidable').show(); @@ -385,6 +390,22 @@ var Features = (function () { }; + /** + * Returns the XMPP server correction support + * @public + * @return {boolean} + */ + self.enabledCorrection = function() { + + try { + return self.isEnabled(NS_URN_CORRECT); + } catch(e) { + Console.error('Features.enabledCorrection', e); + } + + }; + + /** * Normalizes the XMPP server name * @private diff --git a/source/app/javascripts/filter.js b/source/app/javascripts/filter.js index b5adba0..bda2f2e 100644 --- a/source/app/javascripts/filter.js +++ b/source/app/javascripts/filter.js @@ -20,6 +20,300 @@ var Filter = (function () { var self = {}; + /* Constants */ + self.message_regex = { + 'commands': { + 'me': /((^)|((.+)(>)))(\/me )([^<]+)/ + }, + + 'emotes': { + 'angry': [ + /(:-?@)($|\s|<)/gi, + '$2' + ], + + 'bat': [ + /(:-?\[)($|\s|<)/gi, + '$2' + ], + + 'beer': [ + /(\(B\))($|\s|<)/g, + '$2' + ], + + 'biggrin': [ + /((:-?D)|(XD))($|\s|<)/gi, + '$4' + ], + + 'blush': [ + /(:-?\$)($|\s|<)/gi, + '$2' + ], + + 'boy': [ + /(\(Z\))($|\s|<)/g, + '$2' + ], + + 'brflower': [ + /(\(W\))($|\s|<)/g, + '$2' + ], + + 'brheart': [ + /((<\/3)|(\(U\)))($|\s|<)/g, + '$4' + ], + + 'coffee': [ + /(\(C\))($|\s|<)/g, + '$2' + ], + + 'coolglasses': [ + /((8-\))|(\(H\)))($|\s|<)/g, + '$4' + ], + + 'cry': [ + /(:'-?\()($|\s|<)/gi, + '$2' + ], + + 'cuffs': [ + /(\(%\))($|\s|<)/g, + '$2' + ], + + 'devil': [ + /(\]:-?>)($|\s|<)/gi, + '$2' + ], + + 'drink': [ + /(\(D\))($|\s|<)/g, + '$2' + ], + + 'flower': [ + /(@}->--)($|\s|<)/gi, + '$2' + ], + + 'frowning': [ + /((:-?\/)|(:-?S))($|\s|<)/gi, + '$4' + ], + + 'girl': [ + /(\(X\))($|\s|<)/g, + '$2' + ], + + 'heart': [ + /((<3)|(\(L\)))($|\s|<)/g, + '$4' + ], + + 'hugleft': [ + /(\(}\))($|\s|<)/g, + '$2' + ], + + 'hugright': [ + /(\({\))($|\s|<)/g, + '$2' + ], + + 'kis': [ + /(:-?{})($|\s|<)/gi, + '$2' + ], + + 'lamp': [ + /(\(I\))($|\s|<)/g, + '$2' + ], + + 'lion': [ + /(:-?3)($|\s|<)/gi, + '$2' + ], + + 'mail': [ + /(\(E\))($|\s|<)/g, + '$2' + ], + + 'moon': [ + /(\(S\))($|\s|<)/g, + '$2' + ], + + 'music': [ + /(\(8\))($|\s|<)/g, + '$2' + ], + + 'oh': [ + /((=-?O)|(:-?O))($|\s|<)/gi, + '$4' + ], + + 'phone': [ + /(\(T\))($|\s|<)/g, + '$2' + ], + + 'photo': [ + /(\(P\))($|\s|<)/g, + '$2' + ], + + 'puke': [ + /(:-?!)($|\s|<)/gi, + '$2' + ], + + 'pussy': [ + /(\(@\))($|\s|<)/g, + '$2' + ], + + 'rainbow': [ + /(\(R\))($|\s|<)/g, + '$2' + ], + + 'smile': [ + /(:-?\))($|\s|<)/gi, + '$2' + ], + + 'star': [ + /(\(\*\))($|\s|<)/g, + '$2' + ], + + 'stare': [ + /(:-?\|)($|\s|<)/gi, + '$2' + ], + + 'thumbdown': [ + /(\(N\))($|\s|<)/g, + '$2' + ], + + 'thumbup': [ + /(\(Y\))($|\s|<)/g, + '$2' + ], + + 'tongue': [ + /(:-?P)($|\s|<)/gi, + '$2' + ], + + 'unhappy': [ + /(:-?\()($|\s|<)/gi, + '$2' + ], + + 'wink': [ + /(;-?\))($|\s|<)/gi, + '$2' + ] + + }, + + 'formatting': { + 'bold': [ + /(^|\s|>|\()((\*)([^<>'"\*]+)(\*))($|\s|<|\))/gi, + '$1$2$6' + ], + + 'italic': [ + /(^|\s|>|\()((\/)([^<>'"\/]+)(\/))($|\s|<|\))/gi, + '$1$2$6' + ], + + 'underline': [ + /(^|\s|>|\()((_)([^<>'"_]+)(_))($|\s|<|\))/gi, + '$1$2$6' + ] + } + + }; + + self.xhtml_allow = { + 'elements': [ + 'a', + 'abbr', + 'acronym', + 'address', + 'blockquote', + 'body', + 'br', + 'cite', + 'code', + 'dd', + 'dfn', + 'div', + 'dt', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'html', + 'kbd', + 'li', + 'ol', + 'p', + 'pre', + 'q', + 'samp', + 'span', + 'strong', + 'title', + 'ul', + 'var' + ], + + 'attributes': [ + 'accesskey', + 'alt', + 'charset', + 'cite', + 'class', + 'height', + 'href', + 'hreflang', + 'id', + 'longdesc', + 'profile', + 'rel', + 'rev', + 'src', + 'style', + 'tabindex', + 'title', + 'type', + 'uri', + 'version', + 'width', + 'xml:lang', + 'xmlns' + ] + }; + + /** * Generates a given emoticon HTML code * @public @@ -42,83 +336,64 @@ var Filter = (function () { /** * Filters a given message * @public - * @param {string} neutralMessage + * @param {string} message * @param {string} nick * @param {string} html_escape * @return {string} */ - self.message = function(neutralMessage, nick, html_escape) { + self.message = function(message, nick, html_escape) { try { - var filteredMessage = neutralMessage; - + var filtered = message; + // We encode the HTML special chars - if(html_escape) - filteredMessage = filteredMessage.htmlEnc(); - - // /me command - filteredMessage = filteredMessage.replace(/((^)|((.+)(>)))(\/me )([^<]+)/, nick + ' $7') - - // We replace the smilies text into images - .replace(/(:-?@)($|\s|<)/gi, self.emoteImage('angry', '$1', '$2')) - .replace(/(:-?\[)($|\s|<)/gi, self.emoteImage('bat', '$1', '$2')) - .replace(/(\(B\))($|\s|<)/g, self.emoteImage('beer', '$1', '$2')) - .replace(/((:-?D)|(XD))($|\s|<)/gi, self.emoteImage('biggrin', '$1', '$4')) - .replace(/(:-?\$)($|\s|<)/gi, self.emoteImage('blush', '$1', '$2')) - .replace(/(\(Z\))($|\s|<)/g, self.emoteImage('boy', '$1', '$2')) - .replace(/(\(W\))($|\s|<)/g, self.emoteImage('brflower', '$1', '$2')) - .replace(/((<\/3)|(\(U\)))($|\s|<)/g, self.emoteImage('brheart', '$1', '$4')) - .replace(/(\(C\))($|\s|<)/g, self.emoteImage('coffee', '$1', '$2')) - .replace(/((8-\))|(\(H\)))($|\s|<)/g, self.emoteImage('coolglasses', '$1', '$4')) - .replace(/(:'-?\()($|\s|<)/gi, self.emoteImage('cry', '$1', '$2')) - .replace(/(\(%\))($|\s|<)/g, self.emoteImage('cuffs', '$1', '$2')) - .replace(/(\]:-?>)($|\s|<)/gi, self.emoteImage('devil', '$1', '$2')) - .replace(/(\(D\))($|\s|<)/g, self.emoteImage('drink', '$1', '$2')) - .replace(/(@}->--)($|\s|<)/gi, self.emoteImage('flower', '$1', '$2')) - .replace(/((:-?\/)|(:-?S))($|\s|<)/gi, self.emoteImage('frowning', '$1', '$4')) - .replace(/(\(X\))($|\s|<)/g, self.emoteImage('girl', '$1', '$2')) - .replace(/((<3)|(\(L\)))($|\s|<)/g, self.emoteImage('heart', '$1', '$4')) - .replace(/(\(}\))($|\s|<)/g, self.emoteImage('hugleft', '$1', '$2')) - .replace(/(\({\))($|\s|<)/g, self.emoteImage('hugright', '$1', '$2')) - .replace(/(:-?{})($|\s|<)/gi, self.emoteImage('kiss', '$1', '$2')) - .replace(/(\(I\))($|\s|<)/g, self.emoteImage('lamp', '$1', '$2')) - .replace(/(:-?3)($|\s|<)/gi, self.emoteImage('lion', '$1', '$2')) - .replace(/(\(E\))($|\s|<)/g, self.emoteImage('mail', '$1', '$2')) - .replace(/(\(S\))($|\s|<)/g, self.emoteImage('moon', '$1', '$2')) - .replace(/(\(8\))($|\s|<)/g, self.emoteImage('music', '$1', '$2')) - .replace(/((=-?O)|(:-?O))($|\s|<)/gi, self.emoteImage('oh', '$1', '$4')) - .replace(/(\(T\))($|\s|<)/g, self.emoteImage('phone', '$1', '$2')) - .replace(/(\(P\))($|\s|<)/g, self.emoteImage('photo', '$1', '$2')) - .replace(/(:-?!)($|\s|<)/gi, self.emoteImage('puke', '$1', '$2')) - .replace(/(\(@\))($|\s|<)/g, self.emoteImage('pussy', '$1', '$2')) - .replace(/(\(R\))($|\s|<)/g, self.emoteImage('rainbow', '$1', '$2')) - .replace(/(:-?\))($|\s|<)/gi, self.emoteImage('smile', '$1', '$2')) - .replace(/(\(\*\))($|\s|<)/g, self.emoteImage('star', '$1', '$2')) - .replace(/(:-?\|)($|\s|<)/gi, self.emoteImage('stare', '$1', '$2')) - .replace(/(\(N\))($|\s|<)/g, self.emoteImage('thumbdown', '$1', '$2')) - .replace(/(\(Y\))($|\s|<)/g, self.emoteImage('thumbup', '$1', '$2')) - .replace(/(:-?P)($|\s|<)/gi, self.emoteImage('tongue', '$1', '$2')) - .replace(/(:-?\()($|\s|<)/gi, self.emoteImage('unhappy', '$1', '$2')) - .replace(/(;-?\))($|\s|<)/gi, self.emoteImage('wink', '$1', '$2')) - - // Text in bold - .replace(/(^|\s|>|\()((\*)([^<>'"\*]+)(\*))($|\s|<|\))/gi, '$1$2$6') - - // Italic text - .replace(/(^|\s|>|\()((\/)([^<>'"\/]+)(\/))($|\s|<|\))/gi, '$1$2$6') - - // Underlined text - .replace(/(^|\s|>|\()((_)([^<>'"_]+)(_))($|\s|<|\))/gi, '$1$2$6'); - - // Add the links if(html_escape) { - filteredMessage = Links.apply(filteredMessage, 'desktop'); + filtered = filtered.htmlEnc(); } - - // Filter integratebox links - filteredMessage = IntegrateBox.filter(filteredMessage); - - return filteredMessage; + + // Security: don't filter huge messages (avoids crash attacks) + if(filtered.length < 10000) { + // /me command + filtered = filtered.replace(self.message_regex.commands.me, nick + ' $7'); + + // We replace the smilies text into images + var cur_emote; + + for(var cur_emote_name in self.message_regex.emotes) { + cur_emote = self.message_regex.emotes[cur_emote_name]; + + filtered = filtered.replace( + cur_emote[0], + self.emoteImage( + cur_emote_name, + '$1', + cur_emote[1] + ) + ); + } + + // Text formatting + var cur_formatting; + + for(var cur_formatting_name in self.message_regex.formatting) { + cur_formatting = self.message_regex.formatting[cur_formatting_name]; + + filtered = filtered.replace( + cur_formatting[0], + cur_formatting[1] + ); + } + + // Add the links + if(html_escape) { + filtered = Links.apply(filtered, 'desktop'); + } + + // Filter integratebox links + filtered = IntegrateBox.filter(filtered); + } + + return filtered; } catch(e) { Console.error('Filter.message', e); } @@ -126,6 +401,32 @@ var Filter = (function () { }; + /** + * Returns whether XHTML body exists or not + * @public + * @param {DOM} xhtml_sel + * @return {boolean} + */ + self.has_xhtml_body = function(xhtml_sel) { + + var has_xhtml_body = false; + + try { + xhtml_sel.find('*').each(function() { + if($(this).text()) { + has_xhtml_body = true; + return false; + } + }); + } catch(e) { + Console.error('Filter.has_xhtml_body', e); + } finally { + return has_xhtml_body; + } + + }; + + /** * Filters a xHTML message to be displayed in Jappix * @public @@ -135,106 +436,46 @@ var Filter = (function () { self.xhtml = function(code) { try { - // Allowed elements array - var elements = new Array( - 'a', - 'abbr', - 'acronym', - 'address', - 'blockquote', - 'body', - 'br', - 'cite', - 'code', - 'dd', - 'dfn', - 'div', - 'dt', - 'em', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'head', - 'html', - 'kbd', - 'li', - 'ol', - 'p', - 'pre', - 'q', - 'samp', - 'span', - 'strong', - 'title', - 'ul', - 'var' - ); - - // Allowed attributes array - var attributes = new Array( - 'accesskey', - 'alt', - 'charset', - 'cite', - 'class', - 'height', - 'href', - 'hreflang', - 'id', - 'longdesc', - 'profile', - 'rel', - 'rev', - 'src', - 'style', - 'tabindex', - 'title', - 'type', - 'uri', - 'version', - 'width', - 'xml:lang', - 'xmlns' - ); - + var code_sel = $(code); + // Check if Filter for XHTML-IM images is enabled if(DataStore.getDB(Connection.desktop_hash, 'options', 'no-xhtml-images') != '1') { - elements.push("img"); + self.xhtml_allow.elements.push("img"); } - + // Remove forbidden elements - $(code).find('html body *').each(function() { + code_sel.find('html body *').each(function() { // This element is not authorized - if(!Utils.existArrayValue(elements, (this).nodeName.toLowerCase())) + if(!Utils.existArrayValue(self.xhtml_allow.elements, (this).nodeName.toLowerCase())) { $(this).remove(); + } }); - + // Remove forbidden attributes - $(code).find('html body *').each(function() { + code_sel.find('html body *').each(function() { // Put a pointer on this element (jQuery way & normal way) var cSelector = $(this); var cElement = (this); - + // Loop the attributes of the current element $(cElement.attributes).each(function(index) { // Read the current attribute var cAttr = cElement.attributes[index]; var cName = cAttr.name; var cVal = cAttr.value; - + // This attribute is not authorized, or contains JS code - if(!Utils.existArrayValue(attributes, cName.toLowerCase()) || ((cVal.toLowerCase()).match(/(^|"|')javascript:/))) + if(!Utils.existArrayValue(self.xhtml_allow.attributes, cName.toLowerCase()) || + ((cVal.toLowerCase()).match(/(^|"|')javascript:/))) { cSelector.removeAttr(cName); + } }); }); - + // Filter some other elements - $(code).find('a').attr('target', '_blank'); - - return $(code).find('html body').html(); + code_sel.find('a').attr('target', '_blank'); + + return code_sel.find('html body').html(); } catch(e) { Console.error('Filter.xhtml', e); } diff --git a/source/app/javascripts/groupchat.js b/source/app/javascripts/groupchat.js index 1a5b778..21ed7da 100644 --- a/source/app/javascripts/groupchat.js +++ b/source/app/javascripts/groupchat.js @@ -21,7 +21,199 @@ var Groupchat = (function () { /* Variables */ - var JOIN_SUGGEST = []; + self.join_suggest = []; + + + /** + * Apply generate events + * @private + * @param {object} input_sel + * @param {string} hash + * @param {string} room + * @return {undefined} + */ + self._createEvents = function(input_sel, hash, room) { + + try { + self._createEventsInput(input_sel, hash); + self._createEventsKey(input_sel, hash, room); + } catch(e) { + Console.error('Groupchat._createEvents', e); + } + + }; + + + /** + * Apply generate events (input) + * @private + * @param {object} input_sel + * @param {string} hash + * @return {undefined} + */ + self._createEventsInput = function(input_sel, hash) { + + try { + // Focus event + input_sel.focus(function() { + // Clean notifications for this chat + Interface.chanCleanNotify(hash); + + // Store focus on this chat! + Interface.chat_focus_hash = hash; + }); + + // Blur event + input_sel.blur(function() { + // Reset storage about focus on this chat! + if(Interface.chat_focus_hash == hash) { + Interface.chat_focus_hash = null; + } + + // Reset autocompletion + Autocompletion.reset(hash); + }); + } catch(e) { + Console.error('Groupchat._createEventsInput', e); + } + + }; + + + /** + * Apply generate events (key) + * @private + * @param {object} input_sel + * @param {string} hash + * @param {string} room + * @return {undefined} + */ + self._createEventsKey = function(input_sel, hash, room) { + + try { + // Lock to the input + input_sel.keydown(function(e) { + // Enter key + if(e.keyCode == 13) { + // If shift key (without any others modifiers) was pressed, add a new line + if(e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { + input_sel.val(input_sel.val() + '\n'); + } else { + if(Correction.isIn(room) === true) { + var corrected_value = input_sel.val().trim(); + + if(corrected_value) { + // Send the corrected message + Correction.send(room, 'groupchat', corrected_value); + } + + Correction.leave(room); + } else { + // Send the message + Message.send(hash, 'groupchat'); + + // Reset the composing database entry + DataStore.setDB(Connection.desktop_hash, 'chatstate', room, 'off'); + } + } + + return false; + } + + // Remove chars (leave correction) + else if(e.keyCode == 8) { + // Leave correction mode? (another way, by flushing input value progressively) + if(Correction.isIn(room) === true && !input_sel.val()) { + Correction.leave(room); + } + } + + // Tabulation key (without any modifiers) + else if(!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey && e.keyCode == 9) { + Autocompletion.create(hash); + + return false; + } + + // Reset the autocompleter + else { + Autocompletion.reset(hash); + } + }); + + input_sel.keyup(function(e) { + if(e.keyCode == 27) { + // Escape key + input_sel.val(''); + + // Leave correction mode? (simple escape way) + if(Correction.isIn(room) === true) { + Correction.leave(room); + } + } else { + Correction.detect(room, input_sel); + } + }); + } catch(e) { + Console.error('Groupchat._createEventsKey', e); + } + + }; + + + /** + * Apply suggest check events + * @private + * @return {undefined} + */ + self._suggestCheckEvents = function() { + + try { + // Click events + $('#suggest .content a.one').click(function() { + var this_sel = $(this); + + // Add/remove the active class + this_sel.toggleClass('active'); + + // We require at least one room to be chosen + if(Common.exists('#suggest .content a.one.active')) { + $('#suggest a.next').removeClass('disabled'); + } else { + $('#suggest a.next').addClass('disabled'); + } + + return false; + }); + + $('#suggest a.next').click(function() { + var this_sel = $(this); + + // Disabled? + if(this_sel.hasClass('disabled')) { + return false; + } + + // Store groupchats to join? + if(this_sel.is('.continue')) { + $('#suggest .content a.one.active').each(function() { + self.join_suggest.push( + $(this).attr('data-xid') + ); + }); + } + + // Switch to talk UI + $('#suggest').remove(); + Connection.triggerConnected(); + + return false; + }); + } catch(e) { + Console.error('Groupchat._suggestCheckEvents', e); + } + + }; /** @@ -37,17 +229,20 @@ var Groupchat = (function () { try { // We must be in the "login" mode - if(Utils.isAnonymous()) + if(Utils.isAnonymous()) { return; - + } + // We check if the user is a room owner or administrator to give him privileges - if(affiliation == 'owner' || affiliation == 'admin') + if(affiliation == 'owner' || affiliation == 'admin') { $('#' + id + ' .tools-mucadmin').show(); - + } + // We check if the room hasn't been yet created - if(statuscode == 201) + if(statuscode == 201) { Board.openThisInfo(4); - + } + // We add the click event $('#' + id + ' .tools-mucadmin').click(function() { MUCAdmin.open(xid, affiliation); @@ -72,33 +267,35 @@ var Groupchat = (function () { try { // Room hash var hash = hex_md5(room); - + // Reset the elements $('#' + hash + ' .muc-ask').remove(); $('#' + hash + ' .compose').show(); - + // No nickname? if(!nickname) { // Get some values - if(!Utils.isAnonymous()) + if(!Utils.isAnonymous()) { nickname = Name.getNick(); - else + } else { nickname = ANONYMOUS_NICK; - + } + // If the nickname could not be retrieved, ask it - if(!nickname) + if(!nickname) { self.generateMUCAsk('nickname', room, hash, nickname, password); + } } - + // Got our nickname? if(nickname) { // Get our general presence var show = DataStore.getDB(Connection.desktop_hash, 'presence-show', 1); var status = DataStore.getDB(Connection.desktop_hash, 'options', 'presence-status'); - + // Set my nick $('#' + hash).attr('data-nick', escape(nickname)); - + // Send the appropriate presence Presence.send(room + '/' + nickname, '', show, status, '', true, password, self.handleMUC); } @@ -126,43 +323,50 @@ var Groupchat = (function () { var room = Common.bareXID(from); var nickname = Common.thisResource(from); var hash = hex_md5(room); - + var id = presence.getID(); + // No ID: must fix M-Link bug - if(presence.getID() === null) { - presence.setID(1); + if(id === null) { + id = 1; + presence.setID(id); } - + Console.info('First MUC presence: ' + from); - + // Catch the errors if(!Errors.handle(xml)) { // Define some stuffs var muc_user = $(xml).find('x[xmlns="' + NS_MUC_USER + '"]'); var affiliation = muc_user.find('item').attr('affiliation'); - var statuscode = parseInt(muc_user.find('status').attr('code')); - + var statuscode = parseInt(muc_user.find('status').attr('code')); + // Handle my presence Presence.handle(presence); - + + // Configure the new room + if(affiliation == 'owner' || affiliation == 'admin') { + self._initialConfiguration(id, room); + } + // Check if I am a room owner self.openAdmin(affiliation, hash, room, statuscode); - + // Tell the MUC we can notify the incoming presences $(document).oneTime('15s', function() { $('#' + hash).attr('data-initial', 'true'); }); - + // Enable the chatting input $(document).oneTime(10, function() { $('#' + hash + ' .message-area').removeAttr('disabled').focus(); }); } - + // A password is required else if($(xml).find('error[type="auth"] not-authorized').size()) { self.generateMUCAsk('password', room, hash, nickname); } - + // There's a nickname conflict else if($(xml).find('error[type="cancel"] conflict').size()) { self.generateMUCAsk('nickname', room, hash); @@ -189,34 +393,34 @@ var Groupchat = (function () { try { // Generate the path to the elements var path_to = '#' + hash + ' .muc-ask'; - + // Define the label text var label_text; - + switch(type) { case 'nickname': label_text = Common._e("Nickname"); break; - + case 'password': label_text = Common._e("Password"); break; } - + // Create the HTML markup $('#' + hash + ' .compose').hide(); - + $('#' + hash).append( - '
' + - '' + - '' + + '
' + + '' + + '' + '
' ); - + // When a key is pressed in the input $(path_to + ' input').keyup(function(e) { var value_input = $(this).val(); - + // Enter key pressed if(e.keyCode == 13) { // $.trim() fixes #304 @@ -224,14 +428,14 @@ var Groupchat = (function () { nickname = $.trim(value_input); return self.getMUC(room, nickname, password); } - + if(type == 'password' && value_input) { password = value_input; return self.getMUC(room, nickname, password); } } }); - + // Focus on the input $(document).oneTime(10, function() { $(path_to + ' input').focus(); @@ -259,86 +463,37 @@ var Groupchat = (function () { try { Console.info('New groupchat: ' + room); - + // Create the chat content Chat.generate('groupchat', hash, room, chan); - + // Create the chat switcher Chat.generateSwitch('groupchat', hash, room, chan); - + // The icons-hover functions Tooltip.icons(room, hash); - + // Click event on the add tool $('#' + hash + ' .tools-add').click(function() { // Hide the icon (to tell the user all is okay) $(this).hide(); - + // Add the groupchat to the user favorites Favorites.addThis(room, chan); }); - - // Must show the add button? - if(!DataStore.existDB('favorites', room)) - $('#' + hash + ' .tools-add').show(); - - // The event handlers - var inputDetect = $('#' + hash + ' .message-area'); - - // Focus event - inputDetect.focus(function() { - // Clean notifications for this chat - Interface.chanCleanNotify(hash); - - // Store focus on this chat! - Interface.chat_focus_hash = hash; - }); - - // Blur event - inputDetect.blur(function() { - // Reset storage about focus on this chat! - if(Interface.chat_focus_hash == hash) - Interface.chat_focus_hash = null; - // Reset autocompletion - Autocompletion.reset(hash); - }); - - // Lock to the input - inputDetect.keydown(function(e) { - // Enter key - if(e.keyCode == 13) { - // If shift key (without any others modifiers) was pressed, add a new line - if(e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) - inputDetect.val(inputDetect.val() + '\n'); - - // Send the message - else { - Message.send(hash, 'groupchat'); - - // Reset the composing database entry - DataStore.setDB(Connection.desktop_hash, 'chatstate', room, 'off'); - } - - return false; - } - - // Tabulation key (without any modifiers) - else if(!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey && e.keyCode == 9) { - Autocompletion.create(hash); - - return false; - } - - // Reset the autocompleter - else { - Autocompletion.reset(hash); - } - }); - + // Must show the add button? + if(!DataStore.existDB(Connection.desktop_hash, 'favorites', room)) { + $('#' + hash + ' .tools-add').show(); + } + + // The event handlers + var input_sel = $('#' + hash + ' .message-area'); + self._createEvents(input_sel, hash, room); + // Chatstate events - ChatState.events(inputDetect, room, hash, 'groupchat'); - + ChatState.events(input_sel, room, hash, 'groupchat'); + // Get the current muc informations and content self.getMUC(room, nickname, password); } catch(e) { @@ -359,27 +514,30 @@ var Groupchat = (function () { // Values array var muc_arr = [GROUPCHATS_JOIN]; var new_arr = []; - + // Try to split it - if(GROUPCHATS_JOIN.indexOf(',') != -1) + if(GROUPCHATS_JOIN.indexOf(',') != -1) { muc_arr = GROUPCHATS_JOIN.split(','); - + } + for(var i in muc_arr) { // Get the current value var muc_current = $.trim(muc_arr[i]); - + // No current value? - if(!muc_current) + if(!muc_current) { continue; - + } + // Filter the current value muc_current = Common.generateXID(muc_current, 'groupchat'); - + // Add the current value - if(!Utils.existArrayValue(new_arr, muc_current)) + if(!Utils.existArrayValue(new_arr, muc_current)) { new_arr.push(muc_current); + } } - + return new_arr; } catch(e) { Console.error('Groupchat.arrayJoin', e); @@ -397,13 +555,14 @@ var Groupchat = (function () { try { // Nothing to join? - if(!JOIN_SUGGEST) + if(!self.join_suggest) { return; - + } + // Join the chats - if(JOIN_SUGGEST.length) { - for(var g in JOIN_SUGGEST) { - Chat.checkCreate(JOIN_SUGGEST[g], 'groupchat'); + if(self.join_suggest.length) { + for(var g in self.join_suggest) { + Chat.checkCreate(self.join_suggest[g], 'groupchat'); } } } catch(e) { @@ -422,16 +581,17 @@ var Groupchat = (function () { try { var groupchat_arr = self.arrayJoin(); - + // Must suggest the user? if((GROUPCHATS_SUGGEST == 'on') && groupchat_arr.length) { - if(Common.exists('#suggest')) + if(Common.exists('#suggest')) { return; - + } + // Create HTML code var html = '
'; html += '
' + Common._e("Suggested chatrooms") + '
'; - + html += '
'; for(var g in groupchat_arr) { html += ''; @@ -442,52 +602,21 @@ var Groupchat = (function () { html += ''; } html += '
'; - + html += '
'; html += ''; html += ''; html += '
'; html += '
'; - + // Append HTML code $('body').append(html); - - // Click events - $('#suggest .content a.one').click(function() { - // Add/remove the active class - $(this).toggleClass('active'); - - // We require at least one room to be chosen - if(Common.exists('#suggest .content a.one.active')) - $('#suggest a.next').removeClass('disabled'); - else - $('#suggest a.next').addClass('disabled'); - - return false; - }); - - $('#suggest a.next').click(function() { - // Disabled? - if($(this).hasClass('disabled')) { - return false; - } - - // Store groupchats to join? - if($(this).is('.continue')) { - $('#suggest .content a.one.active').each(function() { - JOIN_SUGGEST.push($(this).attr('data-xid')); - }); - } - - // Switch to talk UI - $('#suggest').remove(); - Connection.triggerConnected(); - - return false; - }); + + // Attach events + self._suggestCheckEvents(); } else { - JOIN_SUGGEST = groupchat_arr; - + self.join_suggest = groupchat_arr; + Connection.triggerConnected(); } } catch(e) { @@ -512,20 +641,26 @@ var Groupchat = (function () { if(!ban_xid) { Board.openThisInfo(6); - Console.warning('Could not ban user with XID: ' + ban_xid + ' from room: ' + room_xid); + Console.warn('Could not ban user with XID: ' + ban_xid + ' from room: ' + room_xid); } else { // We generate the ban IQ var iq = new JSJaCIQ(); iq.setTo(room_xid); iq.setType('set'); - + var iqQuery = iq.setQuery(NS_MUC_ADMIN); - var item = iqQuery.appendChild(iq.buildNode('item', {'affiliation': 'outcast', 'jid': ban_xid, 'xmlns': NS_MUC_ADMIN})); - + var item = iqQuery.appendChild(iq.buildNode('item', { + 'affiliation': 'outcast', + 'jid': ban_xid, + 'xmlns': NS_MUC_ADMIN + })); + if(reason) { - item.appendChild(iq.buildNode('reason', {'xmlns': NS_MUC_ADMIN}, reason)); + item.appendChild(iq.buildNode('reason', { + 'xmlns': NS_MUC_ADMIN + }, reason)); } - + con.send(iq, Errors.handleReply); Console.log('Banned user with XID: ' + ban_xid + ' from room: ' + room_xid); @@ -559,14 +694,20 @@ var Groupchat = (function () { var iq = new JSJaCIQ(); iq.setTo(room_xid); iq.setType('set'); - + var iqQuery = iq.setQuery(NS_MUC_ADMIN); - var item = iqQuery.appendChild(iq.buildNode('item', {'nick': nick, 'role': 'none', 'xmlns': NS_MUC_ADMIN})); - + var item = iqQuery.appendChild(iq.buildNode('item', { + 'nick': nick, + 'role': 'none', + 'xmlns': NS_MUC_ADMIN + })); + if(reason) { - item.appendChild(iq.buildNode('reason', {'xmlns': NS_MUC_ADMIN}, reason)); + item.appendChild(iq.buildNode('reason', { + 'xmlns': NS_MUC_ADMIN + }, reason)); } - + con.send(iq, Errors.handleReply); Console.info('Kicked user "' + nick + '" from room: ' + room_xid); @@ -671,10 +812,54 @@ var Groupchat = (function () { }; + /** + * Sends initial configuration of the room + * @private + * @param {string} pid + * @param {string} xid + * @return {undefined} + */ + self._initialConfiguration = function(pid, xid) { + + try { + var iq = new JSJaCIQ(); + + iq.setTo(xid); + iq.setType('set'); + iq.setID('first-muc-config-' + pid); + + var iqQuery = iq.setQuery(NS_MUC_OWNER); + + // Configure room with nil(null) fields + var iqX = iqQuery.appendChild(iq.buildNode('x', { + 'xmlns': NS_XDATA, + 'type': 'submit' + })); + + // Build a new field node + var iqField = iqX.appendChild(iq.buildNode('field', { + 'var': 'FORM_TYPE', + 'type': 'hidden', + 'xmlns': NS_XDATA + })); + + iqField.appendChild(iq.buildNode('value', { + 'xmlns': NS_XDATA + }, NS_MUC_CONFIG)); + + con.send(iq); + + Console.info('Groupchat._initialConfiguration', 'Sent initial room configuration: ' + xid); + } catch(e) { + Console.error('Groupchat._initialConfiguration', e); + } + }; + + /** * Return class scope */ return self; -})(); \ No newline at end of file +})(); diff --git a/source/app/javascripts/home.js b/source/app/javascripts/home.js index a733181..06f8672 100644 --- a/source/app/javascripts/home.js +++ b/source/app/javascripts/home.js @@ -20,6 +20,119 @@ var Home = (function () { var self = {}; + /** + * Apply change events + * @private + * @param {object} current_sel + * @param {string} div + * @return {undefined} + */ + self._eventsChange = function(current_sel, div) { + + try { + // Create the attached events + switch(div) { + // Login tool + case 'loginer': + current_sel.find('a.to-anonymous').click(function() { + return self.change('anonymouser'); + }); + + current_sel.find('a.advanced').click(self.showAdvanced); + current_sel.find('form').submit(self.loginForm); + + break; + + // Anonymous login tool + case 'anonymouser': + current_sel.find('a.to-home').click(function() { + return self.change('loginer'); + }); + + current_sel.find('form').submit(Connection.doAnonymous); + + // Keyup event on anonymous join's room input + current_sel.find('input.room').keyup(function() { + var value = $(this).val(); + var report_sel = current_sel.find('.report'); + var span_sel = current_sel.find('span'); + + if(!value) { + report_sel.hide(); + span_sel.text(''); + } else { + report_sel.show(); + span_sel.text(JAPPIX_LOCATION + '?r=' + value); + } + }); + + break; + + // Register tool + case 'registerer': + // Server input change + $('#home input.server').keyup(function(e) { + if($.trim($(this).val()) == HOST_MAIN) { + $('#home .captcha_grp').show(); + $('#home input.captcha').removeAttr('disabled'); + } else { + $('#home .captcha_grp').hide(); + $('#home input.captcha').attr('disabled', true); + } + }); + + // Register input placeholder + // FIXME: breaks IE compatibility + //$('#home input[placeholder]').placeholder(); + + // Register form submit + current_sel.find('form').submit(self.registerForm); + + break; + } + } catch(e) { + Console.error('Home._eventsChange', e); + } + + }; + + + /** + * Create obsolete form + * @private + * @param {string} home + * @param {string} locale + * @return {undefined} + */ + self._obsolete = function(home, locale) { + + try { + // Add the code + $(locale).after( + '
' + + '

' + Common._e("Your browser is out of date!") + '

' + + + '' + + '' + + '' + + '' + + '' + + '
' + ); + + // Display it later + $(home + '.obsolete').oneTime('1s', function() { + $(this).slideDown(); + }); + + Console.warn('Jappix does not support this browser!'); + } catch(e) { + Console.error('Home._obsolete', e); + } + + }; + + /** * Allows the user to switch the difference home page elements * @public @@ -33,19 +146,19 @@ var Home = (function () { var home = '#home .'; var right = home + 'right '; var current = right + '.homediv.' + div; - + // We switch the div $(right + '.homediv, ' + right + '.top').hide(); $(right + '.' + div).show(); - + // We reset the homedivs $(home + 'homediv:not(.default), ' + home + 'top:not(.default)').remove(); - + // Get the HTML code to display var disable_form = ''; var lock_host = ''; var code = ''; - + // Apply the previous link switch(div) { case 'loginer': @@ -54,196 +167,144 @@ var Home = (function () { if(!Common.exists(right + '.top.sub')) { // Append the HTML code for previous link $(right + '.top.default').after('

«

'); - + // Click event on previous link $(home + 'top.sub a.previous').click(function() { return self.change('default'); }); } - + break; } - + // Apply the form switch(div) { // Login tool case 'loginer': lock_host = Utils.disableInput(LOCK_HOST, 'on'); - code = '

' + Common.printf(Common._e("Login to your existing XMPP account. You can also use the %s to join a groupchat."), '' + Common._e("anonymous mode") + '') + '

' + - - '
' + - '
' + - '' + Common._e("Required") + '' + - - '' + - '@' + - '' + - '' + - '' + - '' + - '
' + - - '' + Common._e("Advanced") + '' + - - '
' + - '' + Common._e("Advanced") + '' + - - '' + - '' + - '' + - '' + - '
' + - - '
' + - '' + + code = '

' + Common.printf(Common._e("Login to your existing XMPP account. You can also use the %s to join a groupchat."), '' + Common._e("anonymous mode") + '') + '

' + - '
' + - '
' + + '' + + '
' + + '' + Common._e("Required") + '' + + + '' + + '@' + + '' + + '' + + '' + + '' + + '
' + + + '' + Common._e("Advanced") + '' + + + '
' + + '' + Common._e("Advanced") + '' + + + '' + + '' + + '' + + '' + + '
' + + + '
' + + '' + + + '
' + + '
' + '
'; - + break; - + // Anonymous login tool case 'anonymouser': disable_form = Utils.disableInput(ANONYMOUS, 'off'); code = '

' + Common.printf(Common._e("Enter the groupchat you want to join and the nick you want to have. You can also go back to the %s."), '' + Common._e("login page") + '') + '

'; - - if(LEGAL) + + if(LEGAL) { code += '

' + Common.printf(Common._e("By using our service, you accept %s."), '' + Common._e("our terms of use") + '') + '

'; - - code += '
' + - '
' + - '' + Common._e("Required") + '' + - - '' + - '' + - - '' + - '' + - '
' + - - '' + - '
' + - - '
' + - Common._e("Share this link with your friends:") + ' ' + + } + + code += '
' + + '
' + + '' + Common._e("Required") + '' + + + '' + + '' + + + '' + + '' + + '
' + + + '' + + '
' + + + '
' + + Common._e("Share this link with your friends:") + ' ' + '
'; - + break; - + // Register tool case 'registerer': disable_form = Utils.disableInput(REGISTRATION, 'off'); - - if(!disable_form) + + if(!disable_form) { lock_host = Utils.disableInput(LOCK_HOST, 'on'); - + } + code = '

' + Common._e("Register a new XMPP account to join your friends on your own social cloud. That's simple!") + '

'; - - if(LEGAL) + + if(LEGAL) { code += '

' + Common.printf(Common._e("By using our service, you accept %s."), '' + Common._e("our terms of use") + '') + '

'; - - code += '
' + - '
' + - '' + Common._e("Required") + '' + - - '' + - '@' + - '' + + } + + code += '' + + '
' + + '' + Common._e("Required") + '' + + + '' + + '@' + + '' + ''; - - if(REGISTER_API == 'on') - code += '
' + - '' + + + if(REGISTER_API == 'on') { + code += '
' + + '' + '
'; - - code += '
' + - - '' + + } + + code += '
' + + + '' + '
'; - + break; } - + // Form disabled? - if(disable_form) - code += '
' + - Common._e("This tool has been disabled!") + + if(disable_form) { + code += '
' + + Common._e("This tool has been disabled!") + '
'; - + } + // Create this HTML code if(code && !Common.exists(current)) { - // Append it! - $(right + '.homediv.default').after('
' + code + '
'); - - // Create the attached events - switch(div) { - // Login tool - case 'loginer': - $(current + ' a.to-anonymous').click(function() { - return self.change('anonymouser'); - }); - - $(current + ' a.advanced').click(self.showAdvanced); - $(current + ' form').submit(self.loginForm); - - break; - - // Anonymous login tool - case 'anonymouser': - $(current + ' a.to-home').click(function() { - return self.change('loginer'); - }); - - $(current + ' form').submit(Connection.doAnonymous); - - // Keyup event on anonymous join's room input - $(current + ' input.room').keyup(function() { - var value = $(this).val(); - var report = current + ' .report'; - var span = report + ' span'; - - if(!value) { - $(report).hide(); - $(span).text(''); - } - - else { - $(report).show(); - $(span).text(JAPPIX_LOCATION + '?r=' + value); - } - }); - - break; - - // Register tool - case 'registerer': - // Server input change - $('#home input.server').keyup(function(e) { - if($.trim($(this).val()) == HOST_MAIN) { - $('#home .captcha_grp').show(); - $('#home input.captcha').removeAttr('disabled'); - } else { - $('#home .captcha_grp').hide(); - $('#home input.captcha').attr('disabled', true); - } - }); - - // Register input placeholder - // FIXME: breaks IE compatibility - //$('#home input[placeholder]').placeholder(); - - // Register form submit - $(current + ' form').submit(self.registerForm); - - break; - } + $(right + '.homediv.default').after( + '
' + code + '
' + ); + + self._eventsChange( + $(current), + div + ); } - + // We focus on the first input $(document).oneTime(10, function() { $(right + 'input:visible:first').focus(); @@ -267,7 +328,7 @@ var Home = (function () { try { // Hide the link $('#home a.advanced').hide(); - + // Show the fieldset $('#home fieldset.advanced').show(); } catch(e) { @@ -288,27 +349,29 @@ var Home = (function () { try { // We get the values - var lPath = '#home .loginer '; - var lServer = $(lPath + '.server').val(); - var lNick = Common.nodeprep($(lPath + '.nick').val()); - var lPass = $(lPath + '.password').val(); - var lResource = $(lPath + '.resource').val(); - var lPriority = $(lPath + '.priority').val(); - var lRemember = $(lPath + '.remember').filter(':checked').size(); - + var path_sel = $('#home .loginer'); + + var lServer = path_sel.find('.server').val(); + var lNick = Common.nodeprep(path_sel.find('.nick').val()); + var lPass = path_sel.find('.password').val(); + var lResource = path_sel.find('.resource').val(); + var lPriority = path_sel.find('.priority').val(); + var lRemember = path_sel.find('.remember').filter(':checked').size(); + // Enough values? if(lServer && lNick && lPass && lResource && lPriority) { Connection.doLogin(lNick, lServer, lPass, lResource, lPriority, lRemember); } else { $(lPath + 'input[type="text"], ' + lPath + 'input[type="password"]').each(function() { var select = $(this); - - if(!select.val()) + + if(!select.val()) { $(document).oneTime(10, function() { select.addClass('please-complete').focus(); }); - else - select.removeClass('please-complete'); + } else { + select.removeClass('please-complete'); + } }); } } catch(e) { @@ -328,38 +391,40 @@ var Home = (function () { self.registerForm = function() { try { - var rPath = '#home .registerer '; - + var path = '#home .registerer'; + var path_sel = $(path); + // Remove the success info - $(rPath + '.success').remove(); - + path_sel.find('.success').remove(); + // Get the values - var username = Common.nodeprep($(rPath + '.nick').val()); - var domain = $(rPath + '.server').val(); - var pass = $(rPath + '.password').val(); - var spass = $(rPath + '.spassword').val(); - var captcha = $(rPath + '.captcha').val(); - + var username = Common.nodeprep(path_sel.find('.nick').val()); + var domain = path_sel.find('.server').val(); + var pass = path_sel.find('.password').val(); + var spass = path_sel.find('.spassword').val(); + var captcha = path_sel.find('.captcha').val(); + // Enough values? if(domain && username && pass && spass && (pass == spass) && !((REGISTER_API == 'on') && (domain == HOST_MAIN) && !captcha)) { // We remove the not completed class to avoid problems $('#home .registerer input').removeClass('please-complete'); - + // Fire the register event! Connection.doRegister(username, domain, pass, captcha); } - + // Something is missing? else { - $(rPath + 'input[type="text"], ' + rPath + 'input[type="password"]').each(function() { + $(path + ' input[type="text"], ' + path + ' input[type="password"]').each(function() { var select = $(this); - - if(!select.val() || (select.is('#spassword') && pass && (pass != spass))) + + if(!select.val() || (select.is('#spassword') && pass && (pass != spass))) { $(document).oneTime(10, function() { select.addClass('please-complete').focus(); }); - else - select.removeClass('please-complete'); + } else { + select.removeClass('please-complete'); + } }); } } catch(e) { @@ -387,76 +452,61 @@ var Home = (function () { var corp = home + '.corporation'; var aboutus = home + '.aboutus'; var locale = home + '.locale'; - + // Removes the